New upstream version 11.3.10+dfsg

This commit is contained in:
Pirate Praveen 2018-11-20 20:47:30 +05:30
parent bd56fdbb00
commit b5b5f601e2
2431 changed files with 127294 additions and 16482 deletions

View file

@ -33,6 +33,15 @@ rules:
- error - error
- max: 1 - max: 1
promise/catch-or-return: error promise/catch-or-return: error
no-param-reassign:
- error
- props: true
ignorePropertyModificationsFor:
- "acc" # for reduce accumulators
- "accumulator" # for reduce accumulators
- "el" # for DOM elements
- "element" # for DOM elements
- "state" # for Vuex mutations
no-underscore-dangle: no-underscore-dangle:
- error - error
- allow: - allow:

View file

@ -1,34 +0,0 @@
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
app/controllers/projects/approver_groups_controller.rb
app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
app/models/project_services/packagist_service.rb
lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
lib/gitlab/ci/trace/chunked_io.rb
lib/gitlab/gitaly_client/ref_service.rb
lib/gitlab/gitaly_client/commit_service.rb
lib/gitlab/git/commit.rb
lib/gitlab/git/tag.rb
ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/api/epics.rb
ee/lib/api/geo_nodes.rb
ee/lib/ee/api/group_boards.rb
ee/lib/ee/api/boards.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
ee/spec/**/*

1
.gitignore vendored
View file

@ -77,3 +77,4 @@ eslint-report.html
/plugins/* /plugins/*
/.gitlab_pages_secret /.gitlab_pages_secret
package-lock.json package-lock.json
/junit_rspec.xml

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
@ -131,7 +131,7 @@ stages:
.single-script-job: &single-script-job .single-script-job: &single-script-job
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
stage: build stage: test
cache: {} cache: {}
dependencies: [] dependencies: []
variables: &single-script-job-variables variables: &single-script-job-variables
@ -171,7 +171,7 @@ stages:
- '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}' - '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}' - '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
@ -180,6 +180,8 @@ stages:
- knapsack/ - knapsack/
- rspec_flaky/ - rspec_flaky/
- tmp/capybara/ - tmp/capybara/
reports:
junit: junit_rspec.xml
.rspec-metadata-pg: &rspec-metadata-pg .rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata <<: *rspec-metadata
@ -311,10 +313,13 @@ review-docs-cleanup:
environment: environment:
name: review-docs/$CI_COMMIT_REF_SLUG name: review-docs/$CI_COMMIT_REF_SLUG
action: stop action: stop
when: manual
script: script:
- gem install gitlab --no-ri --no-rdoc - gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME cleanup - ./$SCRIPT_NAME cleanup
when: manual
only:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
## ##
# Trigger a docker image build in CNG (Cloud Native GitLab) repository # Trigger a docker image build in CNG (Cloud Native GitLab) repository
@ -322,7 +327,7 @@ review-docs-cleanup:
cloud-native-image: cloud-native-image:
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
stage: build stage: test
allow_failure: true allow_failure: true
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
@ -703,7 +708,6 @@ gitlab:assets:compile:
SETUP_DB: "false" SETUP_DB: "false"
SKIP_STORAGE_VALIDATION: "true" SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true" WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
# we override the max_old_space_size to prevent OOM errors # we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584 NODE_OPTIONS: --max_old_space_size=3584
script: script:
@ -717,6 +721,7 @@ gitlab:assets:compile:
expire_in: 31d expire_in: 31d
paths: paths:
- webpack-report/ - webpack-report/
- public/assets/
karma: karma:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
@ -739,7 +744,7 @@ karma:
- chrome_debug.log - chrome_debug.log
- coverage-javascript/ - coverage-javascript/
codequality: code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable image: docker:stable
allow_failure: true allow_failure: true
@ -757,9 +762,13 @@ codequality:
script: script:
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code - docker run
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts: artifacts:
paths: [codeclimate.json] paths: [gl-code-quality-report.json]
expire_in: 1 week expire_in: 1 week
sast: sast:

17
.gitlab/CODEOWNERS Normal file
View file

@ -0,0 +1,17 @@
# Backend Maintainers are the default for all ruby files
*.rb @ayufan @DouweM @dzaporozhets @grzesiek @nick.thomas @rspeicher @rymai @smcgivern
*.rake @ayufan @DouweM @dzaporozhets @grzesiek @nick.thomas @rspeicher @rymai @smcgivern
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
# Feature specific owners
/ee/lib/gitlab/code_owners/ @reprazent
/ee/lib/ee/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/auth/ldap/ @dblessing @mkozono

View file

@ -0,0 +1,100 @@
## Details
- **Feature Toggle Name**: `FEATURE_NAME`
- **Required GitLab Version**: `vX.X`
--------------------------------------------------------------------------------
## 1. Preparation
- [ ] **Controllers and workers**:
1. Please link to dashboards of the workers, and the controllers and actions that can be impacted
2. ...
3. ...
## 2. Development Trial
#### Check Dev Server Versions
- [ ] GitLab: https://dev.gitlab.org/help
#### Enable on `dev.gitlab.org`:
- [ ] `/chatops feature set FEATURE_NAME true --dev` in [`#dev-gitlab`](https://gitlab.slack.com/messages/C6WQ87MU3)
Then leave running while monitoring and performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
## 2. Staging Trial
#### Check Staging Server Versions
- [ ] GitLab: https://staging.gitlab.com/help
#### Enable on `staging.gitlab.com`
- [ ] `/chatops run feature set FEATURE_NAME true --staging` in [`#development`](https://gitlab.slack.com/messages/C02PF508L/)
Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 4. Production Server Version Check
- [ ] GitLab: https://gitlab.com/help
## 5. Initial Impact Check
- [ ] Enable for a subset of users, when using percentage gates: 1%.
Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 6. Low Impact Check
- [ ] Enable for a bigger subset of users, when using percentage gates: 10%.
Then leave running while monitoring for at least **30 minutes** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 7. Mid Impact Trial
- [ ] Enable for a big subset of users, when using percentage gates: 50%.
Then leave running while monitoring for at least **12 hours** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 8. Full Impact Trial
- [ ] Enable for all users: `/chatops run feature set FEATURE_NAME true
Then leave running while monitoring for at least **1 week**.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
#### Success?
- [ ] Remove the feature gate from the code, and close this issue with that MR.

View file

@ -0,0 +1,54 @@
<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation -->
<!-- Mention "documentation" or "docs" in the issue title -->
<!-- Use this description template for new docs or updates to existing docs. -->
<!-- Check the documentation structure guidelines for guidance: https://docs.gitlab.com/ee/development/documentation/structure.html-->
- [ ] Documents Feature A <!-- feature name -->
- [ ] Follow-up from: #XXX, !YYY <!-- Mention related issues, MRs, and epics when available -->
## New doc or update?
<!-- Mark either of these boxes: -->
- [ ] New documentation
- [ ] Update existing documentation
## Checklists
### Product Manager
<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#1-product-manager-s-role-in-the-documentation-process -->
- [ ] Add the correct labels
- [ ] Add the correct milestone
- [ ] Indicate the correct document/directory for this feature <!-- (ping the tech writers for help if you're not sure) -->
- [ ] Fill the doc blurb below
#### Documentation blurb
<!-- Documentation template: https://docs.gitlab.com/ee/development/documentation/structure.html#documentation-template-for-new-docs -->
- Doc **title**
<!-- write the doc title here -->
- Feature **overview/description**
<!-- Write the feature overview here -->
- Feature **use cases**
<!-- Write the use cases here -->
### Developer
<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process -->
- [ ] Copy the doc blurb above and paste it into the doc
- [ ] Write the tutorial - explain how to use the feature
- [ ] Submit the MR using the appropriate MR description template
/label ~Documentation

View file

@ -22,13 +22,13 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases - [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
- [ ] At this point, it might be easy to squash the commits from the MR into one - [ ] At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation] - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable) - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y` - [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. - [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script [secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
#### Documentation and final details #### Documentation and final details

View file

@ -0,0 +1,96 @@
# Test Plan
<!-- This issue outlines testing activities related to a particular issue or epic.
[Here is an example test plan](https://gitlab.com/gitlab-org/gitlab-ce/issues/50353)
This and other comments should be removed as you write the plan -->
## Introduction
<!-- Briefly outline what is being tested
Mention the issue(s) this test plan is related to -->
## Scope
<!-- State any limits on aspects of the feature being tested
Outline the types of data to be included
Outline the types of tests to be performed (functional, security, performance,
database, automated, etc) -->
## ACC Matrix
<!-- Use the matrix below as a template to identify the Attributes, Components, and
Capabilities relevant to the scope of this test plan. Add or remove Attributes
and Components as required and list Capabilities in the next section
Attributes (columns) are adverbs or adjectives that describe (at a high level)
the qualities testing is meant to ensure Components have.
Components (rows) are nouns that define major parts of the product being tested.
Capabilities link Attributes and Components. They are what your product needs to
do to make sure a Component fulfills an Attribute
For more information see the [Google Testing Blog article about the 10 minute
test plan](https://testing.googleblog.com/2011/09/10-minute-test-plan.html) and
[this wiki page from an open-source tool that implements the ACC
model](https://code.google.com/archive/p/test-analytics/wikis/AccExplained.wiki). -->
| | Simple | Secure | Responsive | Obvious | Stable |
|------------|:------:|:------:|:----------:|:-------:|:------:|
| Admin | | | | | |
| Groups | | | | | |
| Project | | | | | |
| Repository | | | | | |
| Issues | | | | | |
| MRs | | | | | |
| CI/CD | | | | | |
| Ops | | | | | |
| Registry | | | | | |
| Wiki | | | | | |
| Snippets | | | | | |
| Settings | | | | | |
| Tracking | | | | | |
| API | | | | | |
## Capabilities
<!-- Use the ACC matrix above to help you identify Capabilities at each relevant
intersection of Components and Attributes.
Some features might be simple enough that they only involve one Component, while
more complex features could involve multiple or even all.
Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353):
* Respository is
* Simple
* It's easy to select the desired file template
* It doesn't require unnecessary actions to save the change
* It's easy to undo the change after selecting a template
* Responsive
* The list of templates can be restricted to allow a user to find a specific template among many
* Once a template is selected the file content updates quickly and smoothly
-->
## Test Plan
<!-- If the scope is small enough you may not need to write a list of tests to
perform. It might be enough to use the Capabilities to guide your testing.
If the feature is more complex, especially if it involves multiple Components,
briefly outline a set of tests here. When identifying tests to perform be sure
to consider risk. Note inherent/known levels of risk so that testing can focus
on high risk areas first.
New end-to-end and integration tests (Selenium and API) should be added to the
[Test Coverage sheet](https://docs.google.com/spreadsheets/d/1RlLfXGboJmNVIPP9jgFV5sXIACGfdcFq1tKd7xnlb74/)
Please note if automated tests already exist.
When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ce/development/testing_guide/testing_levels.html)
in mind.
-->
/label ~Quality

View file

@ -0,0 +1,32 @@
<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/ -->
<!-- Use this description template for changing documentation location. For new docs or updates to existing docs, use the "Documentation" template -->
## What does this MR do?
<!-- Briefly describe what this MR is about -->
## Related issues
<!-- Mention the issue(s) this MR closes or is related to -->
Closes
## Moving docs to a new location?
Read the guidelines:
https://docs.gitlab.com/ce/development/documentation/index.html#changing-document-location
- [ ] Make sure the old link is not removed and has its contents replaced with
a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review.
/label ~Documentation

View file

@ -1,4 +1,8 @@
<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/index.html --> <!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation -->
<!-- Mention "documentation" or "docs" in the MR title -->
<!-- Use this description template for new docs or updates to existing docs. For changing documentation location use the "Change documentation location" template -->
## What does this MR do? ## What does this MR do?
@ -10,20 +14,19 @@
Closes Closes
## Moving docs to a new location? ## Author's checklist
Read the guidelines: - [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process)
https://docs.gitlab.com/ee/development/documentation/#changing-document-location - [ ] Crosslink the document from the higher-level index
- [ ] Crosslink the document from other subject-related docs
- [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers)
- [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_
- [ ] Make sure the old link is not removed and has its contents replaced with ## Review checklist
a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken. - [ ] Your team's review (required)
- [ ] Search and replace any links referring to the old docs in the GitLab Rails app, - [ ] PM's review (recommended, but not a blocker)
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. - [ ] Technical writer's review (required)
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/documentation/index.html#redirections-for-pages-with-disqus-comments) - [ ] Merge the EE-MR first, CE-MR afterwards
to the new document if there are any Disqus comments on the old document thread.
- [ ] If working on CE and the `ee-compat-check` jobs fails, [submit an MR to EE
with the changes](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee) as well.
- [ ] Ping one of the technical writers for review.
/label ~Documentation /label ~Documentation

View file

@ -70,14 +70,15 @@ linters:
enabled: false enabled: false
RuboCop: RuboCop:
enabled: false enabled: true
# These cops are incredibly noisy when it comes to HAML templates, so we # These cops are incredibly noisy when it comes to HAML templates, so we
# ignore them. # ignore them.
ignored_cops: ignored_cops:
- Lint/BlockAlignment - Layout/BlockAlignment
- Lint/EndAlignment - Layout/EndAlignment
- Lint/Void - Lint/Void
- Metrics/LineLength - Metrics/LineLength
- Naming/FileName
- Style/AlignParameters - Style/AlignParameters
- Style/BlockNesting - Style/BlockNesting
- Style/ElseAlignment - Style/ElseAlignment
@ -91,6 +92,51 @@ linters:
- Style/TrailingWhitespace - Style/TrailingWhitespace
- Style/WhileUntilModifier - Style/WhileUntilModifier
# These cops should eventually get enabled
- Cop/LineBreakAfterGuardClauses
- Cop/LineBreakAroundConditionalBlock
- Cop/ProjectPathHelper
- GitlabSecurity/PublicSend
- Layout/LeadingCommentSpace
- Layout/SpaceAfterColon
- Layout/SpaceAfterComma
- Layout/SpaceAroundOperators
- Layout/SpaceBeforeBlockBraces
- Layout/SpaceBeforeComma
- Layout/SpaceBeforeFirstArg
- Layout/SpaceInsideArrayLiteralBrackets
- Layout/SpaceInsideHashLiteralBraces
- Layout/SpaceInsideStringInterpolation
- Layout/TrailingBlankLines
- Lint/BooleanSymbol
- Lint/LiteralInInterpolation
- Lint/ParenthesesAsGroupedExpression
- Lint/RedundantWithIndex
- Lint/Syntax
- Metrics/BlockNesting
- Naming/VariableName
- Performance/RedundantMatch
- Performance/StringReplacement
- Rails/Presence
- Rails/RequestReferer
- Style/AndOr
- Style/ColonMethodCall
- Style/ConditionalAssignment
- Style/HashSyntax
- Style/IdenticalConditionalBranches
- Style/NegatedIf
- Style/NestedTernaryOperator
- Style/Not
- Style/ParenthesesAroundCondition
- Style/RedundantParentheses
- Style/SelfAssignment
- Style/Semicolon
- Style/TernaryParentheses
- Style/TrailingCommaInHashLiteral
- Style/UnlessElse
- Style/WordArray
- Style/ZeroLengthPredicate
RubyComments: RubyComments:
enabled: true enabled: true

View file

@ -31,6 +31,10 @@ Style/MutableConstant:
- 'ee/db/post_migrate/**/*' - 'ee/db/post_migrate/**/*'
- 'ee/db/geo/migrate/**/*' - 'ee/db/geo/migrate/**/*'
# TODO: Move this to gitlab-styles
Style/SafeNavigation:
Enabled: false
Naming/FileName: Naming/FileName:
ExpectMatchingDefinition: true ExpectMatchingDefinition: true
Exclude: Exclude:
@ -44,6 +48,8 @@ Naming/FileName:
- 'qa/bin/*' - 'qa/bin/*'
- 'config/**/*' - 'config/**/*'
- 'lib/generators/**/*' - 'lib/generators/**/*'
- 'locale/unfound_translations.rb'
- 'ee/locale/unfound_translations.rb'
- 'ee/lib/generators/**/*' - 'ee/lib/generators/**/*'
IgnoreExecutableScripts: true IgnoreExecutableScripts: true
AllowedAcronyms: AllowedAcronyms:

View file

@ -706,12 +706,6 @@ Style/RescueModifier:
Style/RescueStandardError: Style/RescueStandardError:
Enabled: false Enabled: false
# Offense count: 92
# Cop supports --auto-correct.
# Configuration parameters: ConvertCodeThatCanStartToReturnNil.
Style/SafeNavigation:
Enabled: false
# Offense count: 8 # Offense count: 8
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/SelfAssignment: Style/SelfAssignment:

View file

@ -2,38 +2,76 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.2.8 (2018-10-31) ## 11.3.10 (2018-11-18)
### Security (1 change)
- Escape user fullname while rendering autocomplete template to prevent XSS.
## 11.3.9 (2018-10-31)
### Security (1 change) ### Security (1 change)
- Monkey kubeclient to not follow any redirects. - Monkey kubeclient to not follow any redirects.
## 11.2.7 (2018-10-27) ## 11.3.8 (2018-10-27)
- No changes. - No changes.
## 11.2.6 (2018-10-26) ## 11.3.7 (2018-10-26)
### Security (5 changes) ### Security (6 changes)
- Escape entity title while autocomplete template rendering to prevent XSS. !2558 - Escape entity title while autocomplete template rendering to prevent XSS. !2557
- Persist only SHA digest of PersonalAccessToken#token.
- Fix XSS in merge request source branch name. - Fix XSS in merge request source branch name.
- Redact personal tokens in unsubscribe links. - Redact personal tokens in unsubscribe links.
- Persist only SHA digest of PersonalAccessToken#token.
- Prevent SSRF attacks in HipChat integration. - Prevent SSRF attacks in HipChat integration.
- Validate Wiki attachments are valid temporary files.
## 11.2.5 (2018-10-05) ## 11.3.6 (2018-10-17)
- No changes.
## 11.3.5 (2018-10-15)
### Fixed (2 changes)
- Fix loading issue on some merge request discussion. !21982
- Fix project deletion when there is a export available. !22276
## 11.3.4 (2018-10-05)
### Security (3 changes) ### Security (3 changes)
- Filter user sensitive data from discussions JSON. !2538 - Filter user sensitive data from discussions JSON. !2537
- Properly filter private references from system notes. - Properly filter private references from system notes.
- Markdown API no longer displays confidential title references unless authorized. - Markdown API no longer displays confidential title references unless authorized.
## 11.2.4 (2018-09-26) ## 11.3.3 (2018-10-04)
- No changes.
## 11.3.2 (2018-10-03)
### Fixed (4 changes)
- Fix NULL pipeline import problem and pipeline user mapping issue. !21875
- Fix migration to avoid an exception during upgrade. !22055
- Fixes admin runners table not wrapping content.
- Fix Error 500 when forking projects with Gravatar disabled.
### Other (1 change)
- Removes the 'required' attribute from the 'project name' field. !21770
## 11.3.1 (2018-09-26)
### Security (6 changes) ### Security (6 changes)
@ -45,9 +83,258 @@ entry.
- Block loopback addresses in UrlBlocker. - Block loopback addresses in UrlBlocker.
## 11.3.0 (2018-09-22)
### Security (5 changes, 1 of them is from the community)
- Disable the Sidekiq Admin Rack session. !21441
- Set issuable_sort, diff_view, and perf_bar_enabled cookies to secure when possible. !21442
- Update rubyzip to 1.2.2 (CVE-2018-1000544). !21460 (Takuya Noguchi)
- Fixed persistent XSS rendering/escaping of diff location lines.
- Block link-local addresses in URLBlocker.
### Removed (1 change)
- Remove Gemnasium service. !21185
### Fixed (83 changes, 24 of them are from the community)
- Hide PAT creation advice for HTTP clone if PAT exists. !18208 (George Thomas @thegeorgeous)
- Allow spaces in wiki markdown links when using CommonMark. !20417
- disable_statement_timeout no longer leak to other migrations. !20503
- Events API now requires the read_user or api scope. !20627 (Warren Parad)
- Fix If-Check the result that a function was executed several times. !20640 (Max Dicker)
- Add migration to cleanup internal_ids inconsistency. !20926
- Fix fallback logic for automatic MR title assignment. !20930 (Franz Liedke)
- Fixed bug when the project logo file is stored in LFS. !20948
- Fix buttons on the new file page wrapping outside of the container. !21015
- Solve tooltip appears under modal. !21017
- Fix Bitbucket Cloud importer omitting replies. !21076
- Fix pipeline fixture seeder. !21088
- Fix blocked user card style. !21095
- Fix empty merge requests not opening in the Web IDE. !21102
- Fix label list item container height when there is no label description. !21106
- Fixes input alignment in user admin form with errors. !21108 (Jacopo Beschi @jacopo-beschi)
- Rails5 fix specs duplicate key value violates unique constraint 'index_gpg_signatures_on_commit_sha'. !21119 (Jasper Maes)
- Add gitlab theme to spam logs pagination. !21145
- Split remembering sorting for issues and merge requests. !21153 (Jacopo Beschi @jacopo-beschi)
- Fix git submodule link for subgroup projects with relative path. !21154
- Fix: Project deletion may not log audit events during group deletion. !21162
- Fix 1px cutoff of emojis. !21180 (gfyoung)
- Auto-DevOps.gitlab-ci.yml: update glibc package to 2.28. !21191 (sgerrand)
- Show google icon in audit log. !21207 (Jan Beckmann)
- Fix bin/secpick error and security branch prefixing. !21210
- Importing a project no longer fails when visibility level holds a string value type. !21242
- Fix attachments not displaying inline with Google Cloud Storage. !21265
- Fix IDE issues with persistent banners. !21283
- Fix "Confidential comments" button not saving in project hooks. !21289
- Bump fog-google to 1.7.0 and google-api-client to 0.23.0. !21295
- Don't use arguments keyword in gettext script. !21296 (gfyoung)
- Fix breadcrumb link to issues on new issue page. !21305 (J.D. Bean)
- Show '< 1%' when percent value evaluated is less than 1 on Stacked Progress Bar. !21306
- API: Catch empty commit messages. !21322 (Robert Schilling)
- Fix SQL error when sorting 2FA-enabled users by name in admin area. !21324
- API: Catch empty code content for project snippets. !21325 (Robert Schilling)
- Avoid nil safe message. !21326 (Yi Siliang)
- Allow date parameters on Issues, Notes, and Discussions API for group owners. !21342 (Florent Dubois)
- Fix remote mirrors failing if Git remotes have not been added. !21351
- Removing a group no longer triggers hooks for project deletion twice. !21366
- Use slugs for default project path and sanitize names before import. !21367
- Vertically centres landscape avatars. !21371 (Vicary Archangel)
- Fix Web IDE unable to commit to same file twice. !21372
- Fix project transfer name validation issues causing a redirect loop. !21408
- Fix Error 500s due to encoding issues when Wiki hooks fire. !21414
- Rails 5: include opclasses in rails 5 schema dump. !21416 (Jasper Maes)
- Bump GitLab Pages to v1.1.0. !21419
- Fix links in RSS feed elements. !21424 (Marc Schwede)
- Allow gaps in multiseries metrics charts. !21427
- Auto-DevOps.gitlab-ci.yml: fix redeploying deleted app gives helm error. !21429
- Use sample data for push event when no commits created. !21440 (Takuya Noguchi)
- Fix importers not assigning a new default group. !21456
- Fix edge cases of JUnitParser. !21469
- Fix breadcrumb link to merge requests on new merge request page. !21502 (J.D. Bean)
- Handle database statement timeouts in usage ping. !21523
- Handles exception during file upload - replaces the stack trace with a small error message. !21528
- Fix closing issue default pattern. !21531 (Samuele Kaplun)
- Fix outdated discussions being shown on Merge Request Changes tab. !21543
- Remove orphaned label links. !21552
- Delete a container registry asynchronously. !21553
- Make MR diff file filter input Clear button functional. !21556
- Replace white spaces in wiki attachments file names. !21569
- API: Use find_branch! in all places. !21614 (Robert Schilling)
- Fixes double +/- on inline diff view. !21634
- Fix broken exports when they include a projet avatar. !21649
- Fix workhorse temp path for namespace uploads. !21650
- Fixed resolved discussions not toggling expanded state on changes tab. !21676
- Update GitLab Shell to v8.3.2. !21701
- Fix absent Click to Expand link on diffs not rendered on first load of Merge Requests Changes tab. !21716
- Update GitLab Shell to v8.3.3. !21750
- Fix import error when archive does not have the correct extension. !21765
- Fixed IDE deleting new files creating wrong state.
- Does not collapse runners section when using pagination.
- Fix Emojis cutting in the right way. (Alexander Popov)
- Fix NamespaceUploader.base_dir for remote uploads.
- Increase width of checkout branch modal box.
- Fixes SVGs for empty states in job page overflowing on mobile.
- Fix checkboxes on runner admin settings - The labels are now clickable.
- Fixed IDE file row scrolling into view when hovering.
- Accept upload files in public/uplaods/tmp when using accelerated uploads.
- Include correct CSS file for xterm in environments page.
- Increase padding in code blocks.
- Fix: Project deletion may not log audit events during user deletion.
### Changed (32 changes, 5 of them are from the community)
- Add default avatar to group. !17271 (George Tsiolis)
- Allow project owners to set up forking relation through API. !18104
- Limit navbar search for current project or group for small viewports. !18634 (George Tsiolis)
- Add Noto Color Emoji font support. !19036 (Alexander Popov)
- Update design of project overview page. !20536
- Improve visuals of language bar on projects. !21006
- Migrate NULL wiki_access_level to correct number so we count active wikis correctly. !21030
- Support a custom action, such as proxying to another server, after /api/v4/internal/allowed check succeeds. !21034
- Remove storage path dependency of gitaly install task. !21101
- Support Kubernetes RBAC for GitLab Managed Apps when adding a existing cluster. !21127
- Change 'Backlog' list title to 'Open' in Issue Boards. !21131
- Enable Auto DevOps Instance Wide Default. !21157
- Allow author to vote on their own issue and MRs. !21203
- Truncate branch names and update "commits behind" text in MR page. !21206
- Adds count for different board list types (label lists, assignee lists, and milestone lists) to usage statistics. !21208
- Render files (`.md`) and wikis using CommonMark. !21228
- Show deprecation message on project milestone page for category tabs. !21236
- Remove redundant header from metrics page. !21282
- Add default parameter to branches API. !21294 (Riccardo Padovani)
- Restrict reopening locked issues for non authorized issue authors. !21299
- Send back required object storage PUT headers in /uploads/authorize API. !21319
- Display default status emoji if only message is entered. !21330
- Move badge settings to general settings. !21333
- Move project settings for default branch under "Repository". !21380
- Import all common metrics into database. !21459
- Improved commit panel in Web IDE. !21471
- Administrative cleanup rake tasks now leverage Gitaly. !21588
- Remove health check feature flag in BackgroundMigrationWorker.
- Expose user's id in /admin/users/ show page. (Eva Kadlecova)
- Improved styling of top bar in IDE job trace pane.
- Make terminal button more visible.
- Shows download artifacts button for pipelines on small screens.
### Performance (13 changes, 2 of them are from the community)
- Enable frozen string in rest of app/models/**/*.rb.
- Add background migrations for legacy artifacts. !18615
- Optimize querying User#manageable_groups. !21050
- Incremental rendering with Vue on merge request page. !21063
- Remove redundant ci_builds (status) index. !21070
- Enable frozen in app/mailers/**/*.rb. !21147 (gfyoung)
- Improve performance when fetching related merge requests for an issue. !21237
- Speed up diff comparisons by limiting number of commit messages rendered. !21335
- Write diff highlighting cache upon MR creation (refactors caching). !21489
- Bulk-render commit titles in the tree view to improve performance. !21500
- Enable frozen string in vestigial app files. (gfyoung)
- Disable project avatar validation if avatar has not changed.
- Bitbucket Server importer: Eliminate most idle-in-transaction issues.
### Added (41 changes, 17 of them are from the community)
- API: Protected tags. !14986 (Robert Schilling)
- Include private contributions to contributions calendar. !17296 (George Tsiolis)
- Add an option to whitelist users based on email address as internal when the "New user set to external" setting is enabled. !17711 (Roger Rüttimann)
- Overhaul listing of projects in the group overview page. !20262
- Add the ability to reference projects in comments and other markdown text. !20285 (Reuben Pereira)
- Add branch filter to project webhooks. !20338 (Duana Saskia)
- Allows to cancel a Created job. !20635 (Jacopo Beschi @jacopo-beschi)
- First Improvements made to the contributor on-boarding experience. !20682 (Eddie Stubbington)
- `/tag` quick action on Commit comments. !20694 (Peter Leitzen)
- Allow admins to configure the maximum Git push size. !20758
- Expose all artifacts sizes in jobs api. !20821 (Peter Marko)
- Get the merge base of two refs through the API. !20929
- Add ability to suppress the global "You won't be able to use SSH" message. !21027 (Ævar Arnfjörð Bjarmason)
- API: Add expiration date for shared projects to the project entity. !21104 (Robert Schilling)
- Added tooltips to tree list header. !21138
- #47845 Add failure_reason to job webhook. !21143 (matemaciek)
- Vendor Auto-DevOps.gitlab-ci.yml with new proxy env vars passed through to docker. !21159 (kinolaev)
- Disable Auto DevOps for project upon first pipeline failure. !21172
- Add rake command to migrate archived traces from local storage to object storage. !21193
- Add Czech as an available language. !21201
- Add Galician as an available language. !21202
- Add support for extendable CI/CD config with. !21243
- Disable Web IDE button if user is not allowed to push the source branch. !21288
- Feature flag to disable Hashed Storage migration when renaming a repository. !21291
- Store wiki uploads inside git repository. !21362
- Adds Rubocop rule to enforce class_methods over module ClassMethods. !21379 (Jacopo Beschi @jacopo-beschi)
- Merge request copies all associated issue labels and milestone on creation. !21383
- Add group name badge under group milestone. !21384
- Adds diverged_commits_count field to GET api/v4/projects/:project_id/merge_requests/:merge_request_iid. !21405 (Jacopo Beschi @jacopo-beschi)
- Update Import/Export to only use new storage uploaders logic. !21409
- Ask user explicitly about usage stats agreement on single user deployments. !21423
- Added atom feed for tags. !21428
- Add search to a group labels page. !21480
- Display banner on project page if AutoDevOps is implicitly enabled. !21503
- Recognize 'UNLICENSE' license files. !21508 (J.D. Bean)
- Add git_v2 feature flag. !21520
- Added file templates to the Web IDE.
- Enabled multiple file uploads in the Web IDE.
- Allow to delete group milestones.
- Use separate model for tracking resource label changes and render label system notes based on data from this model.
- Add system note when due date is changed. (Eva Kadlecova)
### Other (48 changes, 16 of them are from the community)
- Remove extra spaces from MR discussion notes. !18946 (Takuya Noguchi)
- Add an example of the configuration of archive trace cron worker in gitlab.yml.example. !20583
- Add target branch name to cherrypick confirmation message. !20846 (George Andrinopoulos)
- CE Port of Protected Environments backend. !20859
- Added missing i18n strings to issue boards lables dropdown. !21081
- Combines emoji award spec files into single user_interacts_with_awards_in_issue_spec.rb file. !21126 (Nate Geslin)
- Clarify current runners online text. !21151 (Ben Bodenmiller)
- Rails5: Enable verbose query logs. !21231 (Jasper Maes)
- Update presentation for SSO providers on log in page. !21233
- Make margin of user status emoji consistent. !21268
- Move usage ping payload from User Cohorts page to admin application settings. !21343
- Add JSON logging for Bitbucket Server importer. !21378
- Re-add project name field on "Create new project" page. !21386
- Rails 5: replace removed silence_stream. !21387 (Jasper Maes)
- Rails5 update Gemfile.rails5.lock. !21388 (Jasper Maes)
- Rails5: fix can't quote ActiveSupport::HashWithIndifferentAccess. !21397 (Jasper Maes)
- Don't show flash messages for performance bar errors. !21411
- Backport schema_changed.sh from EE which prints the diff if the schema is different. !21422 (Jasper Maes)
- Remove unused CSS part in mobile framework. !21439 (Takuya Noguchi)
- Bump unauthenticated session time from 1 hour to 2 hours. !21453
- Run review-docs-cleanup job for gitlab-org repos only. !21463 (Takuya Noguchi)
- Rails 5: support schema t.index for mysql. !21485 (Jasper Maes)
- Add route information to lograge structured logging for API logs. !21487
- Add gitaly_calls attribute to API logs. !21496
- Ignore irrelevant sql commands in metrics. !21498
- Rails 5: fix hashed_path? method that looks up file_location that doesn't exist when running certain migration specs. !21510 (Jasper Maes)
- Explicit hashed path check for trace, prevents background migration from accessing file_location column that doesn't exist. !21533 (Jasper Maes)
- Add terminal_path to job API response. !21537
- Add User-Agent to production_json.log. !21546
- Make cluster page settings easier to read. !21550
- Remove striped table styling of Find files and Admin Area Applications views. !21560 (Andreas Kämmerle)
- Update ffi to 1.9.25. !21561 (Takuya Noguchi)
- Send max_patch_bytes to Gitaly via Gitaly::CommitDiffRequest. !21575
- Add margin between username and subsequent text in issuable header. !21697
- Send artifact information in job API. !50460
- Reduce differences between CE and EE code base in reports components.
- Move project services log to a separate file.
- Creates vue component for job log top bar with controllers.
- Creates Vue component for trigger variables block in job log page.
- Creates Vvue component for warning block about stuck runners.
- Creates vue component for job log trace.
- Creates vue component for erased block on job view.
- Creates vue component for environments information in job log view.
- Upgrade Monaco editor.
- Creates empty state vue component for job view.
- Creates vue component for commit block in job log page.
- Creates vue components for stage dropdowns and job list container for job log view.
- Creates Vue component for artifacts block on job page.
## 11.2.3 (2018-08-28) ## 11.2.3 (2018-08-28)
- No changes. ### Fixed (1 change)
- Fixed cache invalidation issue with diff lines from 11.2.2.
## 11.2.2 (2018-08-27) ## 11.2.2 (2018-08-27)
@ -310,6 +597,25 @@ entry.
- Moves help_popover component to a common location. - Moves help_popover component to a common location.
## 11.1.6 (2018-08-28)
### Fixed (1 change)
- Fixed cache invalidation issue with diff lines from 11.2.2.
## 11.1.5 (2018-08-27)
### Security (3 changes)
- Fixed persistent XSS rendering/escaping of diff location lines.
- Adding CSRF protection to Hooks resend action.
- Block link-local addresses in URLBlocker.
### Fixed (1 change, 1 of them is from the community)
- Sanitize git URL in import errors. (Jamie Schembri)
## 11.1.4 (2018-07-30) ## 11.1.4 (2018-07-30)
### Fixed (4 changes, 1 of them is from the community) ### Fixed (4 changes, 1 of them is from the community)
@ -592,6 +898,19 @@ entry.
- Use monospaced font for MR diff commit link ref on GFM. - Use monospaced font for MR diff commit link ref on GFM.
## 11.0.6 (2018-08-27)
### Security (3 changes)
- Fixed persistent XSS rendering/escaping of diff location lines.
- Adding CSRF protection to Hooks resend action.
- Block link-local addresses in URLBlocker.
### Fixed (1 change, 1 of them is from the community)
- Sanitize git URL in import errors. (Jamie Schembri)
## 11.0.5 (2018-07-26) ## 11.0.5 (2018-07-26)
### Security (4 changes) ### Security (4 changes)

View file

@ -21,48 +21,54 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Contributing Documentation has been moved](#contributing-documentation-has-been-moved)
- [Contribute to GitLab](#contribute-to-gitlab) - [Contribute to GitLab](#contribute-to-gitlab)
- [Security vulnerability disclosure](#security-vulnerability-disclosure) - [Security vulnerability disclosure](#security-vulnerability-disclosure)
- [Code of conduct](#code-of-conduct)
- [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests) - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
- [Helping others](#helping-others) - [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute) - [I want to contribute!](#i-want-to-contribute)
- [Contribution Flow](#contribution-flow)
- [Workflow labels](#workflow-labels) - [Workflow labels](#workflow-labels)
- [Type labels](#type-labels) - [Type labels](#type-labels)
- [Subject labels](#subject-labels) - [Subject labels](#subject-labels)
- [Team labels](#team-labels) - [Team labels](#team-labels)
- [Release Scoping labels](#release-scoping-labels) - [Release Scoping labels](#release-scoping-labels)
- [Bug Priority labels](#bug-priority-labels) - [Priority labels](#priority-labels)
- [Bug Severity labels](#bug-severity-labels) - [Severity labels](#severity-labels)
- [Severity impact guidance](#severity-impact-guidance) - [Severity impact guidance](#severity-impact-guidance)
- [Label for community contributors](#label-for-community-contributors) - [Label for community contributors](#label-for-community-contributors)
- [Implement design & UI elements](#implement-design-ui-elements) - [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging) - [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals) - [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight) - [Issue weight](#issue-weight)
- [Regression issues](#regression-issues) - [Regression issues](#regression-issues)
- [Technical and UX debt](#technical-and-ux-debt) - [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship) - [Stewardship](#stewardship)
- [Merge requests](#merge-requests) - [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines) - [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria) - [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Definition of done](#definition-of-done) - [Definition of done](#definition-of-done)
- [Style guides](#style-guides) - [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
- [Contribution Flow](#contribution-flow)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
--- ---
## Contributing Documentation has been moved
As of July 2018, all the documentation for contributing to the GitLab project has been moved to a new location.
[view the new documentation](doc/development/contributing/index.md) to find the latest information.
## Contribute to GitLab ## Contribute to GitLab
For a first-time step-by-step guide to the contribution process, see
["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Thank you for your interest in contributing to GitLab. This guide details how Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone. to contribute to GitLab in a way that is easy for everyone.
For a first-time step-by-step guide to the contribution process, please see
["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute). Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute).
@ -71,10 +77,10 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for edition. Throughout this guide you will see references to CE and EE for
abbreviation. abbreviation.
If you have read this guide and want to know how the GitLab [core team] If you want to know how the GitLab [core team]
operates please see [the GitLab contributing process](PROCESS.md). operates please see [the GitLab contributing process](PROCESS.md).
- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
## Security vulnerability disclosure ## Security vulnerability disclosure
@ -84,6 +90,36 @@ Please report suspected security vulnerabilities in private to
Please do **NOT** create publicly viewable issues for suspected security Please do **NOT** create publicly viewable issues for suspected security
vulnerabilities. vulnerabilities.
## Code of conduct
As contributors and maintainers of this project, we pledge to respect all
people who contribute through reporting issues, posting feature requests,
updating documentation, submitting pull requests or patches, and other
activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual
language or imagery, derogatory comments or personal attacks, trolling, public
or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. Project maintainers who do not
follow the Code of Conduct may be removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
## Closing policy for issues and merge requests ## Closing policy for issues and merge requests
GitLab is a popular open source project and the capacity to deal with issues GitLab is a popular open source project and the capacity to deal with issues
@ -123,623 +159,6 @@ learn how to communicate with GitLab. If you're looking for a Gitter or Slack ch
please consider we favor please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution! [asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
## Workflow labels
To allow for asynchronous issue handling, we use [milestones][milestones-page]
and [labels][labels-page]. Leads and product managers handle most of the
scheduling into milestones. Labelling is a task for everyone.
Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Plan, ~Manage, ~Quality, etc.
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the
[labels page][labels-page].
If you come across an issue that has none of these, and you're allowed to set
labels, you can _always_ add the team and type, and often also the subject.
[milestones-page]: https://gitlab.com/gitlab-org/gitlab-ce/milestones
[labels-page]: https://gitlab.com/gitlab-org/gitlab-ce/labels
### Type labels
Type labels are very important. They define what kind of issue this is. Every
issue should have one or more.
Examples of type labels are ~"feature proposal", ~bug, ~customer, ~security,
and ~"direction".
A number of type labels have a priority assigned to them, which automatically
makes them float to the top, depending on their importance.
Type labels are always lowercase, and can have any color, besides blue (which is
already reserved for subject labels).
The descriptions on the [labels page][labels-page] explain what falls under each type label.
### Subject labels
Subject labels are labels that define what area or feature of GitLab this issue
hits. They are not always necessary, but very convenient.
Examples of subject labels are ~wiki, ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
issue is labeled with a subject label corresponding to your expertise.
Subject labels are always all-lowercase.
### Team labels
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are:
- ~Configuration
- ~"CI/CD"
- ~Create
- ~Distribution
- ~Documentation
- ~Geo
- ~Gitaly
- ~Manage
- ~Monitoring
- ~Plan
- ~Quality
- ~Release
- ~Secure
- ~UX
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
Within those team labels, we also have the ~backend and ~frontend labels to
indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
### Release Scoping labels
Release Scoping labels help us clearly communicate expectations of the work for the
release. There are three levels of Release Scoping labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
- ~Stretch: Issues that are a stretch goal for delivering in the current
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
- ~"Next Patch Release": Issues to put in the next patch release. Work on these
first, and add the "Pick Into X" label to the merge request, along with the
appropriate milestone.
Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Priority labels
Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Estimate time to fix |
|-------|-----------------|------------------------------------------------------------------|
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com |
| ~P2 | High Priority | The next release |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) |
### Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Impact on Functionality | Example |
|-------|-------------------|-------------------------------------------------------|---------|
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
#### Severity impact guidance
Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline.
| Severity | Affected Customers/Users | GitLab.com Availability | Performance Degradation |
|----------|---------------------------------------------------------------------|----------------------------------------------------|------------------------------|
| ~S1 | >50% users affected (possible company extinction level event) | Significant impact on all of GitLab.com | |
| ~S2 | Many users or multiple paid customers affected (but not apocalyptic)| Significant impact on large portions of GitLab.com | Degradation is guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer affected | Limited impact on important portions of GitLab.com | Degradation is likely to occur in the near future |
| ~S4 | No paid users/customer affected, or expected to in the near future | Minor impact on on GitLab.com | Degradation _may_ occur but it's not likely |
### Label for community contributors
Issues that are beneficial to our users, 'nice to haves', that we currently do
not have the capacity for or want to give the priority to, are labeled as
~"Accepting Merge Requests", so the community can make a contribution.
Community contributors can submit merge requests for any issue they want, but
the ~"Accepting Merge Requests" label has a special meaning. It points to
changes that:
1. We already agreed on,
1. Are well-defined,
1. Are likely to get accepted by a maintainer.
We want to avoid a situation when a contributor picks an
~"Accepting Merge Requests" issue and then their merge request gets closed,
because we realize that it does not fit our vision, or we want to solve it in a
different way.
We add the ~"Accepting Merge Requests" label to:
- Low priority ~bug issues (i.e. we do not add it to the bugs that we want to
solve in the ~"Next Patch Release")
- Small ~"feature proposal"
- Small ~"technical debt" issues
After adding the ~"Accepting Merge Requests" label, we try to estimate the
[weight](#issue-weight) of the issue. We use issue weight to let contributors
know how difficult the issue is. Additionally:
- We advertise ["Accepting Merge Requests" issues with weight < 5][up-for-grabs]
as suitable for people that have never contributed to GitLab before on the
[Up For Grabs campaign](http://up-for-grabs.net)
- We encourage people that have never contributed to any open source project to
look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers]
If you've decided that you would like to work on an issue, please @-mention
the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what)
as soon as possible. The product manager will then pull in appropriate GitLab team
members to further discuss scope, design, and technical considerations. This will
ensure that that your contribution is aligned with the GitLab product and minimize
any rework and delay in getting it merged into master.
GitLab team members who apply the ~"Accepting Merge Requests" label to an issue
should update the issue description with a responsible product manager, inviting
any potential community contributor to @-mention per above.
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened
[firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1
## Implement design & UI elements
For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
The UX team uses labels to manage their workflow.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux) of the handbook.
Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue.
The UX team has a special type label called ~"design artifact". This label indicates that the final output
for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
To prevent the misunderstanding that a feature will be be delivered in the
assigned milestone, when only UX design is planned for that milestone, the
Product Manager should create a separate issue for the ~"design artifact",
assign the ~UX, ~"design artifact" and ~"Deliverable" labels, add a milestone
and use a title that makes it clear that the scheduled issue is design only
(e.g. `Design exploration for XYZ`).
When the ~"design artifact" issue has been completed, the UXer removes the ~UX
label, adds the ~"UX ready" label and closes the issue. This indicates the
design artifact is complete. The UXer will also copy the designs to related
issues for implementation in an upcoming milestone.
## Issue tracker
To get support for your particular problem please use the
[getting help channels](https://about.gitlab.com/getting-help/).
The [GitLab CE issue tracker on GitLab.com][ce-tracker] is for bugs concerning
the latest GitLab release and [feature proposals](#feature-proposals).
When submitting an issue please conform to the issue submission guidelines
listed below. Not all issues will be addressed and your issue is more likely to
be addressed if you submit a merge request which partially or fully solves
the issue.
If you're unsure where to post, post to the [mailing list][google-group] or
[Stack Overflow][stackoverflow] first. There are a lot of helpful GitLab users
there who may be able to help you quickly. If your particular issue turns out
to be a bug, it will find its way from there.
If it happens that you know the solution to an existing bug, please first
open the issue in order to keep track of it and then open the relevant merge
request that potentially fixes it.
### Issue triaging
Our issue triage policies are [described in our handbook]. You are very welcome
to help the GitLab team triage issues. We also organize [issue bash events] once
every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone.
We also use [GitLab Triage] to automate some triaging policies. This is
currently setup as a [scheduled pipeline] running on [quality/triage-ops]
project.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
[GitLab Triage]: https://gitlab.com/gitlab-org/gitlab-triage
[scheduled pipeline]: https://gitlab.com/gitlab-org/quality/triage-ops/pipeline_schedules/10512/edit
[quality/triage-ops]: https://gitlab.com/gitlab-org/quality/triage-ops
### Feature proposals
To create a feature proposal for CE, open an issue on the
[issue tracker of CE][ce-tracker].
For feature proposals for EE, open an issue on the
[issue tracker of EE][ee-tracker].
In order to help track the feature proposals, we have created a
[`feature proposal`][fpl] label. For the time being, users that are not members
of the project cannot add labels. You can instead ask one of the [core team]
members to add the label ~"feature proposal" to the issue or add the following
code snippet right after your description in a new line: `~"feature proposal"`.
Please keep feature proposals as small and simple as possible, complex ones
might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
### Issue tracker guidelines
**[Search the issue tracker][ce-tracker]** for similar entries before
submitting your own, there's a good chance somebody else had the same issue or
feature proposal. Show your support with an award emoji and/or join the
discussion.
Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker.
The text in the parenthesis is there to help you with what to include. Omit it
when submitting the actual issue. You can copy-paste it and then edit as you
see fit.
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
one or multiple issues. This makes it possible to schedule work more accurately.
You are encouraged to set the weight of any issue. Following the guidelines
below will make it easy to manage this, without unnecessary overhead.
1. Set weight for any issue at the earliest possible convenience
1. If you don't agree with a set weight, discuss with other developers until
consensus is reached about the weight
1. Issue weights are an abstract measurement of complexity of the issue. Do not
relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring)
and something you want to avoid.
1. Something that has a weight of 1 (or no weight) is really small and simple.
Something that is 9 is rewriting a large fundamental part of GitLab,
which might lead to many hard problems to solve. Changing some text in GitLab
is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
1. If something is very large, it should probably be split up in multiple
issues or chunks. You can simply not set the weight of a parent issue and set
weights to children issues.
### Regression issues
Every monthly release has a corresponding issue on the CE issue tracker to keep
track of functionality broken by that release and any fixes that need to be
included in a patch release (see [8.3 Regressions] as an example).
As outlined in the issue description, the intended workflow is to post one note
with a reference to an issue describing the regression, and then to update that
note with a reference to the merge request that fixes it as it becomes available.
If you're a contributor who doesn't have the required permissions to update
other users' notes, please post a new note with a reference to both the issue
and the merge request.
The release manager will [update the notes] in the regression issue as fixes are
addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
### Technical and UX debt
In order to track things that can be improved in GitLab's codebase,
we use the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
For user experience improvements, we use the ~"UX debt" label.
These labels should be added to issues that describe things that can be improved,
shortcuts that have been taken, features that need additional attention, and all
other things that have been left behind due to high velocity of development.
For example, code that needs refactoring should use the ~"technical debt" label,
user experience refinements should use the ~"UX debt" label.
Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels
can be combined with these labels, to make it easier to schedule
the improvements for a release.
Issues tagged with these labels have the same priority like issues
that describe a new feature to be introduced in GitLab, and should be scheduled
for a release by the appropriate person.
Make sure to mention the merge request that the ~"technical debt" issue or
~"UX debt" issue is associated with in the description of the issue.
### Stewardship
For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab
is a topic of discussion. For instance if GitLab Inc. is planning to add
features from GitLab EE to GitLab CE, related issues would be labelled with
~"stewardship".
A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue].
[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
and/or documentation. The issues that are specifically suitable for
community contributions are listed with the label
[`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
and [EE][accepting-mrs-ee], but you are free to contribute to any other issue
you want.
Please note that if an issue is marked for the current milestone either before
or while you are working on it, a team member may take over the merge request
in order to ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create
a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting Merge Requests`. Please include screenshots or
wireframes if the feature will also change the UI.
Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
If you are new to GitLab development (or web development in general), see the
[I want to contribute!](#i-want-to-contribute) section to get you started with
some potentially easy issues.
To start with GitLab development download the [GitLab Development Kit][gdk] and
see the [Development section](doc/development/README.md) for some guidelines.
### Merge request guidelines
If you can, please submit a merge request with the fix or improvements
including tests. If you don't know how to fix the issue but can write a test
that exposes the issue we will accept that as well. In general bug fixes that
include a regression test are merged quickly while new features without proper
tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows:
1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`
1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation guidelines][doc-guidelines]
1. If you have multiple commits please combine them into a few logically
organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch
1. Your merge request needs at least 1 approval but feel free to require more.
For instance if you're touching backend and frontend code, it's a good idea
to require 2 approvals: 1 from a backend maintainer and 1 from a frontend
maintainer
1. You don't have to select any approvers, but you can if you really want
specific people to approve your merge request
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
used to achieve it.
1. If you are contributing code, fill in the template already provided in the
"Description" field.
1. If you are contributing documentation, choose `Documentation` from the
"Choose a template" menu and fill in the template.
1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or
`Closes #XXX` syntax to auto-close the issue(s) once the merge request will
be merged.
1. If you're allowed to, set a relevant milestone and labels
1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R`
1. Be prepared to answer questions and incorporate feedback even if requests
for this arrive weeks or months after your MR submission
1. If a discussion has been addressed, select the "Resolve discussion" button
beneath it to mark it resolved.
1. If your MR touches code that executes shell commands, reads or opens files or
handles paths to files on disk, make sure it adheres to the
[shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
1. When writing commit messages please follow
[these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
[guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all
migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete.
1. For more complex migrations, write tests.
1. Merge requests **must** adhere to the [merge request performance
guidelines](doc/development/merge_request_performance_guidelines.md).
1. For tests that use Capybara or PhantomJS, see this [article on how
to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
Please keep the change in a single MR **as small as possible**. If you want to
contribute a large feature think very hard what the minimum viable change is.
Can you split the functionality? Can you only submit the backend/API code? Can
you start with a very simple UI? Can you do part of the refactor? The increased
reviewability of small MRs that leads to higher code quality is more important
to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it.
The ['How to get faster PR reviews' document of Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) also has some great points regarding this.
For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback
on your merge request feel free to mention someone from the [core team] or one
of the [Merge request coaches][team].
Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account.
### Contribution acceptance criteria
1. The change is as small as possible
1. Include proper tests and make all tests pass (unless it contains a test
exposing a bug in existing code). Every new class should have corresponding
unit tests, even if the class is exercised at a higher level, such as a feature test.
1. If you suspect a failing CI build is unrelated to your contribution, you may
try and restart the failing CI job or ask a developer to fix the
aforementioned failing test
1. Your MR initially contains a single commit (please use `git rebase -i` to
squash commits)
1. Your changes can merge without problems (if not please rebase if you're the
only one working on your feature branch, otherwise, merge `master`)
1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed)
1. Migrations should do only one thing (e.g., either create a table, move data
to a new table or remove an old table) to aid retrying on failure
1. Keeps the GitLab code base clean and well structured
1. Contains functionality we think other users will benefit from too
1. Doesn't add configuration options or settings options since they complicate
making and testing future changes
1. Changes do not adversely degrade performance.
- Avoid repeated polling of endpoints that require a significant amount of overhead
- Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- Avoid repeated access of filesystem
1. If you need polling to support real-time features, please use
[polling with ETag caching][polling-etag].
1. Changes after submitting the merge request should be in separate commits
(no squashing).
1. It conforms to the [style guides](#style-guides) and the following:
- If your change touches a line that does not follow the style, modify the
entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
1. If the merge request adds any new libraries (gems, JavaScript libraries,
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
1. The merge request meets the [definition of done](#definition-of-done).
## Definition of done
If you contribute to GitLab please know that changes involve more than just
code. We have the following [definition of done][definition-of-done]. Please ensure you support
the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
1. [Unit, integration, and system tests][testing] that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested
1. [Documented][doc-guidelines] in the `/doc` directory
1. [Changelog entry added][changelog], if necessary
1. Reviewed and any concerns are addressed
1. Merged by a project maintainer
1. Added to the release blog article, if relevant
1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant
1. Community questions answered
1. Answers to questions radiated (in docs/wiki/support etc.)
If you add a dependency in GitLab (such as an operating system package) please
consider updating the following and note the applicability of each in your
merge request:
1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/
1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md
1. Upgrader https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md#2-run-gitlab-upgrade-tool
1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies
1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit
1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh
1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
## Style guides
1. [Ruby](https://github.com/bbatsov/ruby-style-guide).
Important sections include [Source Code Layout][rss-source] and
[Naming][rss-naming]. Use:
- multi-line method chaining style **Option A**: dot `.` on the second line
- string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide]
1. [Testing][testing]
1. [JavaScript styleguide][js-styleguide]
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. [Documentation styleguide](https://docs.gitlab.com/ee/development/documentation/styleguide.html)
1. Interface text should be written subjectively instead of objectively. It
should be the GitLab core team addressing a person. It should be written in
present time and never use past tense (has been/was). For example instead
of _prohibited this user from being saved due to the following errors:_ the
text should be _sorry, we could not create your account because:_
1. Code should be written in [US English][us-english]
This is also the style used by linting tools such as
[RuboCop](https://github.com/bbatsov/rubocop),
[PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
## Code of conduct
As contributors and maintainers of this project, we pledge to respect all
people who contribute through reporting issues, posting feature requests,
updating documentation, submitting pull requests or patches, and other
activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual
language or imagery, derogatory comments or personal attacks, trolling, public
or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. Project maintainers who do not
follow the Code of Conduct may be removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
## Contribution Flow ## Contribution Flow
When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty. When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty.
@ -789,3 +208,115 @@ When your code contains more than 500 changes, any major breaking changes, or an
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md [testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English [us-english]: https://en.wikipedia.org/wiki/American_English
## Workflow labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Type labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Subject labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Team labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Release Scoping labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Priority labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Severity labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
#### Severity impact guidance
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Label for community contributors
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
## Implement design & UI elements
This [documentation](doc/development/contributing/design.md) has been moved.
## Issue tracker
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Issue triaging
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Feature proposals
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Issue tracker guidelines
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Issue weight
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Regression issues
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Technical and UX debt
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Stewardship
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
## Merge requests
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
### Merge request guidelines
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
### Contribution acceptance criteria
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Definition of done
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Style guides
This [documentation](doc/development/contributing/design.md) has been moved.

View file

@ -4,4 +4,5 @@ danger.import_dangerfile(path: 'danger/changelog')
danger.import_dangerfile(path: 'danger/specs') danger.import_dangerfile(path: 'danger/specs')
danger.import_dangerfile(path: 'danger/gemfile') danger.import_dangerfile(path: 'danger/gemfile')
danger.import_dangerfile(path: 'danger/database') danger.import_dangerfile(path: 'danger/database')
danger.import_dangerfile(path: 'danger/documentation')
danger.import_dangerfile(path: 'danger/frozen_string') danger.import_dangerfile(path: 'danger/frozen_string')

View file

@ -1 +1 @@
0.117.3 0.120.1

View file

@ -1 +1 @@
1.0.0 1.1.0

View file

@ -1 +1 @@
8.1.1 8.3.3

View file

@ -1 +1 @@
5.1.0 6.1.0

24
Gemfile
View file

@ -68,7 +68,7 @@ gem 'u2f', '~> 0.2.1'
gem 'validates_hostname', '~> 1.0.6' gem 'validates_hostname', '~> 1.0.6'
# Browser detection # Browser detection
gem 'browser', '~> 2.2' gem 'browser', '~> 2.5'
# GPG # GPG
gem 'gpgme' gem 'gpgme'
@ -107,7 +107,9 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.8.8' gem 'hamlit', '~> 2.8.8'
# Files attachments # Files attachments
gem 'carrierwave', '~> 1.2' # Locked until https://github.com/carrierwaveuploader/carrierwave/pull/2332/files is merged.
# config/initializers/carrierwave_patch.rb can be removed once that change is released.
gem 'carrierwave', '= 1.2.3'
gem 'mini_magick' gem 'mini_magick'
# Drag and Drop UI # Drag and Drop UI
@ -116,14 +118,14 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 2.0.1' gem 'fog-aws', '~> 2.0.1'
gem 'fog-core', '~> 1.44' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 1.3.3' gem 'fog-google', '~> 1.7.1'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0' gem 'fog-aliyun', '~> 0.2.0'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.19.8' gem 'google-api-client', '~> 0.23'
# for aws storage # for aws storage
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
@ -180,7 +182,7 @@ gem 'rufus-scheduler', '~> 3.4'
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.13.3'
# Colored output to console # Colored output to console
gem 'rainbow', '~> 2.2' gem 'rainbow', '~> 3.0'
# Progress bar # Progress bar
gem 'ruby-progressbar' gem 'ruby-progressbar'
@ -195,6 +197,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# Export Ruby Regex to Javascript
gem 'js_regex', '~> 2.2.1'
# User agent parsing # User agent parsing
gem 'device_detector' gem 'device_detector'
@ -214,9 +219,6 @@ gem 'jira-ruby', '~> 1.4'
# Flowdock integration # Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1' gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
# Gemnasium integration
gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration # Slack integration
gem 'slack-notifier', '~> 1.5.1' gem 'slack-notifier', '~> 1.5.1'
@ -363,12 +365,11 @@ group :development, :test do
gem 'scss_lint', '~> 0.56.0', require: false gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.10.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'license_finder', '~> 3.1', require: false gem 'license_finder', '~> 5.4', require: false
gem 'knapsack', '~> 1.16' gem 'knapsack', '~> 1.16'
gem 'activerecord_sane_schema_dumper', gem_versions['activerecord_sane_schema_dumper'] gem 'activerecord_sane_schema_dumper', gem_versions['activerecord_sane_schema_dumper']
@ -390,6 +391,7 @@ group :test do
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'concurrent-ruby', '~> 1.0.5' gem 'concurrent-ruby', '~> 1.0.5'
gem 'test-prof', '~> 0.2.5' gem 'test-prof', '~> 0.2.5'
gem 'rspec_junit_formatter'
end end
gem 'octokit', '~> 4.9' gem 'octokit', '~> 4.9'
@ -423,7 +425,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.117.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed

View file

@ -86,12 +86,11 @@ GEM
bindata (2.4.3) bindata (2.4.3)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
bootsnap (1.3.1) bootsnap (1.3.1)
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
browser (2.2.0) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -123,10 +122,10 @@ GEM
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
citrus (3.0.2) citrus (3.0.2)
coderay (1.1.1) coderay (1.1.2)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
commonmarker (0.17.8) commonmarker (0.17.13)
ruby-enum (~> 0.5) ruby-enum (~> 0.5)
concord (0.1.5) concord (0.1.5)
adamantium (~> 0.2.0) adamantium (~> 0.2.0)
@ -209,12 +208,7 @@ GEM
fast_blank (1.0.0) fast_blank (1.0.0)
fast_gettext (1.6.0) fast_gettext (1.6.0)
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.18) ffi (1.9.25)
flay (2.10.0)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flipper (0.13.0) flipper (0.13.0)
flipper-active_record (0.13.0) flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6) activerecord (>= 3.2, < 6)
@ -239,11 +233,11 @@ GEM
builder builder
excon (~> 0.58) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (1.3.3) fog-google (1.7.1)
fog-core fog-core
fog-json fog-json
fog-xml fog-xml
google-api-client (~> 0.19.1) google-api-client (~> 0.23.0)
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
@ -269,8 +263,6 @@ GEM
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (3.3.0) gemojione (3.3.0)
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
@ -284,7 +276,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.113.0) gitaly-proto (0.117.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
@ -331,7 +323,7 @@ GEM
actionpack (>= 3.0) actionpack (>= 3.0)
multi_json multi_json
request_store (>= 1.0) request_store (>= 1.0)
google-api-client (0.19.8) google-api-client (0.23.4)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0) googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0) httpclient (>= 2.8.1, < 3.0)
@ -430,6 +422,8 @@ GEM
multipart-post multipart-post
oauth (~> 0.5, >= 0.5.0) oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
js_regex (2.2.1)
regexp_parser (>= 0.4.11, <= 0.5.0)
json (1.8.6) json (1.8.6)
json-jwt (1.9.4) json-jwt (1.9.4)
activesupport activesupport
@ -465,13 +459,12 @@ GEM
actionmailer (>= 3.2) actionmailer (>= 3.2)
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
license_finder (3.1.1) license_finder (5.4.0)
bundler bundler
httparty
rubyzip rubyzip
thor thor
toml (= 0.1.2) toml (= 0.2.0)
with_env (> 1.0) with_env (= 1.1.0)
xml-simple xml-simple
licensee (8.9.2) licensee (8.9.2)
rugged (~> 0.24) rugged (~> 0.24)
@ -494,7 +487,7 @@ GEM
memoist (0.16.0) memoist (0.16.0)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.9.0)
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
@ -589,9 +582,7 @@ GEM
parallel (1.12.1) parallel (1.12.1)
parser (2.5.1.0) parser (2.5.1.0)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.8.2)
blankslate (~> 2.0)
path_expander (1.0.2)
peek (1.0.1) peek (1.0.1)
concurrent-ruby (>= 0.9.0) concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0) concurrent-ruby-ext (>= 0.9.0)
@ -636,12 +627,11 @@ GEM
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.4) prometheus-client-mmap (0.9.4)
pry (0.10.4) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.9.0)
slop (~> 3.4) pry-byebug (3.4.3)
pry-byebug (3.4.2) byebug (>= 9.0, < 9.1)
byebug (~> 9.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.5)
pry (>= 0.9.10) pry (>= 0.9.10)
@ -692,8 +682,7 @@ GEM
activesupport (= 4.2.10) activesupport (= 4.2.10)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (3.0.0)
rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.1) rake (12.3.1)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
@ -730,6 +719,7 @@ GEM
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.4.1) redis-store (1.4.1)
redis (>= 2.2, < 5) redis (>= 2.2, < 5)
regexp_parser (0.5.0)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
@ -742,7 +732,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
retriable (3.1.1) retriable (3.1.2)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.2.1) rouge (3.2.1)
@ -780,6 +770,9 @@ GEM
rspec-core rspec-core
rspec-set (0.1.3) rspec-set (0.1.3)
rspec-support (3.7.1) rspec-support (3.7.1)
rspec_junit_formatter (0.2.3)
builder (< 4)
rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5) rspec_profiling (0.0.5)
activerecord activerecord
pg pg
@ -808,7 +801,7 @@ GEM
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.6.2) rubyntlm (0.6.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.1) rubyzip (1.2.2)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.4) rugged (0.27.4)
@ -872,7 +865,6 @@ GEM
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.5.1) slack-notifier (1.5.1)
slop (3.6.0)
spring (2.0.1) spring (2.0.1)
activesupport (>= 4.2) activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
@ -912,8 +904,8 @@ GEM
tilt (2.0.8) tilt (2.0.8)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
toml (0.1.2) toml (0.2.0)
parslet (~> 1.5.0) parslet (~> 1.8.0)
toml-rb (1.0.0) toml-rb (1.0.0)
citrus (~> 3.0, > 3.0) citrus (~> 3.0, > 3.0)
trollop (2.1.3) trollop (2.1.3)
@ -999,12 +991,12 @@ DEPENDENCIES
bootsnap (~> 1.3) bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 4.2) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.5)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.15) capybara (~> 2.15)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.2) carrierwave (= 1.2.3)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
@ -1029,26 +1021,24 @@ DEPENDENCIES
faraday (~> 0.12) faraday (~> 0.12)
fast_blank fast_blank
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.10.0)
flipper (~> 0.13.0) flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0.1) fog-aws (~> 2.0.1)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 1.3.3) fog-google (~> 1.7.1)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7) font-awesome-rails (~> 4.7)
foreman (~> 0.84.0) foreman (~> 0.84.0)
fuubar (~> 2.2.0) fuubar (~> 2.2.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.3) gemojione (~> 3.3)
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.113.0) gitaly-proto (~> 0.117.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
@ -1057,7 +1047,7 @@ DEPENDENCIES
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.19.8) google-api-client (~> 0.23)
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
@ -1080,13 +1070,14 @@ DEPENDENCIES
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
js_regex (~> 2.2.1)
json-schema (~> 2.8.0) json-schema (~> 2.8.0)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.16) knapsack (~> 1.16)
kubeclient (~> 3.1.0) kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 5.4)
licensee (~> 8.9) licensee (~> 8.9)
lograge (~> 0.5) lograge (~> 0.5)
loofah (~> 2.2) loofah (~> 2.2)
@ -1136,7 +1127,7 @@ DEPENDENCIES
rails (= 4.2.10) rails (= 4.2.10)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9) rails-i18n (~> 4.0.9)
rainbow (~> 2.2) rainbow (~> 3.0)
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbtrace (~> 0.4) rbtrace (~> 0.4)
@ -1155,6 +1146,7 @@ DEPENDENCIES
rspec-rails (~> 3.7.0) rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
rspec_junit_formatter
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.54.0) rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1) rubocop-rspec (~> 1.22.1)
@ -1207,4 +1199,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.2 1.16.4

View file

@ -89,12 +89,11 @@ GEM
bindata (2.4.3) bindata (2.4.3)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
bootsnap (1.3.1) bootsnap (1.3.1)
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
browser (2.2.0) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -126,7 +125,7 @@ GEM
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
citrus (3.0.2) citrus (3.0.2)
coderay (1.1.1) coderay (1.1.2)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
commonmarker (0.17.8) commonmarker (0.17.8)
@ -212,12 +211,7 @@ GEM
fast_blank (1.0.0) fast_blank (1.0.0)
fast_gettext (1.6.0) fast_gettext (1.6.0)
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.18) ffi (1.9.25)
flay (2.10.0)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flipper (0.13.0) flipper (0.13.0)
flipper-active_record (0.13.0) flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6) activerecord (>= 3.2, < 6)
@ -242,11 +236,11 @@ GEM
builder builder
excon (~> 0.58) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (1.3.3) fog-google (1.7.1)
fog-core fog-core
fog-json fog-json
fog-xml fog-xml
google-api-client (~> 0.19.1) google-api-client (~> 0.23.0)
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
@ -272,8 +266,6 @@ GEM
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (3.3.0) gemojione (3.3.0)
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
@ -287,7 +279,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.113.0) gitaly-proto (0.117.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
@ -334,7 +326,7 @@ GEM
actionpack (>= 3.0) actionpack (>= 3.0)
multi_json multi_json
request_store (>= 1.0) request_store (>= 1.0)
google-api-client (0.19.8) google-api-client (0.23.4)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0) googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0) httpclient (>= 2.8.1, < 3.0)
@ -433,6 +425,8 @@ GEM
multipart-post multipart-post
oauth (~> 0.5, >= 0.5.0) oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
js_regex (2.2.1)
regexp_parser (>= 0.4.11, <= 0.5.0)
json (1.8.6) json (1.8.6)
json-jwt (1.9.4) json-jwt (1.9.4)
activesupport activesupport
@ -468,13 +462,12 @@ GEM
actionmailer (>= 3.2) actionmailer (>= 3.2)
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
license_finder (3.1.1) license_finder (5.4.0)
bundler bundler
httparty
rubyzip rubyzip
thor thor
toml (= 0.1.2) toml (= 0.2.0)
with_env (> 1.0) with_env (= 1.1.0)
xml-simple xml-simple
licensee (8.9.2) licensee (8.9.2)
rugged (~> 0.24) rugged (~> 0.24)
@ -497,7 +490,7 @@ GEM
memoist (0.16.0) memoist (0.16.0)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.9.0)
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
@ -593,9 +586,7 @@ GEM
parallel (1.12.1) parallel (1.12.1)
parser (2.5.1.0) parser (2.5.1.0)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.8.2)
blankslate (~> 2.0)
path_expander (1.0.2)
peek (1.0.1) peek (1.0.1)
concurrent-ruby (>= 0.9.0) concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0) concurrent-ruby-ext (>= 0.9.0)
@ -640,12 +631,11 @@ GEM
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.4) prometheus-client-mmap (0.9.4)
pry (0.10.4) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.9.0)
slop (~> 3.4) pry-byebug (3.4.3)
pry-byebug (3.4.2) byebug (>= 9.0, < 9.1)
byebug (~> 9.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.5)
pry (>= 0.9.10) pry (>= 0.9.10)
@ -701,8 +691,7 @@ GEM
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (3.0.0)
rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.1) rake (12.3.1)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
@ -739,6 +728,7 @@ GEM
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.4.1) redis-store (1.4.1)
redis (>= 2.2, < 5) redis (>= 2.2, < 5)
regexp_parser (0.5.0)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
@ -751,10 +741,10 @@ GEM
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
retriable (3.1.1) retriable (3.1.2)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.2.0) rouge (3.2.1)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
@ -789,6 +779,8 @@ GEM
rspec-core rspec-core
rspec-set (0.1.3) rspec-set (0.1.3)
rspec-support (3.7.1) rspec-support (3.7.1)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rspec_profiling (0.0.5) rspec_profiling (0.0.5)
activerecord activerecord
pg pg
@ -817,10 +809,10 @@ GEM
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.6.2) rubyntlm (0.6.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.1) rubyzip (1.2.2)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.2) rugged (0.27.4)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
@ -881,7 +873,6 @@ GEM
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.5.1) slack-notifier (1.5.1)
slop (3.6.0)
spring (2.0.1) spring (2.0.1)
activesupport (>= 4.2) activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
@ -919,8 +910,8 @@ GEM
tilt (2.0.8) tilt (2.0.8)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
toml (0.1.2) toml (0.2.0)
parslet (~> 1.5.0) parslet (~> 1.8.0)
toml-rb (1.0.0) toml-rb (1.0.0)
citrus (~> 3.0, > 3.0) citrus (~> 3.0, > 3.0)
trollop (2.1.3) trollop (2.1.3)
@ -1009,12 +1000,12 @@ DEPENDENCIES
bootsnap (~> 1.3) bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 4.2) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.5)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.15) capybara (~> 2.15)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.2) carrierwave (= 1.2.3)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
@ -1039,26 +1030,24 @@ DEPENDENCIES
faraday (~> 0.12) faraday (~> 0.12)
fast_blank fast_blank
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.10.0)
flipper (~> 0.13.0) flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0.1) fog-aws (~> 2.0.1)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 1.3.3) fog-google (~> 1.7.1)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7) font-awesome-rails (~> 4.7)
foreman (~> 0.84.0) foreman (~> 0.84.0)
fuubar (~> 2.2.0) fuubar (~> 2.2.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.3) gemojione (~> 3.3)
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.113.0) gitaly-proto (~> 0.117.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
@ -1067,7 +1056,7 @@ DEPENDENCIES
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.19.8) google-api-client (~> 0.23)
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
@ -1090,13 +1079,14 @@ DEPENDENCIES
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
js_regex (~> 2.2.1)
json-schema (~> 2.8.0) json-schema (~> 2.8.0)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.16) knapsack (~> 1.16)
kubeclient (~> 3.1.0) kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 5.4)
licensee (~> 8.9) licensee (~> 8.9)
lograge (~> 0.5) lograge (~> 0.5)
loofah (~> 2.2) loofah (~> 2.2)
@ -1147,7 +1137,7 @@ DEPENDENCIES
rails-controller-testing rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rainbow (~> 2.2) rainbow (~> 3.0)
raindrops (~> 0.18) raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbtrace (~> 0.4) rbtrace (~> 0.4)
@ -1166,6 +1156,7 @@ DEPENDENCIES
rspec-rails (~> 3.7.0) rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
rspec_junit_formatter
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.54.0) rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1) rubocop-rspec (~> 1.22.1)
@ -1217,4 +1208,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.3 1.16.4

View file

@ -15,8 +15,9 @@
- [Between the 1st and the 7th](#between-the-1st-and-the-7th) - [Between the 1st and the 7th](#between-the-1st-and-the-7th)
- [On the 7th](#on-the-7th) - [On the 7th](#on-the-7th)
- [After the 7th](#after-the-7th) - [After the 7th](#after-the-7th)
- [Regressions](#regressions) - [Bugs](#bugs)
- [How to manage a regression](#how-to-manage-a-regression) - [Regressions](#regressions)
- [Managing bugs](#managing-bugs)
- [Release retrospective and kickoff](#release-retrospective-and-kickoff) - [Release retrospective and kickoff](#release-retrospective-and-kickoff)
- [Retrospective](#retrospective) - [Retrospective](#retrospective)
- [Kickoff](#kickoff) - [Kickoff](#kickoff)
@ -115,6 +116,11 @@ target. However, if one does and falls into either of the above categories, it's
the reviewer's responsibility to manage the above communication and assignment the reviewer's responsibility to manage the above communication and assignment
on behalf of the community member. on behalf of the community member.
Every new feature or change should be shipped with its corresponding documentation
in accordance with the
[documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html)
and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html).
#### What happens if these deadlines are missed? #### What happens if these deadlines are missed?
If a small or large feature is _not_ with a maintainer or reviewer by the If a small or large feature is _not_ with a maintainer or reviewer by the
@ -140,14 +146,9 @@ and to prevent any last minute surprises.
### On the 7th ### On the 7th
Merge requests should still be complete, following the Merge requests should still be complete, following the [definition of done][done].
[definition of done][done]. The single exception is documentation, and this can
only be left until after the freeze if:
* There is a follow-up issue to add documentation. #### Feature merge requests
* It is assigned to the person writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
If a merge request is not ready, but the developers and Product Manager If a merge request is not ready, but the developers and Product Manager
responsible for the feature think it is essential that it is in the release, responsible for the feature think it is essential that it is in the release,
@ -163,15 +164,32 @@ information, see
[Automatic CE->EE merge][automatic_ce_ee_merge] and [Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features]. [Guidelines for implementing Enterprise Edition features][ee_features].
#### Documentation merge requests
Documentation is part of the product and must be shipped with the feature.
The single exception for the feature freeze is documentation, and it can
be left to be **merged up to the 14th** if:
* There is a follow-up issue to add documentation.
* It is assigned to the developer writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the labels ~Documentation, ~Deliverable,
~missed-deliverable, and "pick into X.Y" applied.
* It must be reviewed and approved by a technical writer.
For more information read the process for
[documentation shipped late](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late).
### After the 7th ### After the 7th
Once the stable branch is frozen, the only MRs that can be cherry-picked into Once the stable branch is frozen, the only MRs that can be cherry-picked into
the stable branch are: the stable branch are:
* Fixes for [regressions](#regressions) * Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section.
* Fixes for security issues * Fixes for security issues
* Fixes or improvements to automated QA scenarios * Fixes or improvements to automated QA scenarios
* Documentation updates for changes in the same release * [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the During the feature freeze all merge requests that are meant to go into the
@ -201,48 +219,59 @@ you can ask for an exception to be made.
Check [this guide](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md) about how to open an exception request before opening one. Check [this guide](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md) about how to open an exception request before opening one.
## Regressions ## Bugs
A regression for a particular monthly release is a bug that exists in that A ~bug is a defect, error, failure which causes the system to behave incorrectly or prevents it from fulfilling the product requirements.
release, but wasn't present in the release before. This includes bugs in
features that were only added in that monthly release. Every regression **must**
have the milestone of the release it was introduced in - if a regression doesn't
have a milestone, it might be 'just' a bug!
For instance, if 10.5.0 adds a feature, and that feature doesn't work correctly, The level of impact of a ~bug can vary from blocking a whole functionality
then this is a regression in 10.5. If 10.5.1 then fixes that, but 10.5.3 somehow or a feature usability bug. A bug should always be linked to a severity level.
reintroduces the bug, then this bug is still a regression in 10.5. Refer to our [severity levels](../CONTRIBUTING.md#severity-labels)
Because GitLab.com runs release candidates of new releases, a regression can be Whether the bug is also a regression or not, the triage process should start as soon as possible.
reported in a release before its 'official' release date on the 22nd of the Ensure that the Engineering Manager and/or the Product Manager for the relative area is involved to prioritize the work as needed.
month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
### How to manage a regression ### Regressions
Regressions are very important, and they should be considered high priority A ~regression implies that a previously **verified working functionality** no longer works.
issues that should be solved as soon as possible, especially if they affect Regressions are a subset of bugs. We use the ~regression label to imply that the defect caused the functionality to regress.
users. Despite that, ~regression label itself does not imply when the issue The label tells us that something worked before and it needs extra attention from Engineering and Product Managers to schedule/reschedule.
will be scheduled.
When a regression is found: The regression label does not apply to ~bugs for new features for which functionality was **never verified as working**.
1. Create an issue describing the problem in the most detailed way possible These, by definition, are not regressions.
1. If possible, provide links to real examples and how to reproduce the problem
A regression should always have the `regression:xx.x` label on it to designate when it was introduced.
Regressions should be considered high priority issues that should be solved as soon as possible, especially if they have severe impact on users.
### Managing bugs
**Prioritization:** We give higher priority to regressions on features that worked in the last recent monthly release and the current release candidates.
The two scenarios below can [bypass the exception request in the release process](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md#after-the-7th), where the affected regression version matches the current monthly release version.
* A regression which worked in the **Last monthly release**
* **Example:** In 11.0 we released a new `feature X` that is verified as working. Then in release 11.1 the feature no longer works, this is regression for 11.1. The issue should have the `regression:11.1` label.
* *Note:* When we say `the last recent monthly release`, this can refer to either the version currently running on GitLab.com, or the most recent version available in the package repositories.
* A regression which worked in the **Current release candidates**
* **Example:** In 11.1-RC3 we shipped a new feature which has been verified as working. Then in 11.1-RC5 the feature no longer works, this is regression for 11.1. The issue should have the `regression:11.1` label.
* *Note:* Because GitLab.com runs release candidates of new releases, a regression can be reported in a release before its 'official' release date on the 22nd of the month.
When a bug is found:
1. Create an issue describing the problem in the most detailed way possible.
1. If possible, provide links to real examples and how to reproduce the problem.
1. Label the issue properly, using the [team label](../CONTRIBUTING.md#team-labels), 1. Label the issue properly, using the [team label](../CONTRIBUTING.md#team-labels),
the [subject label](../CONTRIBUTING.md#subject-labels) the [subject label](../CONTRIBUTING.md#subject-labels)
and any other label that may apply in the specific case and any other label that may apply in the specific case
1. Add the ~bug and ~regression labels 1. Notify the respective Engineering Manager to evaluate and apply the [Severity label](../CONTRIBUTING.md#bug-severity-labels) and [Priority label](../CONTRIBUTING.md#bug-priority-labels).
1. Notify the respective Engineering Manager to evaluate the Severity of the regression and add a [Severity label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-severity-labels). The counterpart Product Manager is included to weigh-in on prioritization as needed to set the [Priority label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-priority-labels). The counterpart Product Manager is included to weigh-in on prioritization as needed.
1. If the regression is either an ~S1, ~S2 or ~S3 severity, label the regression with the current milestone as it should be fixed in the current milestone. 1. If the ~bug is **NOT** a regression:
1. If the regression was introduced in an RC of the current release, label with ~Deliverable 1. The Engineering Manager decides which milestone the bug will be fixed. The appropriate milestone is applied.
1. If the regression was introduced in the previous release, label with ~"Next Patch Release" 1. If the bug is a ~regression:
1. If the regression is an ~S4 severity, the regression may be scheduled for later milestones at the discretion of Engineering Manager and Product Manager. 1. Determine the release that the regression affects and add the corresponding `regression:xx.x` label.
1. If the affected release version can't be determined, add the generic ~regression label for the time being.
When a new issue is found, the fix should start as soon as possible. You can 1. If the affected version `xx.x` in `regression:xx.x` is the **current release**, it's recommended to schedule the fix for the current milestone.
ping the Engineering Manager or the Product Manager for the relative area to 1. This falls under regressions which worked in the last release and the current RCs. More detailed explanations in the **Prioritization** section above.
make them aware of the issue earlier. They will analyze the priority and change 1. If the affected version `xx.x` in `regression:xx.x` is older than the **current release**
it if needed. 1. If the regression is an ~S1 severity, it's recommended to schedule the fix for the current milestone. We would like to fix the highest severity regression as soon as we can.
1. If the regression is an ~S2, ~S3 or ~S4 severity, the regression may be scheduled for later milestones at the discretion of the Engineering Manager and Product Manager.
## Release retrospective and kickoff ## Release retrospective and kickoff

View file

@ -58,7 +58,7 @@ You can access a new installation with the login **`root`** and password **`5ive
## Contributing ## Contributing
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details.
## Licensing ## Licensing
@ -66,7 +66,7 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
## Install a development environment ## Install a development environment

View file

@ -1 +1 @@
11.2.8 11.3.10

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -15,6 +15,7 @@ const Api = {
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
templatesPath: '/api/:version/templates/:key',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
@ -265,6 +266,12 @@ const Api = {
}); });
}, },
templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key);
return axios.get(url, { params });
},
buildUrl(url) { buildUrl(url) {
let urlRoot = ''; let urlRoot = '';
if (gon.relative_url_root != null) { if (gon.relative_url_root != null) {

View file

@ -109,8 +109,6 @@ export class AwardsHandler {
} }
const $menu = $(`.${this.menuClass}`); const $menu = $(`.${this.menuClass}`);
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) { if ($menu.length) {
if ($menu.is('.is-visible')) { if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active'); $addBtn.removeClass('is-active');
@ -134,9 +132,6 @@ export class AwardsHandler {
}, 200); }, 200);
}); });
} }
$thumbsBtn.toggleClass('disabled', $userAuthored);
$thumbsBtn.prop('disabled', $userAuthored);
} }
// Create the emoji menu with the first category of emojis. // Create the emoji menu with the first category of emojis.
@ -364,10 +359,6 @@ export class AwardsHandler {
return $emojiButton.hasClass('active'); return $emojiButton.hasClass('active');
} }
isUserAuthored($button) {
return $button.hasClass('js-user-authored');
}
decrementCounter($emojiButton, emoji) { decrementCounter($emojiButton, emoji) {
const counter = $('.js-counter', $emojiButton); const counter = $('.js-counter', $emojiButton);
const counterNumber = parseInt(counter.text(), 10); const counterNumber = parseInt(counter.text(), 10);
@ -474,20 +465,16 @@ export class AwardsHandler {
} }
postEmoji($emojiButton, awardUrl, emoji, callback) { postEmoji($emojiButton, awardUrl, emoji, callback) {
if (this.isUserAuthored($emojiButton)) { axios
this.userAuthored($emojiButton); .post(awardUrl, {
} else { name: emoji,
axios })
.post(awardUrl, { .then(({ data }) => {
name: emoji, if (data.ok) {
}) callback();
.then(({ data }) => { }
if (data.ok) { })
callback(); .catch(() => flash(__('Something went wrong on our end.')));
}
})
.catch(() => flash(__('Something went wrong on our end.')));
}
} }
findEmojiIcon(votesBlock, emoji) { findEmojiIcon(votesBlock, emoji) {

View file

@ -23,6 +23,11 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
wasValidated: false,
};
},
computed: { computed: {
...mapState([ ...mapState([
'badgeInAddForm', 'badgeInAddForm',
@ -39,16 +44,6 @@ export default {
return this.badgeInAddForm; return this.badgeInAddForm;
}, },
canSubmit() {
return (
this.badge !== null &&
this.badge.imageUrl &&
this.badge.imageUrl.trim() !== '' &&
this.badge.linkUrl &&
this.badge.linkUrl.trim() !== '' &&
!this.isSaving
);
},
helpText() { helpText() {
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha'] const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
.map(placeholder => `<code>%{${placeholder}}</code>`) .map(placeholder => `<code>%{${placeholder}}</code>`)
@ -93,11 +88,18 @@ export default {
}); });
}, },
}, },
submitButtonLabel() { badgeImageUrlExample() {
if (this.isEditing) { const exampleUrl =
return s__('Badges|Save changes'); 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg';
} return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
return s__('Badges|Add badge'); exampleUrl,
});
},
badgeLinkUrlExample() {
const exampleUrl = 'https://example.gitlab.com/%{project_path}';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl,
});
}, },
}, },
methods: { methods: {
@ -109,7 +111,9 @@ export default {
this.stopEditing(); this.stopEditing();
}, },
onSubmit() { onSubmit() {
if (!this.canSubmit) { const form = this.$el;
if (!form.checkValidity()) {
this.wasValidated = true;
return Promise.resolve(); return Promise.resolve();
} }
@ -117,6 +121,7 @@ export default {
return this.saveBadge() return this.saveBadge()
.then(() => { .then(() => {
createFlash(s__('Badges|The badge was saved.'), 'notice'); createFlash(s__('Badges|The badge was saved.'), 'notice');
this.wasValidated = false;
}) })
.catch(error => { .catch(error => {
createFlash( createFlash(
@ -129,6 +134,7 @@ export default {
return this.addBadge() return this.addBadge()
.then(() => { .then(() => {
createFlash(s__('Badges|A new badge was added.'), 'notice'); createFlash(s__('Badges|A new badge was added.'), 'notice');
this.wasValidated = false;
}) })
.catch(error => { .catch(error => {
createFlash( createFlash(
@ -138,47 +144,58 @@ export default {
}); });
}, },
}, },
badgeImageUrlPlaceholder:
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
}; };
</script> </script>
<template> <template>
<form <form
class="prepend-top-default append-bottom-default" :class="{ 'was-validated': wasValidated }"
class="prepend-top-default append-bottom-default needs-validation"
novalidate
@submit.prevent.stop="onSubmit" @submit.prevent.stop="onSubmit"
> >
<div class="form-group"> <div class="form-group">
<label for="badge-link-url">{{ s__('Badges|Link') }}</label> <label
for="badge-link-url"
class="label-bold"
>{{ s__('Badges|Link') }}</label>
<p v-html="helpText"></p>
<input <input
id="badge-link-url" id="badge-link-url"
v-model="linkUrl" v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder" type="URL"
type="text"
class="form-control" class="form-control"
required
@input="debouncedPreview" @input="debouncedPreview"
/> />
<span <div class="invalid-feedback">
class="form-text text-muted" {{ s__('Badges|Please fill in a valid URL') }}
v-html="helpText" </div>
></span> <span class="form-text text-muted">
{{ badgeLinkUrlExample }}
</span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label> <label
for="badge-image-url"
class="label-bold"
>{{ s__('Badges|Badge image URL') }}</label>
<p v-html="helpText"></p>
<input <input
id="badge-image-url" id="badge-image-url"
v-model="imageUrl" v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder" type="URL"
type="text"
class="form-control" class="form-control"
required
@input="debouncedPreview" @input="debouncedPreview"
/> />
<span <div class="invalid-feedback">
class="form-text text-muted" {{ s__('Badges|Please fill in a valid URL') }}
v-html="helpText" </div>
></span> <span class="form-text text-muted">
{{ badgeImageUrlExample }}
</span>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -200,20 +217,32 @@ export default {
>{{ s__('Badges|No image to preview') }}</p> >{{ s__('Badges|No image to preview') }}</p>
</div> </div>
<div class="row-content-block"> <div
v-if="isEditing"
class="row-content-block"
>
<loading-button <loading-button
:disabled="!canSubmit"
:loading="isSaving" :loading="isSaving"
:label="submitButtonLabel" :label="s__('Badges|Save changes')"
type="submit" type="submit"
container-class="btn btn-success" container-class="btn btn-success"
/> />
<button <button
v-if="isEditing"
class="btn btn-cancel" class="btn btn-cancel"
type="button" type="button"
@click="onCancel" @click="onCancel"
>{{ __('Cancel') }}</button> >{{ __('Cancel') }}</button>
</div> </div>
<div
v-else
class="form-group"
>
<loading-button
:loading="isSaving"
:label="s__('Badges|Add badge')"
type="submit"
container-class="btn btn-success"
/>
</div>
</form> </form>
</template> </template>

View file

@ -28,7 +28,7 @@ export default {
{{ s__('Badges|Your badges') }} {{ s__('Badges|Your badges') }}
<span <span
v-show="!isLoading" v-show="!isLoading"
class="badge" class="badge badge-pill"
>{{ badges.length }}</span> >{{ badges.length }}</span>
</div> </div>
<loading-icon <loading-icon

View file

@ -43,13 +43,13 @@ export default {
<badge <badge
:image-url="badge.renderedImageUrl" :image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl" :link-url="badge.renderedLinkUrl"
class="table-section section-30" class="table-section section-40"
/> />
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span> <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10"> <div class="table-section section-15">
<span class="badge">{{ badgeKindText }}</span> <span class="badge badge-pill">{{ badgeKindText }}</span>
</div> </div>
<div class="table-section section-10 table-button-footer"> <div class="table-section section-15 table-button-footer">
<div <div
v-if="canEditBadge" v-if="canEditBadge"
class="table-action-buttons"> class="table-action-buttons">

View file

@ -203,7 +203,7 @@ export default {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() { onScroll() {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage(); this.loadNextPage();
} }
}, },

View file

@ -3,7 +3,7 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../flash'; import Flash from '../../flash';
import { __ } from '../../locale'; import { sprintf, __ } from '../../locale';
import Sidebar from '../../right_sidebar'; import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
@ -55,8 +55,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
return this.issue.labels && this.issue.labels.length; return this.issue.labels && this.issue.labels.length;
}, },
labelDropdownTitle() { labelDropdownTitle() {
return this.hasLabels ? return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
`${this.issue.labels[0].title} ${this.issue.labels.length - 1}+ more` : 'Label'; firstLabel: this.issue.labels[0].title,
labelCount: this.issue.labels.length - 1
}) : __('Label');
}, },
selectedLabels() { selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';

View file

@ -1,6 +1,6 @@
<script> <script>
/* global ListIssue */ /* global ListIssue */
import queryData from '~/boards/utils/query_data'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import ModalHeader from './header.vue'; import ModalHeader from './header.vue';
import ModalList from './list.vue'; import ModalList from './list.vue';
@ -109,13 +109,11 @@
loadIssues(clearIssues = false) { loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false; if (!this.showAddIssuesModal) return false;
return gl.boardService return gl.boardService.getBacklog({
.getBacklog( ...urlParamsToObject(this.filter.path),
queryData(this.filter.path, { page: this.page,
page: this.page, per: this.perPage,
per: this.perPage, })
}),
)
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
if (clearIssues) { if (clearIssues) {

View file

@ -1,9 +1,10 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */ /* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */
/* global ListIssue */ /* global ListIssue */
import { __ } from '~/locale';
import ListLabel from '~/vue_shared/models/label'; import ListLabel from '~/vue_shared/models/label';
import ListAssignee from '~/vue_shared/models/assignee'; import ListAssignee from '~/vue_shared/models/assignee';
import queryData from '../utils/query_data'; import { urlParamsToObject } from '~/lib/utils/common_utils';
const PER_PAGE = 20; const PER_PAGE = 20;
@ -30,7 +31,7 @@ class List {
this.id = obj.id; this.id = obj.id;
this._uid = this.guid(); this._uid = this.guid();
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.list_type === 'backlog' ? __('Open') : obj.title;
this.type = obj.list_type; this.type = obj.list_type;
const typeInfo = this.getTypeInfo(this.type); const typeInfo = this.getTypeInfo(this.type);
@ -114,7 +115,10 @@ class List {
} }
getIssues(emptyIssues = true) { getIssues(emptyIssues = true) {
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); const data = {
...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path),
page: this.page,
};
if (this.label && data.label_name) { if (this.label && data.label_name) {
data.label_name = data.label_name.filter(label => label !== this.label.title); data.label_name = data.label_name.filter(label => label !== this.label.title);

View file

@ -1,21 +0,0 @@
export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
if (filterParam === '') return dataParam;
const data = dataParam;
const paramSplit = filterParam.split('=');
const paramKeyNormalized = paramSplit[0].replace('[]', '');
const isArray = paramSplit[0].indexOf('[]');
const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' '));
if (isArray !== -1) {
if (!data[paramKeyNormalized]) {
data[paramKeyNormalized] = [];
}
data[paramKeyNormalized].push(value);
} else {
data[paramKeyNormalized] = value;
}
return data;
}, extraData);

View file

@ -1,16 +1,12 @@
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Vue from 'vue'; import Vue from 'vue';
import initDismissableCallout from '~/dismissable_callout';
import { s__, sprintf } from '../locale'; import { s__, sprintf } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels'; import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from './constants';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue'; import applications from './components/applications.vue';
@ -66,6 +62,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications();
@ -129,7 +126,8 @@ export default class Clusters {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
this.poll.makeRequest(); this.poll.makeRequest();
} else { } else {
this.service.fetchData() this.service
.fetchData()
.then(data => this.handleSuccess(data)) .then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError()); .catch(() => Clusters.handleError());
} }
@ -177,15 +175,21 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) { checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap) const appTitles = Object.keys(newApplicationMap)
.filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && .filter(
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && appId =>
prevApplicationMap[appId].status !== null) newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null,
)
.map(appId => newApplicationMap[appId].title); .map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) { if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), { const text = sprintf(
appList: appTitles.join(', '), s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
}); {
appList: appTitles.join(', '),
},
);
Flash(text, 'notice', this.successApplicationContainer); Flash(text, 'notice', this.successApplicationContainer);
} }
} }
@ -218,13 +222,18 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId, data.params) this.service
.installApplication(appId, data.params)
.then(() => { .then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
}) })
.catch(() => { .catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed')); this.store.updateAppProperty(
appId,
'requestReason',
s__('ClusterIntegration|Request to begin installing failed'),
);
}); });
} }

View file

@ -1,14 +1,14 @@
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons'; import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; import initDismissableCallout from '~/dismissable_callout';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
export default () => { export default () => {
const clusterList = document.querySelector('.js-clusters-list'); const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer(); initDismissableCallout('.gcp-signup-offer');
// The empty state won't have a clusterList // The empty state won't have a clusterList
if (clusterList) { if (clusterList) {

View file

@ -1,4 +1,10 @@
import Vue from 'vue'; import Vue from 'vue';
import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar'; import progressBar from '@gitlab-org/gitlab-ui/dist/components/base/progress_bar';
import modal from '@gitlab-org/gitlab-ui/dist/components/base/modal';
import dModal from '@gitlab-org/gitlab-ui/dist/directives/modal';
Vue.component('gl-progress-bar', progressBar); Vue.component('gl-progress-bar', progressBar);
Vue.component('gl-ui-modal', modal);
Vue.directive('gl-modal', dModal);

View file

@ -1,4 +1,4 @@
/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */ /* eslint-disable object-shorthand, func-names, no-else-return */
/* global CommentsStore */ /* global CommentsStore */
/* global ResolveService */ /* global ResolveService */
@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({
}; };
}, },
computed: { computed: {
showButton: function () { showButton: function() {
if (this.discussion) { if (this.discussion) {
return this.discussion.isResolvable(); return this.discussion.isResolvable();
} else { } else {
return false; return false;
} }
}, },
isDiscussionResolved: function () { isDiscussionResolved: function() {
if (this.discussion) { if (this.discussion) {
return this.discussion.isResolved(); return this.discussion.isResolved();
} else { } else {
return false; return false;
} }
}, },
buttonText: function () { buttonText: function() {
if (this.isDiscussionResolved) { if (this.isDiscussionResolved) {
return "Unresolve discussion"; return 'Unresolve discussion';
} else { } else {
return "Resolve discussion"; return 'Resolve discussion';
} }
}, },
loading: function () { loading: function() {
if (this.discussion) { if (this.discussion) {
return this.discussion.loading; return this.discussion.loading;
} else { } else {
return false; return false;
} }
} },
}, },
created: function () { created: function() {
CommentsStore.createDiscussion(this.discussionId, this.canResolve); CommentsStore.createDiscussion(this.discussionId, this.canResolve);
this.discussion = CommentsStore.state[this.discussionId]; this.discussion = CommentsStore.state[this.discussionId];
}, },
methods: { methods: {
resolve: function () { resolve: function() {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
} },
}, },
}); });

View file

@ -8,9 +8,7 @@ window.gl = window.gl || {};
class ResolveServiceClass { class ResolveServiceClass {
constructor(root) { constructor(root) {
this.noteResource = Vue.resource( this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
`${root}/notes{/noteId}/resolve?html=true`,
);
this.discussionResource = Vue.resource( this.discussionResource = Vue.resource(
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`, `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
); );
@ -51,10 +49,7 @@ class ResolveServiceClass {
discussion.updateHeadline(data); discussion.updateHeadline(data);
}) })
.catch( .catch(
() => () => new Flash('An error occurred when trying to resolve a discussion. Please try again.'),
new Flash(
'An error occurred when trying to resolve a discussion. Please try again.',
),
); );
} }

View file

@ -59,7 +59,7 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapGetters('diffs', ['isParallelView']), ...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched']), ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() { targetBranch() {
return { return {
branchName: this.targetBranchName, branchName: this.targetBranchName,
@ -112,18 +112,45 @@ export default {
}, },
created() { created() {
this.adjustView(); this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
}, },
methods: { methods: {
...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']), ...mapActions('diffs', [
'setBaseConfig',
'fetchDiffFiles',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
]),
fetchData() { fetchData() {
this.fetchDiffFiles().catch(() => { this.fetchDiffFiles()
createFlash(__('Something went wrong on our end. Please try again!')); .then(() => {
}); requestIdleCallback(
() => {
this.setDiscussions();
this.startRenderDiffsQueue();
},
{ timeout: 1000 },
);
})
.catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
});
if (!this.isNotesFetched) { if (!this.isNotesFetched) {
eventHub.$emit('fetchNotesData'); eventHub.$emit('fetchNotesData');
} }
}, },
setDiscussions() {
if (this.isNotesFetched) {
requestIdleCallback(
() => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
},
{ timeout: 1000 },
);
}
},
adjustView() { adjustView() {
if (this.shouldShow && this.isParallelView) { if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer(); window.mrTabs.expandViewContainer();

View file

@ -63,7 +63,7 @@ export default {
v-else v-else
role="button" role="button"
class="fa fa-times dropdown-input-search" class="fa fa-times dropdown-input-search"
@click="clearSearch" @click.stop.prevent="clearSearch"
></i> ></i>
</div> </div>
<div class="dropdown-content"> <div class="dropdown-content">

View file

@ -1,4 +1,5 @@
<script> <script>
import { mapActions } from 'vuex';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue'; import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default { export default {
@ -11,6 +12,14 @@ export default {
required: true, required: true,
}, },
}, },
methods: {
...mapActions('diffs', ['removeDiscussionsFromDiff']),
deleteNoteHandler(discussion) {
if (discussion.notes.length <= 1) {
this.removeDiscussionsFromDiff(discussion);
}
},
},
}; };
</script> </script>
@ -31,6 +40,7 @@ export default {
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true" :always-expanded="true"
:discussions-by-diff-order="true" :discussions-by-diff-order="true"
@noteDeleted="deleteNoteHandler"
/> />
</ul> </ul>
</div> </div>

View file

@ -1,5 +1,5 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
@ -30,6 +30,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() { isCollapsed() {
return this.file.collapsed || false; return this.file.collapsed || false;
}, },
@ -44,18 +45,27 @@ export default {
); );
}, },
showExpandMessage() { showExpandMessage() {
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge; return (
this.isCollapsed ||
!this.file.highlightedDiffLines &&
!this.isLoadingCollapsedDiff &&
!this.file.tooLarge &&
this.file.text
);
},
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['loadCollapsedDiff']), ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
handleToggle() { handleToggle() {
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file; const { highlightedDiffLines, parallelDiffLines } = this.file;
if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) {
if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) {
this.handleLoadCollapsedDiff(); this.handleLoadCollapsedDiff();
} else { } else {
this.file.collapsed = !this.file.collapsed; this.file.collapsed = !this.file.collapsed;
this.file.renderIt = true;
} }
}, },
handleLoadCollapsedDiff() { handleLoadCollapsedDiff() {
@ -65,6 +75,15 @@ export default {
.then(() => { .then(() => {
this.isLoadingCollapsedDiff = false; this.isLoadingCollapsedDiff = false;
this.file.collapsed = false; this.file.collapsed = false;
this.file.renderIt = true;
})
.then(() => {
requestIdleCallback(
() => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
},
{ timeout: 1000 },
);
}) })
.catch(() => { .catch(() => {
this.isLoadingCollapsedDiff = false; this.isLoadingCollapsedDiff = false;
@ -121,16 +140,16 @@ export default {
</div> </div>
<diff-content <diff-content
v-if="!isCollapsed" v-if="!isCollapsed && file.renderIt"
:class="{ hidden: isCollapsed || file.tooLarge }" :class="{ hidden: isCollapsed || file.tooLarge }"
:diff-file="file" :diff-file="file"
/> />
<loading-icon <loading-icon
v-if="isLoadingCollapsedDiff" v-if="showLoadingIcon"
class="diff-content loading" class="diff-content loading"
/> />
<div <div
v-if="showExpandMessage" v-else-if="showExpandMessage"
class="nothing-here-block diff-collapsed" class="nothing-here-block diff-collapsed"
> >
{{ __('This diff is collapsed.') }} {{ __('This diff is collapsed.') }}

View file

@ -13,6 +13,10 @@ export default {
Icon, Icon,
}, },
props: { props: {
line: {
type: Object,
required: true,
},
fileHash: { fileHash: {
type: String, type: String,
required: true, required: true,
@ -21,31 +25,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
lineType: {
type: String,
required: false,
default: '',
},
lineNumber: { lineNumber: {
type: Number, type: Number,
required: false, required: false,
default: 0, default: 0,
}, },
lineCode: {
type: String,
required: false,
default: '',
},
linePosition: { linePosition: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
metaData: {
type: Object,
required: false,
default: () => ({}),
},
showCommentButton: { showCommentButton: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -76,11 +65,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
@ -89,7 +73,7 @@ export default {
}), }),
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
lineHref() { lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#'; return `#${this.line.lineCode || ''}`;
}, },
shouldShowCommentButton() { shouldShowCommentButton() {
return ( return (
@ -103,20 +87,19 @@ export default {
); );
}, },
hasDiscussions() { hasDiscussions() {
return this.discussions.length > 0; return this.line.discussions && this.line.discussions.length > 0;
}, },
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) { if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
return false; return false;
} }
return this.showCommentButton && this.hasDiscussions; return this.showCommentButton && this.hasDiscussions;
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']), ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() { handleCommentButton() {
this.showCommentForm({ lineCode: this.lineCode }); this.showCommentForm({ lineCode: this.line.lineCode });
}, },
handleLoadMoreLines() { handleLoadMoreLines() {
if (this.isRequesting) { if (this.isRequesting) {
@ -125,8 +108,8 @@ export default {
this.isRequesting = true; this.isRequesting = true;
const endpoint = this.contextLinesPath; const endpoint = this.contextLinesPath;
const oldLineNumber = this.metaData.oldPos || 0; const oldLineNumber = this.line.metaData.oldPos || 0;
const newLineNumber = this.metaData.newPos || 0; const newLineNumber = this.line.metaData.newPos || 0;
const offset = newLineNumber - oldLineNumber; const offset = newLineNumber - oldLineNumber;
const bottom = this.isBottom; const bottom = this.isBottom;
const { fileHash } = this; const { fileHash } = this;
@ -201,7 +184,7 @@ export default {
</a> </a>
<diff-gutter-avatars <diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter" v-if="shouldShowAvatarsOnGutter"
:discussions="discussions" :discussions="line.discussions"
/> />
</template> </template>
</div> </div>

View file

@ -6,6 +6,7 @@ import noteForm from '../../notes/components/note_form.vue';
import { getNoteFormData } from '../store/utils'; import { getNoteFormData } from '../store/utils';
import autosave from '../../notes/mixins/autosave'; import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants'; import { DIFF_NOTE_TYPE } from '../constants';
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
export default { export default {
components: { components: {
@ -52,7 +53,7 @@ export default {
} }
}, },
methods: { methods: {
...mapActions('diffs', ['cancelCommentForm']), ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff']),
...mapActions(['saveNote', 'refetchDiscussionById']), ...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm(shouldConfirm, isDirty) { handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) { if (shouldConfirm && isDirty) {
@ -88,7 +89,10 @@ export default {
const endpoint = this.getNotesDataByProp('discussionsPath'); const endpoint = this.getNotesDataByProp('discussionsPath');
this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id }) this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
.then(() => { .then(selectedDiscussion => {
const lineCodeDiscussions = reduceDiscussionsToLineCodes([selectedDiscussion]);
this.assignDiscussionsToDiff(lineCodeDiscussions);
this.handleCancelCommentForm(); this.handleCancelCommentForm();
}) })
.catch(() => { .catch(() => {

View file

@ -11,8 +11,6 @@ import {
LINE_HOVER_CLASS_NAME, LINE_HOVER_CLASS_NAME,
LINE_UNFOLD_CLASS_NAME, LINE_UNFOLD_CLASS_NAME,
INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
} from '../constants'; } from '../constants';
export default { export default {
@ -67,42 +65,24 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
normalizedLine() {
let normalizedLine;
if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
normalizedLine = this.line;
} else if (this.linePosition === LINE_POSITION_LEFT) {
normalizedLine = this.line.left;
} else if (this.linePosition === LINE_POSITION_RIGHT) {
normalizedLine = this.line.right;
}
return normalizedLine;
},
isMatchLine() { isMatchLine() {
return this.normalizedLine.type === MATCH_LINE_TYPE; return this.line.type === MATCH_LINE_TYPE;
}, },
isContextLine() { isContextLine() {
return this.normalizedLine.type === CONTEXT_LINE_TYPE; return this.line.type === CONTEXT_LINE_TYPE;
}, },
isMetaLine() { isMetaLine() {
const { type } = this.normalizedLine; const { type } = this.line;
return ( return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
); );
}, },
classNameMap() { classNameMap() {
const { type } = this.normalizedLine; const { type } = this.line;
return { return {
[type]: type, [type]: type,
@ -116,9 +96,9 @@ export default {
}; };
}, },
lineNumber() { lineNumber() {
const { lineType, normalizedLine } = this; const { lineType } = this;
return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine; return lineType === OLD_LINE_TYPE ? this.line.oldLine : this.line.newLine;
}, },
}, },
}; };
@ -129,20 +109,17 @@ export default {
:class="classNameMap" :class="classNameMap"
> >
<diff-line-gutter-content <diff-line-gutter-content
:line="line"
:file-hash="fileHash" :file-hash="fileHash"
:context-lines-path="contextLinesPath" :context-lines-path="contextLinesPath"
:line-type="normalizedLine.type"
:line-code="normalizedLine.lineCode"
:line-position="linePosition" :line-position="linePosition"
:line-number="lineNumber" :line-number="lineNumber"
:meta-data="normalizedLine.metaData"
:show-comment-button="showCommentButton" :show-comment-button="showCommentButton"
:is-hover="isHover" :is-hover="isHover"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-match-line="isMatchLine" :is-match-line="isMatchLine"
:is-context-line="isContentLine" :is-context-line="isContentLine"
:is-meta-line="isMetaLine" :is-meta-line="isMetaLine"
:discussions="discussions"
/> />
</td> </td>
</template> </template>

View file

@ -21,18 +21,13 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
className() { className() {
return this.discussions.length ? '' : 'js-temp-notes-holder'; return this.line.discussions.length ? '' : 'js-temp-notes-holder';
}, },
}, },
}; };
@ -44,14 +39,13 @@ export default {
class="notes_holder" class="notes_holder"
> >
<td <td
class="notes_line" class="notes_content"
colspan="2" colspan="3"
></td> >
<td class="notes_content">
<div class="content"> <div class="content">
<diff-discussions <diff-discussions
v-if="discussions.length" v-if="line.discussions.length"
:discussions="discussions" :discussions="line.discussions"
/> />
<diff-line-note-form <diff-line-note-form
v-if="diffLineCommentForms[line.lineCode]" v-if="diffLineCommentForms[line.lineCode]"

View file

@ -1,5 +1,5 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import DiffTableCell from './diff_table_cell.vue'; import DiffTableCell from './diff_table_cell.vue';
import { import {
NEW_LINE_TYPE, NEW_LINE_TYPE,
@ -33,11 +33,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
@ -68,7 +63,11 @@ export default {
this.linePositionLeft = LINE_POSITION_LEFT; this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT; this.linePositionRight = LINE_POSITION_RIGHT;
}, },
mounted() {
this.scrollToLineIfNeededInline(this.line);
},
methods: { methods: {
...mapActions('diffs', ['scrollToLineIfNeededInline']),
handleMouseMove(e) { handleMouseMove(e) {
// To show the comment icon on the gutter we need to know if we hover the line. // To show the comment icon on the gutter we need to know if we hover the line.
// Current table structure doesn't allow us to do this with CSS in both of the diff view types // Current table structure doesn't allow us to do this with CSS in both of the diff view types
@ -94,7 +93,6 @@ export default {
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
:show-comment-button="true" :show-comment-button="true"
:discussions="discussions"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<diff-table-cell <diff-table-cell
@ -104,7 +102,6 @@ export default {
:line-type="newLineType" :line-type="newLineType"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
:discussions="discussions"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td

View file

@ -2,7 +2,6 @@
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue'; import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue'; import inlineDiffCommentRow from './inline_diff_comment_row.vue';
import { trimFirstCharOfLineContent } from '../store/utils';
export default { export default {
components: { components: {
@ -20,29 +19,17 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', [ ...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
'commitId',
'shouldRenderInlineCommentRow',
'singleDiscussionByLineCode',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
normalizedDiffLines() {
return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
},
diffLinesLength() { diffLinesLength() {
return this.normalizedDiffLines.length; return this.diffLines.length;
}, },
userColorScheme() { userColorScheme() {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {
discussionsList(line) {
return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
},
},
}; };
</script> </script>
@ -53,7 +40,7 @@ export default {
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"> class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
<tbody> <tbody>
<template <template
v-for="(line, index) in normalizedDiffLines" v-for="(line, index) in diffLines"
> >
<inline-diff-table-row <inline-diff-table-row
:file-hash="diffFile.fileHash" :file-hash="diffFile.fileHash"
@ -61,7 +48,6 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode" :key="line.lineCode"
:discussions="discussionsList(line)"
/> />
<inline-diff-comment-row <inline-diff-comment-row
v-if="shouldRenderInlineCommentRow(line)" v-if="shouldRenderInlineCommentRow(line)"
@ -69,7 +55,6 @@ export default {
:line="line" :line="line"
:line-index="index" :line-index="index"
:key="index" :key="index"
:discussions="discussionsList(line)"
/> />
</template> </template>
</tbody> </tbody>

View file

@ -21,51 +21,49 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
leftLineCode() { leftLineCode() {
return this.line.left.lineCode; return this.line.left && this.line.left.lineCode;
}, },
rightLineCode() { rightLineCode() {
return this.line.right.lineCode; return this.line.right && this.line.right.lineCode;
}, },
hasExpandedDiscussionOnLeft() { hasExpandedDiscussionOnLeft() {
const discussions = this.leftDiscussions; return this.line.left && this.line.left.discussions
? this.line.left.discussions.every(discussion => discussion.expanded)
return discussions ? discussions.every(discussion => discussion.expanded) : false; : false;
}, },
hasExpandedDiscussionOnRight() { hasExpandedDiscussionOnRight() {
const discussions = this.rightDiscussions; return this.line.right && this.line.right.discussions
? this.line.right.discussions.every(discussion => discussion.expanded)
return discussions ? discussions.every(discussion => discussion.expanded) : false; : false;
}, },
hasAnyExpandedDiscussion() { hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
}, },
shouldRenderDiscussionsOnLeft() { shouldRenderDiscussionsOnLeft() {
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft; return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
}, },
shouldRenderDiscussionsOnRight() { shouldRenderDiscussionsOnRight() {
return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type; return (
this.line.right &&
this.line.right.discussions &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
}, },
showRightSideCommentForm() { showRightSideCommentForm() {
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode]; return (
this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
);
}, },
className() { className() {
return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0 return (this.left && this.line.left.discussions.length > 0) ||
(this.right && this.line.right.discussions.length > 0)
? '' ? ''
: 'js-temp-notes-holder'; : 'js-temp-notes-holder';
}, },
@ -85,8 +83,8 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="leftDiscussions.length" v-if="line.left.discussions.length"
:discussions="leftDiscussions" :discussions="line.left.discussions"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
@ -104,8 +102,8 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="rightDiscussions.length" v-if="line.right.discussions.length"
:discussions="rightDiscussions" :discussions="line.right.discussions"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form

View file

@ -1,6 +1,6 @@
<script> <script>
import { mapActions } from 'vuex';
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters } from 'vuex';
import DiffTableCell from './diff_table_cell.vue'; import DiffTableCell from './diff_table_cell.vue';
import { import {
NEW_LINE_TYPE, NEW_LINE_TYPE,
@ -10,8 +10,7 @@ import {
OLD_NO_NEW_LINE_TYPE, OLD_NO_NEW_LINE_TYPE,
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE,
LINE_POSITION_LEFT, EMPTY_CELL_TYPE,
LINE_POSITION_RIGHT,
} from '../constants'; } from '../constants';
export default { export default {
@ -36,16 +35,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
@ -54,32 +43,33 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters('diffs', ['isParallelView']),
isContextLine() { isContextLine() {
return this.line.left.type === CONTEXT_LINE_TYPE; return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
}, },
classNameMap() { classNameMap() {
return { return {
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine, [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
[PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView, [PARALLEL_DIFF_VIEW_TYPE]: true,
}; };
}, },
parallelViewLeftLineType() { parallelViewLeftLineType() {
if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) { if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
return OLD_NO_NEW_LINE_TYPE; return OLD_NO_NEW_LINE_TYPE;
} }
return this.line.left.type; return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
}, },
}, },
created() { created() {
this.newLineType = NEW_LINE_TYPE; this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE; this.oldLineType = OLD_LINE_TYPE;
this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT;
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE; this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
}, },
mounted() {
this.scrollToLineIfNeededParallel(this.line);
},
methods: { methods: {
...mapActions('diffs', ['scrollToLineIfNeededParallel']),
handleMouseMove(e) { handleMouseMove(e) {
const isHover = e.type === 'mouseover'; const isHover = e.type === 'mouseover';
const hoveringCell = e.target.closest('td'); const hoveringCell = e.target.closest('td');
@ -116,47 +106,57 @@ export default {
@mouseover="handleMouseMove" @mouseover="handleMouseMove"
@mouseout="handleMouseMove" @mouseout="handleMouseMove"
> >
<diff-table-cell <template v-if="line.left">
:file-hash="fileHash" <diff-table-cell
:context-lines-path="contextLinesPath" :file-hash="fileHash"
:line="line" :context-lines-path="contextLinesPath"
:line-type="oldLineType" :line="line.left"
:line-position="linePositionLeft" :line-type="oldLineType"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isLeftHover" :is-hover="isLeftHover"
:show-comment-button="true" :show-comment-button="true"
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
:discussions="leftDiscussions" line-position="left"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<td <td
:id="line.left.lineCode" :id="line.left.lineCode"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content parallel left-side" class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown" @mousedown.native="handleParallelLineMouseDown"
v-html="line.left.richText" v-html="line.left.richText"
> >
</td> </td>
<diff-table-cell </template>
:file-hash="fileHash" <template v-else>
:context-lines-path="contextLinesPath" <td class="diff-line-num old_line empty-cell"></td>
:line="line" <td class="line_content parallel left-side empty-cell"></td>
:line-type="newLineType" </template>
:line-position="linePositionRight" <template v-if="line.right">
:is-bottom="isBottom" <diff-table-cell
:is-hover="isRightHover" :file-hash="fileHash"
:show-comment-button="true" :context-lines-path="contextLinesPath"
:diff-view-type="parallelDiffViewType" :line="line.right"
:discussions="rightDiscussions" :line-type="newLineType"
class="diff-line-num new_line" :is-bottom="isBottom"
/> :is-hover="isRightHover"
<td :show-comment-button="true"
:id="line.right.lineCode" :diff-view-type="parallelDiffViewType"
:class="line.right.type" line-position="right"
class="line_content parallel right-side" class="diff-line-num new_line"
@mousedown.native="handleParallelLineMouseDown" />
v-html="line.right.richText" <td
> :id="line.right.lineCode"
</td> :class="line.right.type"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
v-html="line.right.richText"
>
</td>
</template>
<template v-else>
<td class="diff-line-num old_line empty-cell"></td>
<td class="line_content parallel right-side empty-cell"></td>
</template>
</tr> </tr>
</template> </template>

View file

@ -2,8 +2,6 @@
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue'; import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue'; import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
import { EMPTY_CELL_TYPE } from '../constants';
import { trimFirstCharOfLineContent } from '../store/utils';
export default { export default {
components: { components: {
@ -21,46 +19,17 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', [ ...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
'commitId',
'singleDiscussionByLineCode',
'shouldRenderParallelCommentRow',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
parallelDiffLines() {
return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
if (line.left) {
parallelLine.left = trimFirstCharOfLineContent(line.left);
} else {
parallelLine.left = { type: EMPTY_CELL_TYPE };
}
if (line.right) {
parallelLine.right = trimFirstCharOfLineContent(line.right);
} else {
parallelLine.right = { type: EMPTY_CELL_TYPE };
}
return parallelLine;
});
},
diffLinesLength() { diffLinesLength() {
return this.parallelDiffLines.length; return this.diffLines.length;
}, },
userColorScheme() { userColorScheme() {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {
discussionsByLine(line, leftOrRight) {
return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
},
},
}; };
</script> </script>
@ -73,7 +42,7 @@ export default {
<table> <table>
<tbody> <tbody>
<template <template
v-for="(line, index) in parallelDiffLines" v-for="(line, index) in diffLines"
> >
<parallel-diff-table-row <parallel-diff-table-row
:file-hash="diffFile.fileHash" :file-hash="diffFile.fileHash"
@ -81,8 +50,6 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="index" :key="index"
:left-discussions="discussionsByLine(line, 'left')"
:right-discussions="discussionsByLine(line, 'right')"
/> />
<parallel-diff-comment-row <parallel-diff-comment-row
v-if="shouldRenderParallelCommentRow(line)" v-if="shouldRenderParallelCommentRow(line)"
@ -90,8 +57,6 @@ export default {
:line="line" :line="line"
:diff-file-hash="diffFile.fileHash" :diff-file-hash="diffFile.fileHash"
:line-index="index" :line-index="index"
:left-discussions="discussionsByLine(line, 'left')"
:right-discussions="discussionsByLine(line, 'right')"
/> />
</template> </template>
</tbody> </tbody>

View file

@ -25,3 +25,6 @@ export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
export const UNFOLD_COUNT = 20; export const UNFOLD_COUNT = 20;
export const COUNT_OF_AVATARS_IN_GUTTER = 3; export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17; export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;

View file

@ -2,7 +2,8 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import { getDiffPositionByLineCode } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
@ -29,6 +30,55 @@ export const fetchDiffFiles = ({ state, commit }) => {
.then(handleLocationHash); .then(handleLocationHash);
}; };
// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
Object.values(allLineDiscussions).forEach(discussions => {
if (discussions.length > 0) {
const { fileHash } = discussions[0];
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
fileHash,
discussions,
diffPositionByLineCode,
});
}
});
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
const { fileHash, line_code } = removeDiscussion;
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
};
export const startRenderDiffsQueue = ({ state, commit }) => {
const checkItem = () =>
new Promise(resolve => {
const nextFile = state.diffFiles.find(
file => !file.renderIt && (!file.collapsed || !file.text),
);
if (nextFile) {
requestAnimationFrame(() => {
commit(types.RENDER_FILE, nextFile);
});
requestIdleCallback(
() => {
checkItem()
.then(resolve)
.catch(() => {});
},
{ timeout: 1000 },
);
} else {
resolve();
}
});
return checkItem();
};
export const setInlineDiffViewType = ({ commit }) => { export const setInlineDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
@ -70,6 +120,25 @@ export const loadMoreLines = ({ commit }, options) => {
}); });
}; };
export const scrollToLineIfNeededInline = (_, line) => {
const hash = getLocationHash();
if (hash && line.lineCode === hash) {
handleLocationHash();
}
};
export const scrollToLineIfNeededParallel = (_, line) => {
const hash = getLocationHash();
if (
hash &&
((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash))
) {
handleLocationHash();
}
};
export const loadCollapsedDiff = ({ commit }, file) => export const loadCollapsedDiff = ({ commit }, file) =>
axios.get(file.loadCollapsedDiffUrl).then(res => { axios.get(file.loadCollapsedDiffUrl).then(res => {
commit(types.ADD_COLLAPSED_DIFFS, { commit(types.ADD_COLLAPSED_DIFFS, {

View file

@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit
export const diffHasAllExpandedDiscussions = (state, getters) => diff => { export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const discussions = getters.getDiffFileDiscussions(diff);
return (discussions.length && discussions.every(discussion => discussion.expanded)) || false; return (
(discussions && discussions.length && discussions.every(discussion => discussion.expanded)) ||
false
);
}; };
/** /**
@ -28,7 +31,10 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
export const diffHasAllCollpasedDiscussions = (state, getters) => diff => { export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const discussions = getters.getDiffFileDiscussions(diff);
return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false; return (
(discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) ||
false
);
}; };
/** /**
@ -40,7 +46,9 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const discussions = getters.getDiffFileDiscussions(diff);
return ( return (
(discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) || (discussions &&
discussions.length &&
discussions.find(discussion => discussion.expanded) !== undefined) ||
false false
); );
}; };
@ -64,45 +72,38 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash), discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
) || []; ) || [];
export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => { export const shouldRenderParallelCommentRow = state => line => {
if (!lineCode || lineCode === undefined) return []; const hasDiscussion =
const discussions = rootGetters.discussionsByLineCode; (line.left && line.left.discussions && line.left.discussions.length) ||
return discussions[lineCode] || []; (line.right && line.right.discussions && line.right.discussions.length);
};
export const shouldRenderParallelCommentRow = (state, getters) => line => { const hasExpandedDiscussionOnLeft =
const leftLineCode = line.left.lineCode; line.left && line.left.discussions && line.left.discussions.length
const rightLineCode = line.right.lineCode; ? line.left.discussions.every(discussion => discussion.expanded)
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode); : false;
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode); const hasExpandedDiscussionOnRight =
const hasDiscussion = leftDiscussions.length || rightDiscussions.length; line.right && line.right.discussions && line.right.discussions.length
? line.right.discussions.every(discussion => discussion.expanded)
const hasExpandedDiscussionOnLeft = leftDiscussions.length : false;
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions.length
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) { if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true; return true;
} }
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode]; const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode];
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode]; const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight; return hasCommentFormOnLeft || hasCommentFormOnRight;
}; };
export const shouldRenderInlineCommentRow = (state, getters) => line => { export const shouldRenderInlineCommentRow = state => line => {
if (state.diffLineCommentForms[line.lineCode]) return true; if (state.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode); if (!line.discussions || line.discussions.length === 0) {
if (lineDiscussions.length === 0) {
return false; return false;
} }
return lineDiscussions.every(discussion => discussion.expanded); return line.discussions.every(discussion => discussion.expanded);
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests

View file

@ -8,3 +8,6 @@ export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';

View file

@ -1,7 +1,13 @@
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils'; import {
findDiffFile,
addLineReferences,
removeMatchLine,
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
} from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
@ -15,8 +21,17 @@ export default {
}, },
[types.SET_DIFF_DATA](state, data) { [types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(diffData);
Object.assign(state, { Object.assign(state, {
...convertObjectPropsToCamelCase(data, { deep: true }), ...diffData,
});
},
[types.RENDER_FILE](state, file) {
Object.assign(file, {
renderIt: true,
}); });
}, },
@ -57,19 +72,93 @@ export default {
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) { [types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true }); const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(normalizedData);
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash); const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash);
if (newFileData) { Object.assign(selectedFile, { ...newFileData });
const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
state.diffFiles.splice(index, 1, newFileData);
}
}, },
[types.EXPAND_ALL_FILES](state) { [types.EXPAND_ALL_FILES](state) {
// eslint-disable-next-line no-param-reassign
state.diffFiles = state.diffFiles.map(file => ({ state.diffFiles = state.diffFiles.map(file => ({
...file, ...file,
collapsed: false, collapsed: false,
})); }));
}, },
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
const firstDiscussion = discussions[0];
const isDiffDiscussion = firstDiscussion.diff_discussion;
const hasLineCode = firstDiscussion.line_code;
const isResolvable = firstDiscussion.resolvable;
const diffPosition = diffPositionByLineCode[firstDiscussion.line_code];
if (
selectedFile &&
isDiffDiscussion &&
hasLineCode &&
isResolvable &&
diffPosition &&
isDiscussionApplicableToLine(firstDiscussion, diffPosition)
) {
const targetLine = selectedFile.parallelDiffLines.find(
line =>
(line.left && line.left.lineCode === firstDiscussion.line_code) ||
(line.right && line.right.lineCode === firstDiscussion.line_code),
);
if (targetLine) {
if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) {
Object.assign(targetLine.left, {
discussions,
});
} else {
Object.assign(targetLine.right, {
discussions,
});
}
}
if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find(
line => line.lineCode === firstDiscussion.line_code,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions,
});
}
}
}
},
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
if (selectedFile) {
const targetLine = selectedFile.parallelDiffLines.find(
line =>
(line.left && line.left.lineCode === lineCode) ||
(line.right && line.right.lineCode === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
discussions: [],
});
}
if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find(
line => line.lineCode === lineCode,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions: [],
});
}
}
}
},
}; };

View file

@ -8,6 +8,8 @@ import {
NEW_LINE_TYPE, NEW_LINE_TYPE,
OLD_LINE_TYPE, OLD_LINE_TYPE,
MATCH_LINE_TYPE, MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
MAX_LINES_TO_BE_RENDERED,
} from '../constants'; } from '../constants';
export function findDiffFile(files, hash) { export function findDiffFile(files, hash) {
@ -161,6 +163,11 @@ export function addContextLines(options) {
* @returns {Object} * @returns {Object}
*/ */
export function trimFirstCharOfLineContent(line = {}) { export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign
delete line.text;
// eslint-disable-next-line no-param-reassign
line.discussions = [];
const parsedLine = Object.assign({}, line); const parsedLine = Object.assign({}, line);
if (line.richText) { if (line.richText) {
@ -174,7 +181,44 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine; return parsedLine;
} }
export function getDiffRefsByLineCode(diffFiles) { // This prepares and optimizes the incoming diff data from the server
// by setting up incremental rendering and removing unneeded data
export function prepareDiffData(diffData) {
const filesLength = diffData.diffFiles.length;
let showingLines = 0;
for (let i = 0; i < filesLength; i += 1) {
const file = diffData.diffFiles[i];
if (file.parallelDiffLines) {
const linesLength = file.parallelDiffLines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.parallelDiffLines[u];
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
}
if (line.right) {
line.right = trimFirstCharOfLineContent(line.right);
}
}
}
if (file.highlightedDiffLines) {
const linesLength = file.highlightedDiffLines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.highlightedDiffLines[u];
Object.assign(line, { ...trimFirstCharOfLineContent(line) });
}
showingLines += file.parallelDiffLines.length;
}
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
});
}
}
export function getDiffPositionByLineCode(diffFiles) {
return diffFiles.reduce((acc, diffFile) => { return diffFiles.reduce((acc, diffFile) => {
const { baseSha, headSha, startSha } = diffFile.diffRefs; const { baseSha, headSha, startSha } = diffFile.diffRefs;
const { newPath, oldPath } = diffFile; const { newPath, oldPath } = diffFile;
@ -186,7 +230,7 @@ export function getDiffRefsByLineCode(diffFiles) {
const { lineCode, oldLine, newLine } = line; const { lineCode, oldLine, newLine } = line;
if (lineCode) { if (lineCode) {
acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine }; acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine, positionType: 'text' };
} }
}); });
} }
@ -194,3 +238,12 @@ export function getDiffRefsByLineCode(diffFiles) {
return acc; return acc;
}, {}); }, {});
} }
// This method will check whether the discussion is still applicable
// to the diff line in question regarding different versions of the MR
export function isDiscussionApplicableToLine(discussion, diffPosition) {
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPosition) || _.isEqual(originalRefs, diffPosition);
}

View file

@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Flash from '~/flash'; import Flash from '~/flash';
export default function gcpSignupOffer() { export default function initDismissableCallout(alertSelector) {
const alertEl = document.querySelector('.gcp-signup-offer'); const alertEl = document.querySelector(alertSelector);
if (!alertEl) { if (!alertEl) {
return; return;
} }

View file

@ -7,6 +7,19 @@ import axios from './lib/utils/axios_utils';
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
/**
* Return the error message string from the given response.
*
* @param {String|Object} res
*/
function getErrorMessage(res) {
if (!res || _.isString(res)) {
return res;
}
return res.message;
}
export default function dropzoneInput(form) { export default function dropzoneInput(form) {
const divHover = '<div class="div-dropzone-hover"></div>'; const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
@ -18,7 +31,7 @@ export default function dropzoneInput(form) {
const $uploadingErrorContainer = form.find('.uploading-error-container'); const $uploadingErrorContainer = form.find('.uploading-error-container');
const $uploadingErrorMessage = form.find('.uploading-error-message'); const $uploadingErrorMessage = form.find('.uploading-error-message');
const $uploadingProgressContainer = form.find('.uploading-progress-container'); const $uploadingProgressContainer = form.find('.uploading-progress-container');
const uploadsPath = window.uploads_path || null; const uploadsPath = form.data('uploads-path') || window.uploads_path || null;
const maxFileSize = gon.max_file_size || 10; const maxFileSize = gon.max_file_size || 10;
const formTextarea = form.find('.js-gfm-input'); const formTextarea = form.find('.js-gfm-input');
let handlePaste; let handlePaste;
@ -42,7 +55,7 @@ export default function dropzoneInput(form) {
if (!uploadsPath) { if (!uploadsPath) {
$formDropzone.addClass('js-invalid-dropzone'); $formDropzone.addClass('js-invalid-dropzone');
return; return null;
} }
const dropzone = $formDropzone.dropzone({ const dropzone = $formDropzone.dropzone({
@ -84,9 +97,7 @@ export default function dropzoneInput(form) {
// xhr object (xhr.responseText is error message). // xhr object (xhr.responseText is error message).
// On error we hide the 'Attach' and 'Cancel' buttons // On error we hide the 'Attach' and 'Cancel' buttons
// and show an error. // and show an error.
const message = getErrorMessage(errorMessage || xhr.responseText);
// If there's xhr error message, let's show it instead of dropzone's one.
const message = xhr ? xhr.responseText : errorMessage;
$uploadingErrorContainer.removeClass('hide'); $uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message); $uploadingErrorMessage.html(message);
@ -274,4 +285,6 @@ export default function dropzoneInput(form) {
$(this).closest('.gfm-form').find('.div-dropzone').click(); $(this).closest('.gfm-form').find('.div-dropzone').click();
formTextarea.focus(); formTextarea.focus();
}); });
return Dropzone.forElement($formDropzone.get(0));
} }

View file

@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) {
canvas.height = numTestEntries * fontSize; canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// Write each emoji to the canvas vertically // Write each emoji to the canvas vertically
let writeIndex = 0; let writeIndex = 0;
testMapKeys.forEach(testKey => { testMapKeys.forEach(testKey => {

View file

@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => { flashEl.addEventListener('transitionend', () => {
flashEl.remove(); flashEl.remove();
window.dispatchEvent(new Event('resize'));
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, { }, {
once: true, once: true,

View file

@ -65,8 +65,8 @@ export const hideMenu = (el) => {
const parentEl = el.parentNode; const parentEl = el.parentNode;
el.style.display = ''; // eslint-disable-line no-param-reassign el.style.display = '';
el.style.transform = ''; // eslint-disable-line no-param-reassign el.style.transform = '';
el.classList.remove(IS_ABOVE_CLASS); el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS); parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);

View file

@ -149,10 +149,16 @@ class GfmAutoComplete {
// Team Members // Team Members
$input.atwho({ $input.atwho({
at: '@', at: '@',
alias: 'users',
displayTpl(value) { displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template; let tmpl = GfmAutoComplete.Loading.template;
if (value.username != null) { const { avatarTag, username, title } = value;
tmpl = GfmAutoComplete.Members.template; if (username != null) {
tmpl = GfmAutoComplete.Members.templateFunction({
avatarTag,
username,
title,
});
} }
return tmpl; return tmpl;
}, },
@ -512,8 +518,9 @@ GfmAutoComplete.Emoji = {
}; };
// Team Members // Team Members
GfmAutoComplete.Members = { GfmAutoComplete.Members = {
// eslint-disable-next-line no-template-curly-in-string templateFunction({ avatarTag, username, title }) {
template: '<li>${avatarTag} ${username} <small>${title}</small></li>', return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small></li>`;
},
}; };
GfmAutoComplete.Labels = { GfmAutoComplete.Labels = {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string

View file

@ -2,14 +2,15 @@
/* global Flash */ /* global Flash */
import $ from 'jquery'; import $ from 'jquery';
import { s__ } from '~/locale'; import { s__, sprintf } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { COMMON_STR } from '../constants'; import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue'; import groupsComponent from './groups.vue';
export default { export default {
@ -19,6 +20,16 @@ export default {
groupsComponent, groupsComponent,
}, },
props: { props: {
action: {
type: String,
required: false,
default: '',
},
containerId: {
type: String,
required: false,
default: '',
},
store: { store: {
type: Object, type: Object,
required: true, required: true,
@ -56,31 +67,28 @@ export default {
? COMMON_STR.GROUP_SEARCH_EMPTY ? COMMON_STR.GROUP_SEARCH_EMPTY
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage); eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren); eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal); eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$on('updatePagination', this.updatePagination); eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups); eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
}, },
mounted() { mounted() {
this.fetchAllGroups(); this.fetchAllGroups();
if (this.containerId) {
this.containerEl = document.getElementById(this.containerId);
}
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage); eventHub.$off(`${this.action}fetchPage`, this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren); eventHub.$off(`${this.action}toggleChildren`, this.toggleChildren);
eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal); eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$off('updatePagination', this.updatePagination); eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups); eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
}, },
methods: { methods: {
fetchGroups({ fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
parentId,
page,
filterGroupsBy,
sortBy,
archived,
updatePagination,
}) {
return this.service return this.service
.getGroups(parentId, page, filterGroupsBy, sortBy, archived) .getGroups(parentId, page, filterGroupsBy, sortBy, archived)
.then(res => { .then(res => {
@ -165,13 +173,13 @@ export default {
} }
}, },
showLeaveGroupModal(group, parentGroup) { showLeaveGroupModal(group, parentGroup) {
const { fullName } = group;
this.targetGroup = group; this.targetGroup = group;
this.targetParentGroup = parentGroup; this.targetParentGroup = parentGroup;
this.showModal = true; this.showModal = true;
this.groupLeaveConfirmationMessage = s__( this.groupLeaveConfirmationMessage = sprintf(
`GroupsTree|Are you sure you want to leave the "${ s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
group.fullName { fullName },
}" group?`,
); );
}, },
hideLeaveGroupModal() { hideLeaveGroupModal() {
@ -197,16 +205,35 @@ export default {
this.targetGroup.isBeingRemoved = false; this.targetGroup.isBeingRemoved = false;
}); });
}, },
showEmptyState() {
const { containerEl } = this;
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
const emptyStateEl = containerEl.querySelector('.empty-state');
if (contentListEl) {
contentListEl.remove();
}
if (emptyStateEl) {
emptyStateEl.classList.remove(HIDDEN_CLASS);
}
},
updatePagination(headers) { updatePagination(headers) {
this.store.setPaginationInfo(headers); this.store.setPaginationInfo(headers);
}, },
updateGroups(groups, fromSearch) { updateGroups(groups, fromSearch) {
this.isSearchEmpty = groups ? groups.length === 0 : false; const hasGroups = groups && groups.length > 0;
this.isSearchEmpty = !hasGroups;
if (fromSearch) { if (fromSearch) {
this.store.setSearchedGroups(groups); this.store.setSearchedGroups(groups);
} else { } else {
this.store.setGroups(groups); this.store.setGroups(groups);
} }
if (this.action && !hasGroups && !fromSearch) {
this.showEmptyState();
}
}, },
}, },
}; };
@ -226,6 +253,7 @@ export default {
:search-empty="isSearchEmpty" :search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage" :search-empty-message="searchEmptyMessage"
:page-info="pageInfo" :page-info="pageInfo"
:action="action"
/> />
<deprecated-modal <deprecated-modal
v-show="showModal" v-show="showModal"

View file

@ -11,8 +11,12 @@ export default {
}, },
groups: { groups: {
type: Array, type: Array,
required: true,
},
action: {
type: String,
required: false, required: false,
default: () => ([]), default: '',
}, },
}, },
computed: { computed: {
@ -37,6 +41,7 @@ export default {
:key="index" :key="index"
:group="group" :group="group"
:parent-group="parentGroup" :parent-group="parentGroup"
:action="action"
/> />
<li <li
v-if="hasMoreChildren" v-if="hasMoreChildren"

View file

@ -30,6 +30,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
action: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
groupDomId() { groupDomId() {
@ -56,10 +61,12 @@ export default {
methods: { methods: {
onClickRowGroup(e) { onClickRowGroup(e) {
const NO_EXPAND_CLS = 'no-expand'; const NO_EXPAND_CLS = 'no-expand';
if (!(e.target.classList.contains(NO_EXPAND_CLS) || const targetClasses = e.target.classList;
e.target.parentElement.classList.contains(NO_EXPAND_CLS))) { const parentElClasses = e.target.parentElement.classList;
if (!(targetClasses.contains(NO_EXPAND_CLS) || parentElClasses.contains(NO_EXPAND_CLS))) {
if (this.hasChildren) { if (this.hasChildren) {
eventHub.$emit('toggleChildren', this.group); eventHub.$emit(`${this.action}toggleChildren`, this.group);
} else { } else {
visitUrl(this.group.relativePath); visitUrl(this.group.relativePath);
} }
@ -93,7 +100,7 @@ export default {
</div> </div>
<div <div
:class="{ 'content-loading': group.isChildrenLoading }" :class="{ 'content-loading': group.isChildrenLoading }"
class="avatar-container s24 d-none d-sm-block" class="avatar-container s24 d-none d-sm-flex"
> >
<a <a
:href="group.relativePath" :href="group.relativePath"
@ -158,6 +165,7 @@ export default {
v-if="group.isOpen && hasChildren" v-if="group.isOpen && hasChildren"
:parent-group="group" :parent-group="group"
:groups="group.children" :groups="group.children"
:action="action"
/> />
</li> </li>
</template> </template>

View file

@ -1,43 +1,48 @@
<script> <script>
import tablePagination from '~/vue_shared/components/table_pagination.vue'; import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
export default { export default {
components: { components: {
tablePagination, tablePagination,
},
props: {
groups: {
type: Array,
required: true,
}, },
props: { pageInfo: {
groups: { type: Object,
type: Array, required: true,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
searchEmpty: {
type: Boolean,
required: true,
},
searchEmptyMessage: {
type: String,
required: true,
},
}, },
methods: { searchEmpty: {
change(page) { type: Boolean,
const filterGroupsParam = getParameterByName('filter_groups'); required: true,
const sortParam = getParameterByName('sort');
const archivedParam = getParameterByName('archived');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
},
}, },
}; searchEmptyMessage: {
type: String,
required: true,
},
action: {
type: String,
required: false,
default: '',
},
},
methods: {
change(page) {
const filterGroupsParam = getParameterByName('filter_groups');
const sortParam = getParameterByName('sort');
const archivedParam = getParameterByName('archived');
eventHub.$emit(`${this.action}fetchPage`, page, filterGroupsParam, sortParam, archivedParam);
},
},
};
</script> </script>
<template> <template>
<div class="groups-list-tree-container"> <div class="groups-list-tree-container qa-groups-list-tree-container">
<div <div
v-if="searchEmpty" v-if="searchEmpty"
class="has-no-search-results" class="has-no-search-results"
@ -47,6 +52,7 @@
<group-folder <group-folder
v-if="!searchEmpty" v-if="!searchEmpty"
:groups="groups" :groups="groups"
:action="action"
/> />
<table-pagination <table-pagination
v-if="!searchEmpty" v-if="!searchEmpty"

View file

@ -21,6 +21,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
action: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
leaveBtnTitle() { leaveBtnTitle() {
@ -32,7 +37,7 @@ export default {
}, },
methods: { methods: {
onLeaveGroup() { onLeaveGroup() {
eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup); eventHub.$emit(`${this.action}showLeaveGroupModal`, this.group, this.parentGroup);
}, },
}, },
}; };

View file

@ -2,13 +2,23 @@ import { __, s__ } from '../locale';
export const MAX_CHILDREN_COUNT = 20; export const MAX_CHILDREN_COUNT = 20;
export const ACTIVE_TAB_SUBGROUPS_AND_PROJECTS = 'subgroups_and_projects';
export const ACTIVE_TAB_SHARED = 'shared';
export const ACTIVE_TAB_ARCHIVED = 'archived';
export const GROUPS_LIST_HOLDER_CLASS = '.js-groups-list-holder';
export const GROUPS_FILTER_FORM_CLASS = '.js-group-filter-form';
export const CONTENT_LIST_CLASS = '.content-list';
export const COMMON_STR = { export const COMMON_STR = {
FAILURE: __('An error occurred. Please try again.'), FAILURE: __('An error occurred. Please try again.'),
LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'), LEAVE_FORBIDDEN: s__(
'GroupsTree|Failed to leave the group. Please make sure you are not the only owner.',
),
LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'), LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'), EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'), GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'), GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
}; };
export const ITEM_TYPE = { export const ITEM_TYPE = {
@ -17,8 +27,12 @@ export const ITEM_TYPE = {
}; };
export const GROUP_VISIBILITY_TYPE = { export const GROUP_VISIBILITY_TYPE = {
public: __('Public - The group and any public projects can be viewed without any authentication.'), public: __(
internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'), 'Public - The group and any public projects can be viewed without any authentication.',
),
internal: __(
'Internal - The group and any internal projects can be viewed by any logged in user.',
),
private: __('Private - The group and its projects can only be viewed by members.'), private: __('Private - The group and its projects can only be viewed by members.'),
}; };

View file

@ -4,13 +4,23 @@ import eventHub from './event_hub';
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList { export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { constructor({
form,
filter,
holder,
filterEndpoint,
pagePath,
dropdownSel,
filterInputField,
action,
}) {
super(form, filter, holder, filterInputField); super(form, filter, holder, filterInputField);
this.form = form; this.form = form;
this.filterEndpoint = filterEndpoint; this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath; this.pagePath = pagePath;
this.filterInputField = filterInputField; this.filterInputField = filterInputField;
this.$dropdown = $(dropdownSel); this.$dropdown = $(dropdownSel);
this.action = action;
} }
getFilterEndpoint() { getFilterEndpoint() {
@ -20,15 +30,16 @@ export default class GroupFilterableList extends FilterableList {
getPagePath(queryData) { getPagePath(queryData) {
const params = queryData ? $.param(queryData) : ''; const params = queryData ? $.param(queryData) : '';
const queryString = params ? `?${params}` : ''; const queryString = params ? `?${params}` : '';
return `${this.pagePath}${queryString}`; const path = this.pagePath || window.location.pathname;
return `${path}${queryString}`;
} }
bindEvents() { bindEvents() {
super.bindEvents(); super.bindEvents();
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this); this.onFilterOptionClickWrapper = this.onOptionClick.bind(this);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper); this.$dropdown.on('click', 'a', this.onFilterOptionClickWrapper);
} }
onFilterInput() { onFilterInput() {
@ -53,7 +64,12 @@ export default class GroupFilterableList extends FilterableList {
} }
setDefaultFilterOption() { setDefaultFilterOption() {
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text()); const defaultOption = $.trim(
this.$dropdown
.find('.dropdown-menu li.js-filter-sort-order a')
.first()
.text(),
);
this.$dropdown.find('.dropdown-label').text(defaultOption); this.$dropdown.find('.dropdown-label').text(defaultOption);
} }
@ -65,11 +81,19 @@ export default class GroupFilterableList extends FilterableList {
// Get type of option selected from dropdown // Get type of option selected from dropdown
const currentTargetClassList = e.currentTarget.parentElement.classList; const currentTargetClassList = e.currentTarget.parentElement.classList;
const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order'); const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects'); const isOptionFilterByArchivedProjects = currentTargetClassList.contains(
'js-filter-archived-projects',
);
// Get option query param, also preserve currently applied query param // Get option query param, also preserve currently applied query param
const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href); const sortParam = getParameterByName(
const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href); 'sort',
isOptionFilterBySort ? e.currentTarget.href : window.location.href,
);
const archivedParam = getParameterByName(
'archived',
isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
);
if (sortParam) { if (sortParam) {
queryData.sort = sortParam; queryData.sort = sortParam;
@ -86,7 +110,9 @@ export default class GroupFilterableList extends FilterableList {
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text)); this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active'); this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
} else if (isOptionFilterByArchivedProjects) { } else if (isOptionFilterByArchivedProjects) {
this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active'); this.$dropdown
.find('.dropdown-menu li.js-filter-archived-projects a')
.removeClass('is-active');
} }
$(e.target).addClass('is-active'); $(e.target).addClass('is-active');
@ -98,11 +124,19 @@ export default class GroupFilterableList extends FilterableList {
onFilterSuccess(res, queryData) { onFilterSuccess(res, queryData) {
const currentPath = this.getPagePath(queryData); const currentPath = this.getPagePath(queryData);
window.history.replaceState({ window.history.replaceState(
page: currentPath, {
}, document.title, currentPath); page: currentPath,
},
document.title,
currentPath,
);
eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); eventHub.$emit(
eventHub.$emit('updatePagination', normalizeHeaders(res.headers)); `${this.action}updateGroups`,
res.data,
Object.prototype.hasOwnProperty.call(queryData, this.filterInputField),
);
eventHub.$emit(`${this.action}updatePagination`, normalizeHeaders(res.headers));
} }
} }

View file

@ -7,18 +7,26 @@ import GroupsService from './service/groups_service';
import groupsApp from './components/app.vue'; import groupsApp from './components/app.vue';
import groupFolderComponent from './components/group_folder.vue'; import groupFolderComponent from './components/group_folder.vue';
import groupItemComponent from './components/group_item.vue'; import groupItemComponent from './components/group_item.vue';
import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants';
Vue.use(Translate); Vue.use(Translate);
export default () => { export default (containerId = 'js-groups-tree', endpoint, action = '') => {
const el = document.getElementById('js-groups-tree'); const containerEl = document.getElementById(containerId);
let dataEl;
// Don't do anything if element doesn't exist (No groups) // Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL // This is for when the user enters directly to the page via URL
if (!el) { if (!containerEl) {
return; return;
} }
const el = action ? containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS) : containerEl;
if (action) {
dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
}
Vue.component('group-folder', groupFolderComponent); Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent); Vue.component('group-item', groupItemComponent);
@ -29,20 +37,26 @@ export default () => {
groupsApp, groupsApp,
}, },
data() { data() {
const { dataset } = this.$options.el; const { dataset } = dataEl || this.$options.el;
const hideProjects = dataset.hideProjects === 'true'; const hideProjects = dataset.hideProjects === 'true';
const service = new GroupsService(endpoint || dataset.endpoint);
const store = new GroupsStore(hideProjects); const store = new GroupsStore(hideProjects);
const service = new GroupsService(dataset.endpoint);
return { return {
action,
store, store,
service, service,
hideProjects, hideProjects,
loading: true, loading: true,
containerId,
}; };
}, },
beforeMount() { beforeMount() {
const { dataset } = this.$options.el; if (this.action) {
return;
}
const { dataset } = dataEl || this.$options.el;
let groupFilterList = null; let groupFilterList = null;
const form = document.querySelector(dataset.formSel); const form = document.querySelector(dataset.formSel);
const filter = document.querySelector(dataset.filterSel); const filter = document.querySelector(dataset.filterSel);
@ -52,10 +66,11 @@ export default () => {
form, form,
filter, filter,
holder, holder,
filterEndpoint: dataset.endpoint, filterEndpoint: endpoint || dataset.endpoint,
pagePath: dataset.path, pagePath: dataset.path,
dropdownSel: dataset.dropdownSel, dropdownSel: dataset.dropdownSel,
filterInputField: 'filter', filterInputField: 'filter',
action: this.action,
}; };
groupFilterList = new GroupFilterableList(opts); groupFilterList = new GroupFilterableList(opts);
@ -64,9 +79,11 @@ export default () => {
render(createElement) { render(createElement) {
return createElement('groups-app', { return createElement('groups-app', {
props: { props: {
action: this.action,
store: this.store, store: this.store,
service: this.service, service: this.service,
hideProjects: this.hideProjects, hideProjects: this.hideProjects,
containerId: this.containerId,
}, },
}); });
}, },

View file

@ -0,0 +1,78 @@
<script>
import $ from 'jquery';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
export default {
components: {
FileIcon,
ChangedFileIcon,
},
props: {
activeFile: {
type: Object,
required: true,
},
},
computed: {
activeButtonText() {
return this.activeFile.staged ? __('Unstage') : __('Stage');
},
isStaged() {
return !this.activeFile.changed && this.activeFile.staged;
},
},
methods: {
...mapActions(['stageChange', 'unstageChange']),
actionButtonClicked() {
if (this.activeFile.staged) {
this.unstageChange(this.activeFile.path);
} else {
this.stageChange(this.activeFile.path);
}
},
showDiscardModal() {
$(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show');
},
},
};
</script>
<template>
<div class="d-flex ide-commit-editor-header align-items-center">
<file-icon
:file-name="activeFile.name"
:size="16"
class="mr-2"
/>
<strong class="mr-2">
{{ activeFile.path }}
</strong>
<changed-file-icon
:file="activeFile"
/>
<div class="ml-auto">
<button
v-if="!isStaged"
type="button"
class="btn btn-remove btn-inverted append-right-8"
@click="showDiscardModal"
>
{{ __('Discard') }}
</button>
<button
:class="{
'btn-success': !isStaged,
'btn-warning': isStaged
}"
type="button"
class="btn btn-inverted"
@click="actionButtonClicked"
>
{{ activeButtonText }}
</button>
</div>
</div>
</template>

View file

@ -1,7 +1,9 @@
<script> <script>
import $ from 'jquery';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue'; import ListItem from './list_item.vue';
@ -9,6 +11,7 @@ export default {
components: { components: {
Icon, Icon,
ListItem, ListItem,
GlModal,
}, },
directives: { directives: {
tooltip, tooltip,
@ -56,6 +59,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
emptyStateText: {
type: String,
required: false,
default: __('No changes'),
},
}, },
computed: { computed: {
titleText() { titleText() {
@ -68,11 +76,19 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']), ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
actionBtnClicked() { actionBtnClicked() {
this[this.action](); this[this.action]();
$(this.$refs.actionBtn).tooltip('hide');
},
openDiscardModal() {
$('#discard-all-changes').modal('show');
}, },
}, },
discardModalText: __(
"You will loose all the unstaged changes you've made in this project. This action cannot be undone.",
),
}; };
</script> </script>
@ -81,27 +97,32 @@ export default {
class="ide-commit-list-container" class="ide-commit-list-container"
> >
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header d-flex mb-0"
> >
<div <div
class="multi-file-commit-panel-header-title" class="d-flex align-items-center flex-fill"
> >
<icon <icon
v-once v-once
:name="iconName" :name="iconName"
:size="18" :size="18"
class="append-right-8"
/> />
{{ titleText }} <strong>
{{ titleText }}
</strong>
<div class="d-flex ml-auto"> <div class="d-flex ml-auto">
<button <button
v-tooltip v-tooltip
v-show="filesLength" ref="actionBtn"
:class="{
'd-flex': filesLength
}"
:title="actionBtnText" :title="actionBtnText"
:aria-label="actionBtnText"
:disabled="!filesLength"
:class="{
'disabled-content': !filesLength
}"
type="button" type="button"
class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center" class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
data-placement="bottom" data-placement="bottom"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
@ -109,18 +130,32 @@ export default {
> >
<icon <icon
:name="actionBtnIcon" :name="actionBtnIcon"
:size="12" :size="16"
class="ml-auto mr-auto" class="ml-auto mr-auto"
/> />
</button> </button>
<span <button
v-tooltip
v-if="!stagedList"
:title="__('Discard all changes')"
:aria-label="__('Discard all changes')"
:disabled="!filesLength"
:class="{ :class="{
'rounded-right': !filesLength 'disabled-content': !filesLength
}" }"
class="ide-commit-file-count order-0 rounded-left text-center" type="button"
class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
data-placement="bottom"
data-container="body"
data-boundary="viewport"
@click="openDiscardModal"
> >
{{ filesLength }} <icon
</span> :size="16"
name="remove-all"
class="ml-auto mr-auto"
/>
</button>
</div> </div>
</div> </div>
</header> </header>
@ -143,9 +178,19 @@ export default {
</ul> </ul>
<p <p
v-else v-else
class="multi-file-commit-list form-text text-muted" class="multi-file-commit-list form-text text-muted text-center"
> >
{{ __('No changes') }} {{ emptyStateText }}
</p> </p>
<gl-modal
v-if="!stagedList"
id="discard-all-changes"
:footer-primary-button-text="__('Discard all changes')"
:header-title-text="__('Discard all unstaged changes?')"
footer-primary-button-variant="danger"
@submit="discardAllChanges"
>
{{ $options.discardModalText }}
</gl-modal>
</div> </div>
</template> </template>

View file

@ -2,6 +2,7 @@
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants'; import { viewerTypes } from '../../constants';
@ -12,6 +13,7 @@ export default {
Icon, Icon,
StageButton, StageButton,
UnstageButton, UnstageButton,
FileIcon,
}, },
directives: { directives: {
tooltip, tooltip,
@ -48,7 +50,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`; return `${getCommitIconMap(this.file).icon}${suffix}`;
}, },
iconClass() { iconClass() {
return `${getCommitIconMap(this.file).class} append-right-8`; return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
}, },
fullKey() { fullKey() {
return `${this.keyPrefix}-${this.file.key}`; return `${this.keyPrefix}-${this.file.key}`;
@ -105,17 +107,24 @@ export default {
@click="openFileInEditor" @click="openFileInEditor"
> >
<span class="multi-file-commit-list-file-path d-flex align-items-center"> <span class="multi-file-commit-list-file-path d-flex align-items-center">
<icon <file-icon
:name="iconName" :file-name="file.name"
:size="16" class="append-right-8"
:css-classes="iconClass"
/>{{ file.name }} />{{ file.name }}
</span> </span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>
</div>
<component
:is="actionComponent"
:path="file.path"
/>
</div>
</div> </div>
<component
:is="actionComponent"
:path="file.path"
class="d-flex position-absolute"
/>
</div> </div>
</template> </template>

View file

@ -1,11 +1,15 @@
<script> <script>
import $ from 'jquery';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import GlModal from '~/vue_shared/components/gl_modal.vue';
export default { export default {
components: { components: {
Icon, Icon,
GlModal,
}, },
directives: { directives: {
tooltip, tooltip,
@ -16,8 +20,22 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
modalId() {
return `discard-file-${this.path}`;
},
modalTitle() {
return sprintf(
__('Discard changes to %{path}?'),
{ path: this.path },
);
},
},
methods: { methods: {
...mapActions(['stageChange', 'discardFileChanges']), ...mapActions(['stageChange', 'discardFileChanges']),
showDiscardModal() {
$(document.getElementById(this.modalId)).modal('show');
},
}, },
}; };
</script> </script>
@ -25,51 +43,50 @@ export default {
<template> <template>
<div <div
v-once v-once
class="multi-file-discard-btn dropdown" class="multi-file-discard-btn d-flex"
> >
<button <button
v-tooltip v-tooltip
:aria-label="__('Stage changes')" :aria-label="__('Stage changes')"
:title="__('Stage changes')" :title="__('Stage changes')"
type="button" type="button"
class="btn btn-blank append-right-5 d-flex align-items-center" class="btn btn-blank align-items-center"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
@click.stop="stageChange(path)" @click.stop.prevent="stageChange(path)"
> >
<icon <icon
:size="12" :size="16"
name="mobile-issue-close" name="mobile-issue-close"
class="ml-auto mr-auto"
/> />
</button> </button>
<button <button
v-tooltip v-tooltip
:title="__('More actions')" :aria-label="__('Discard changes')"
:title="__('Discard changes')"
type="button" type="button"
class="btn btn-blank d-flex align-items-center" class="btn btn-blank align-items-center"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
data-toggle="dropdown" @click.stop.prevent="showDiscardModal"
data-display="static"
> >
<icon <icon
:size="12" :size="16"
name="ellipsis_h" name="remove"
class="ml-auto mr-auto"
/> />
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <gl-modal
<ul> :id="modalId"
<li> :header-title-text="modalTitle"
<button :footer-primary-button-text="__('Discard changes')"
type="button" footer-primary-button-variant="danger"
@click.stop="discardFileChanges(path)" @submit="discardFileChanges(path)"
> >
{{ __('Discard changes') }} {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
</button> </gl-modal>
</li>
</ul>
</div>
</div> </div>
</template> </template>

View file

@ -25,22 +25,23 @@ export default {
<template> <template>
<div <div
v-once v-once
class="multi-file-discard-btn" class="multi-file-discard-btn d-flex"
> >
<button <button
v-tooltip v-tooltip
:aria-label="__('Unstage changes')" :aria-label="__('Unstage changes')"
:title="__('Unstage changes')" :title="__('Unstage changes')"
type="button" type="button"
class="btn btn-blank d-flex align-items-center" class="btn btn-blank align-items-center"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
@click="unstageChange(path)" @click.stop.prevent="unstageChange(path)"
> >
<icon <icon
:size="12" :size="16"
name="history" name="redo"
class="ml-auto mr-auto"
/> />
</button> </button>
</div> </div>

View file

@ -0,0 +1,80 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Dropdown from './dropdown.vue';
export default {
components: {
Dropdown,
},
computed: {
...mapGetters(['activeFile']),
...mapGetters('fileTemplates', ['templateTypes']),
...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']),
showTemplatesDropdown() {
return Object.keys(this.selectedTemplateType).length > 0;
},
},
watch: {
activeFile: 'setInitialType',
},
mounted() {
this.setInitialType();
},
methods: {
...mapActions('fileTemplates', [
'setSelectedTemplateType',
'fetchTemplate',
'undoFileTemplate',
]),
setInitialType() {
const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name);
if (initialTemplateType) {
this.setSelectedTemplateType(initialTemplateType);
}
},
selectTemplateType(templateType) {
this.setSelectedTemplateType(templateType);
},
selectTemplate(template) {
this.fetchTemplate(template);
},
undo() {
this.undoFileTemplate();
},
},
};
</script>
<template>
<div class="d-flex align-items-center ide-file-templates">
<strong class="append-right-default">
{{ __('File templates') }}
</strong>
<dropdown
:data="templateTypes"
:label="selectedTemplateType.name || __('Choose a type...')"
class="mr-2"
@click="selectTemplateType"
/>
<dropdown
v-if="showTemplatesDropdown"
:label="__('Choose a template...')"
:is-async-data="true"
:searchable="true"
:title="__('File templates')"
class="mr-2"
@click="selectTemplate"
/>
<transition name="fade">
<button
v-show="updateSuccess"
type="button"
class="btn btn-default"
@click="undo"
>
{{ __('Undo') }}
</button>
</transition>
</div>
</template>

View file

@ -0,0 +1,125 @@
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
DropdownButton,
LoadingIcon,
},
props: {
data: {
type: Array,
required: false,
default: () => [],
},
label: {
type: String,
required: true,
},
title: {
type: String,
required: false,
default: null,
},
isAsyncData: {
type: Boolean,
required: false,
default: false,
},
searchable: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
search: '',
};
},
computed: {
...mapState('fileTemplates', ['templates', 'isLoading']),
outputData() {
return (this.isAsyncData ? this.templates : this.data).filter(t => {
if (!this.searchable) return true;
return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
});
},
showLoading() {
return this.isAsyncData ? this.isLoading : false;
},
},
mounted() {
$(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
},
beforeDestroy() {
$(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
},
methods: {
...mapActions('fileTemplates', ['fetchTemplateTypes']),
fetchTemplatesIfAsync() {
if (this.isAsyncData) {
this.fetchTemplateTypes();
}
},
clickItem(item) {
this.$emit('click', item);
},
},
};
</script>
<template>
<div class="dropdown">
<dropdown-button
:toggle-text="label"
data-display="static"
/>
<div class="dropdown-menu pb-0">
<div
v-if="title"
class="dropdown-title ml-0 mr-0"
>
{{ title }}
</div>
<div
v-if="!showLoading && searchable"
class="dropdown-input"
>
<input
v-model="search"
:placeholder="__('Filter...')"
type="search"
class="dropdown-input-field"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
</div>
<div class="dropdown-content">
<loading-icon
v-if="showLoading"
size="2"
/>
<ul v-else>
<li
v-for="(item, index) in outputData"
:key="index"
>
<button
type="button"
@click="clickItem(item)"
>
{{ item.name }}
</button>
</li>
</ul>
</div>
</div>
</div>
</template>

View file

@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue'; import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue'; import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue'; import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
const originalStopCallback = Mousetrap.stopCallback; const originalStopCallback = Mousetrap.stopCallback;
@ -23,6 +24,7 @@ export default {
FindFile, FindFile,
RightPane, RightPane,
ErrorMessage, ErrorMessage,
CommitEditorHeader,
}, },
computed: { computed: {
...mapState([ ...mapState([
@ -34,7 +36,7 @@ export default {
'currentProjectId', 'currentProjectId',
'errorMessage', 'errorMessage',
]), ]),
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']), ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
}, },
mounted() { mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e); window.onbeforeunload = e => this.onBeforeUnload(e);
@ -78,13 +80,13 @@ export default {
</script> </script>
<template> <template>
<article class="ide"> <article class="ide position-relative d-flex flex-column align-items-stretch">
<error-message <error-message
v-if="errorMessage" v-if="errorMessage"
:message="errorMessage" :message="errorMessage"
/> />
<div <div
class="ide-view" class="ide-view flex-grow d-flex"
> >
<find-file <find-file
v-show="fileFindVisible" v-show="fileFindVisible"
@ -96,7 +98,12 @@ export default {
<template <template
v-if="activeFile" v-if="activeFile"
> >
<commit-editor-header
v-if="isCommitModeActive"
:active-file="activeFile"
/>
<repo-tabs <repo-tabs
v-else
:active-file="activeFile" :active-file="activeFile"
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"

View file

@ -1,7 +1,11 @@
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
directives: {
tooltip,
},
components: { components: {
Icon, Icon,
}, },
@ -26,6 +30,11 @@ export default {
default: true, default: true,
}, },
}, },
computed: {
tooltipTitle() {
return this.showLabel ? '' : this.label;
},
},
methods: { methods: {
clicked() { clicked() {
this.$emit('click'); this.$emit('click');
@ -36,7 +45,9 @@ export default {
<template> <template>
<button <button
v-tooltip
:aria-label="label" :aria-label="label"
:title="tooltipTitle"
type="button" type="button"
class="btn-blank" class="btn-blank"
@click.stop.prevent="clicked" @click.stop.prevent="clicked"

View file

@ -1,6 +1,7 @@
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants'; import { modalTypes } from '../../constants';
@ -15,6 +16,7 @@ export default {
}, },
computed: { computed: {
...mapState(['entryModal']), ...mapState(['entryModal']),
...mapGetters('fileTemplates', ['templateTypes']),
entryName: { entryName: {
get() { get() {
if (this.entryModal.type === modalTypes.rename) { if (this.entryModal.type === modalTypes.rename) {
@ -31,7 +33,9 @@ export default {
if (this.entryModal.type === modalTypes.tree) { if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory'); return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) { } else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); return this.entryModal.entry.type === modalTypes.tree
? __('Rename folder')
: __('Rename file');
} }
return __('Create new file'); return __('Create new file');
@ -40,11 +44,16 @@ export default {
if (this.entryModal.type === modalTypes.tree) { if (this.entryModal.type === modalTypes.tree) {
return __('Create directory'); return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) { } else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); return this.entryModal.entry.type === modalTypes.tree
? __('Rename folder')
: __('Rename file');
} }
return __('Create file'); return __('Create file');
}, },
isCreatingNew() {
return this.entryModal.type !== modalTypes.rename;
},
}, },
methods: { methods: {
...mapActions(['createTempEntry', 'renameEntry']), ...mapActions(['createTempEntry', 'renameEntry']),
@ -61,6 +70,14 @@ export default {
}); });
} }
}, },
createFromTemplate(template) {
this.createTempEntry({
name: template.name,
type: this.entryModal.type,
});
$('#ide-new-entry').modal('toggle');
},
focusInput() { focusInput() {
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
}, },
@ -77,6 +94,7 @@ export default {
:header-title-text="modalTitle" :header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel" :footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success" footer-primary-button-variant="success"
modal-size="lg"
@submit="submitForm" @submit="submitForm"
@open="focusInput" @open="focusInput"
@closed="closedModal" @closed="closedModal"
@ -84,16 +102,35 @@ export default {
<div <div
class="form-group row" class="form-group row"
> >
<label class="label-bold col-form-label col-sm-3"> <label class="label-bold col-form-label col-sm-2">
{{ __('Name') }} {{ __('Name') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-10">
<input <input
ref="fieldName" ref="fieldName"
v-model="entryName" v-model="entryName"
type="text" type="text"
class="form-control" class="form-control"
placeholder="/dir/file_name"
/> />
<ul
v-if="isCreatingNew"
class="prepend-top-default list-inline"
>
<li
v-for="(template, index) in templateTypes"
:key="index"
class="list-inline-item"
>
<button
type="button"
class="btn btn-missing p-1 pr-2 pl-2"
@click="createFromTemplate(template)"
>
{{ template.name }}
</button>
</li>
</ul>
</div> </div>
</div> </div>
</gl-modal> </gl-modal>

View file

@ -24,12 +24,6 @@ export default {
default: null, default: null,
}, },
}, },
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: { methods: {
createFile(target, file, isText) { createFile(target, file, isText) {
const { name } = file; const { name } = file;
@ -85,6 +79,8 @@ export default {
ref="fileUpload" ref="fileUpload"
type="file" type="file"
class="hidden" class="hidden"
multiple
@change="openFile"
/> />
</div> </div>
</template> </template>

View file

@ -95,8 +95,9 @@ export default {
:file-list="changedFiles" :file-list="changedFiles"
:action-btn-text="__('Stage all changes')" :action-btn-text="__('Stage all changes')"
:active-file-key="activeFileKey" :active-file-key="activeFileKey"
:empty-state-text="__('There are no unstaged changes')"
action="stageAllChanges" action="stageAllChanges"
action-btn-icon="mobile-issue-close" action-btn-icon="stage-all"
item-action-component="stage-button" item-action-component="stage-button"
class="is-first" class="is-first"
icon-name="unstaged" icon-name="unstaged"
@ -108,8 +109,9 @@ export default {
:action-btn-text="__('Unstage all changes')" :action-btn-text="__('Unstage all changes')"
:staged-list="true" :staged-list="true"
:active-file-key="activeFileKey" :active-file-key="activeFileKey"
:empty-state-text="__('There are no staged changes')"
action="unstageAllChanges" action="unstageAllChanges"
action-btn-icon="history" action-btn-icon="unstage-all"
item-action-component="unstage-button" item-action-component="unstage-button"
icon-name="staged" icon-name="staged"
/> />

View file

@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants'; import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import ExternalLink from './external_link.vue'; import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue';
export default { export default {
components: { components: {
ContentViewer, ContentViewer,
DiffViewer, DiffViewer,
ExternalLink, ExternalLink,
FileTemplatesBar,
}, },
props: { props: {
file: { file: {
@ -34,6 +36,7 @@ export default {
'isCommitModeActive', 'isCommitModeActive',
'isReviewModeActive', 'isReviewModeActive',
]), ]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
@ -216,7 +219,7 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div class="ide-mode-tabs clearfix" > <div class="ide-mode-tabs clearfix">
<ul <ul
v-if="!shouldHideEditor && isEditModeActive" v-if="!shouldHideEditor && isEditModeActive"
class="nav-links float-left" class="nav-links float-left"
@ -249,6 +252,9 @@ export default {
:file="file" :file="file"
/> />
</div> </div>
<file-templates-bar
v-if="showFileTemplatesBar(file.name)"
/>
<div <div
v-show="!shouldHideEditor && file.viewMode ==='editor'" v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor" ref="editor"

View file

@ -95,16 +95,18 @@ export default {
return this.file.changed || this.file.tempFile || this.file.staged; return this.file.changed || this.file.tempFile || this.file.staged;
}, },
}, },
watch: {
'file.active': function fileActiveWatch(active) {
if (this.file.type === 'blob' && active) {
this.scrollIntoView();
}
},
},
mounted() { mounted() {
if (this.hasPathAtCurrentRoute()) { if (this.hasPathAtCurrentRoute()) {
this.scrollIntoView(true); this.scrollIntoView(true);
} }
}, },
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.scrollIntoView();
}
},
methods: { methods: {
...mapActions(['toggleTreeOpen']), ...mapActions(['toggleTreeOpen']),
clickFile() { clickFile() {

View file

@ -3,7 +3,6 @@ import VueRouter from 'vue-router';
import { join as joinPath } from 'path'; import { join as joinPath } from 'path';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter); Vue.use(VueRouter);
@ -74,98 +73,23 @@ router.beforeEach((to, from, next) => {
projectId: to.params.project, projectId: to.params.project,
}) })
.then(() => { .then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`; const basePath = to.params[0] || '';
const projectId = `${to.params.namespace}/${to.params.project}`;
const branchId = to.params.branchid; const branchId = to.params.branchid;
const mergeRequestId = to.params.mrid;
if (branchId) { if (branchId) {
const basePath = to.params[0] || ''; store.dispatch('openBranch', {
projectId,
store.dispatch('setCurrentBranchId', branchId);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId, branchId,
basePath,
});
} else if (mergeRequestId) {
store.dispatch('openMergeRequest', {
projectId,
mergeRequestId,
targetProjectId: to.query.target_project,
}); });
store
.dispatch('getFiles', {
projectId: fullProjectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
const treeEntry = store.state.entries[treeEntryKey];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
}
})
.catch(e => {
throw e;
});
} else if (to.params.mrid) {
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('updateActivityBarView', activityBarViews.review);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
return store.dispatch('getFiles', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
})
.then(() =>
store.dispatch('getMergeRequestVersions', {
projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid,
}),
)
.then(() =>
store.dispatch('getMergeRequestChanges', {
projectId: fullProjectId,
targetProjectId: to.query.target_project,
mergeRequestId: to.params.mrid,
}),
)
.then(mrChanges => {
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = store.state.entries[change.new_path];
if (changeTreeEntry) {
store.dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
store.dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash('Error while loading the merge request. Please try again.');
throw e;
});
} }
}) })
.catch(e => { .catch(e => {

View file

@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash'; import flash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker'; import FilesDecoratorWorker from './workers/files_decorator_worker';
import { stageKeys } from '../constants';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
@ -122,14 +123,28 @@ export const scrollToTab = () => {
}); });
}; };
export const stageAllChanges = ({ state, commit }) => { export const stageAllChanges = ({ state, commit, dispatch }) => {
const openFile = state.openFiles[0];
commit(types.SET_LAST_COMMIT_MSG, ''); commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
dispatch('openPendingTab', {
file: state.stagedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.staged,
});
}; };
export const unstageAllChanges = ({ state, commit }) => { export const unstageAllChanges = ({ state, commit, dispatch }) => {
const openFile = state.openFiles[0];
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path)); state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
dispatch('openPendingTab', {
file: state.changedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.unstaged,
});
}; };
export const updateViewer = ({ commit }, viewer) => { export const updateViewer = ({ commit }, viewer) => {
@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
const entry = state.entries[entryPath || path]; const entry = state.entries[entryPath || path];
commit(types.RENAME_ENTRY, { path, name, entryPath }); commit(types.RENAME_ENTRY, { path, name, entryPath });
if (entry.type === 'tree') { if (entry.type === 'tree') {
@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
); );
} }
if (!entryPath) { if (!entryPath && !entry.tempFile) {
dispatch('deleteEntry', path); dispatch('deleteEntry', path);
} }
}; };

View file

@ -5,7 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants'; import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const { path } = file; const { path } = file;
@ -54,9 +54,6 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_FILE_ACTIVE, { path, active: true }); commit(types.SET_FILE_ACTIVE, { path, active: true });
dispatch('scrollToTab'); dispatch('scrollToTab');
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
}; };
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
@ -211,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
}; };
export const stageChange = ({ commit, state }, path) => { export const stageChange = ({ commit, state, dispatch }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const openFile = state.openFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path); commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, ''); commit(types.SET_LAST_COMMIT_MSG, '');
@ -220,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => {
if (stagedFile) { if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
} }
if (openFile && openFile.active) {
const file = state.stagedFiles.find(f => f.path === path);
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.staged,
});
}
}; };
export const unstageChange = ({ commit }, path) => { export const unstageChange = ({ commit, dispatch, state }, path) => {
const openFile = state.openFiles.find(f => f.path === path);
commit(types.UNSTAGE_CHANGE, path); commit(types.UNSTAGE_CHANGE, path);
if (openFile && openFile.active) {
const file = state.changedFiles.find(f => f.path === path);
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.unstaged,
});
}
}; };
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix }); commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`); router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
return true; return true;

View file

@ -1,6 +1,8 @@
import { __ } from '../../../locale'; import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { activityBarViews } from '../../constants';
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, dispatch, state }, { commit, dispatch, state },
@ -104,3 +106,67 @@ export const getMergeRequestVersions = (
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions); resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
} }
}); });
export const openMergeRequest = (
{ dispatch, state },
{ projectId, targetProjectId, mergeRequestId } = {},
) =>
dispatch('getMergeRequestData', {
projectId,
targetProjectId,
mergeRequestId,
})
.then(mr => {
dispatch('setCurrentBranchId', mr.source_branch);
dispatch('getBranchData', {
projectId,
branchId: mr.source_branch,
});
return dispatch('getFiles', {
projectId,
branchId: mr.source_branch,
});
})
.then(() =>
dispatch('getMergeRequestVersions', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(() =>
dispatch('getMergeRequestChanges', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(mrChanges => {
if (mrChanges.changes.length) {
dispatch('updateActivityBarView', activityBarViews.review);
}
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = state.entries[change.new_path];
if (changeTreeEntry) {
dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash(__('Error while loading the merge request. Please try again.'));
throw e;
});

View file

@ -124,3 +124,35 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
actionPayload: branchId, actionPayload: branchId,
}); });
}; };
export const openBranch = (
{ dispatch, state },
{ projectId, branchId, basePath },
) => {
dispatch('setCurrentBranchId', branchId);
dispatch('getBranchData', {
projectId,
branchId,
});
return (
dispatch('getFiles', {
projectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
}
}
})
);
};

Some files were not shown because too many files have changed in this diff Show more