New upstream version 10.8.7+dfsg

This commit is contained in:
Pirate Praveen 2018-10-15 14:42:47 +05:30
parent 67db7fce51
commit 52cfd360f9
1721 changed files with 45512 additions and 20206 deletions

View file

@ -1,6 +1,9 @@
{
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
"karma": {
"plugins": ["rewire"]
},
"coverage": {
"plugins": [
[
@ -14,7 +17,8 @@
{
"process.env.BABEL_ENV": "coverage"
}
]
],
"rewire"
]
}
}

View file

@ -9,3 +9,4 @@ lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
lib/gitlab/ci/trace/chunked_io.rb

4
.gitignore vendored
View file

@ -64,11 +64,15 @@ eslint-report.html
/tags
/tmp/*
/vendor/bundle/*
/vendor/gitaly-ruby
/builds*
/shared/*
/.gitlab_workhorse_secret
/webpack-report/
/knapsack/
/rspec_flaky/
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
/plugins/*
/.gitlab_pages_secret

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
@ -6,10 +6,11 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
key: "ruby-2.3.6-with-yarn"
key: "ruby-2.3.7-debian-stretch-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
- vendor/gitaly-ruby
.push-cache: &push-cache
cache:
@ -110,7 +111,7 @@ stages:
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *except-docs
<<: *pull-cache
dependencies:
- setup-test-env
@ -122,6 +123,10 @@ stages:
variables:
SETUP_DB: "false"
.dedicated-no-docs-and-no-qa-pull-cache-job: &dedicated-no-docs-and-no-qa-pull-cache-job
<<: *dedicated-no-docs-pull-cache-job
<<: *except-docs-and-qa
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
@ -222,7 +227,7 @@ stages:
- master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
CREATE_DB_USER: "true"
@ -262,12 +267,12 @@ stages:
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate:reset
.migration-paths: &migration-paths
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
CREATE_DB_USER: "true"
script:
@ -289,7 +294,6 @@ stages:
# Trigger a package build in omnibus-gitlab repository
#
package-and-qa:
<<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
stage: build
@ -364,10 +368,11 @@ update-tests-metadata:
- rspec_flaky/
policy: push
script:
- retry gem install fog-aws mime-types
- retry gem install fog-aws mime-types activesupport
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
@ -434,6 +439,7 @@ setup-test-env:
paths:
- tmp/tests
- config/secrets.yml
- vendor/gitaly-ruby
rspec-pg 0 28: *rspec-metadata-pg
rspec-pg 1 28: *rspec-metadata-pg
@ -571,7 +577,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
key: "ruby-2.3.6-with-yarn-and-rubocop"
key: "ruby-2.3.7-debian-stretch-with-yarn-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
@ -647,7 +653,7 @@ migration:path-mysql:
<<: *use-mysql
.db-rollback: &db-rollback
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
@ -670,7 +676,7 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-no-docs-no-db-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
dependencies: []
variables:
NODE_ENV: "production"
@ -691,7 +697,7 @@ gitlab:assets:compile:
- webpack-report/
karma:
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
@ -720,7 +726,7 @@ codequality:
tags: []
before_script: []
services:
- docker:dind
- docker:stable-dind
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay2
@ -735,16 +741,50 @@ codequality:
expire_in: 1 week
sast:
<<: *except-docs
image: registry.gitlab.com/gitlab-org/gl-sast:latest
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
CONFIDENCE_LEVEL: 2
SAST_CONFIDENCE_LEVEL: 2
DOCKER_DRIVER: overlay2
allow_failure: true
tags: []
before_script: []
cache: {}
dependencies: []
services:
- docker:stable-dind
script:
- /app/bin/run .
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
--volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
paths: [gl-sast-report.json]
dependency_scanning:
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
tags: []
before_script: []
cache: {}
dependencies: []
services:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
--volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
artifacts:
paths: [gl-dependency-scanning-report.json]
qa:internal:
<<: *dedicated-no-docs-no-db-pull-cache-job
services: []
@ -781,7 +821,7 @@ coverage:
- coverage/assets/
lint:javascript:report:
<<: *dedicated-no-docs-no-db-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
stage: post-test
dependencies:
- compile-assets

View file

@ -0,0 +1,70 @@
<!--
# Read me first!
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
Set the title to: `[Security] Description of the original issue`
-->
### Prior to the security release
- [ ] Read the [security process for developers] if you are not familiar with it.
- [ ] Link to the original issue adding it to the [links section](#links)
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
- [ ] 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
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick 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 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
- [ ] 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
#### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary
#### Links
| Description | Link |
| -------- | -------- |
| Original issue | #TODO |
| Security release issue | #TODO |
| `master` MR | !TODO |
| `master` MR (EE) | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
#### Details
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
| Thanks | | |
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/
/label ~security

View file

@ -45,4 +45,4 @@ When removing columns, tables, indexes or other structures:
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-qa` manual pipeline job)
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)

View file

@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb'
- 'spec/support/seed_repo.rb'
- 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/helpers/seed_repo.rb'
# Offense count: 112
# Configuration parameters: Blacklist.
@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb'
- 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102
# Cop supports --auto-correct.

View file

@ -1 +1 @@
2.3.6
2.3.7

View file

@ -2,30 +2,58 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.7.7 (2018-07-17)
## 10.8.7 (2018-07-26)
### Security (1 change)
### Security (4 changes)
- Don't expose project names in various counters.
- Don't expose project names in GitHub counters.
- Adding CSRF protection to Hooks test action.
- Fixed XSS in branch name in Web IDE.
### Fixed (1 change)
- Escapes milestone and label's names on flash notice when promoting them.
## 10.8.6 (2018-07-17)
### Security (2 changes)
- Fix symlink vulnerability in project import.
- Merge branch 'fix-mr-widget-border' into 'master'.
## 10.7.6 (2018-06-21)
## 10.8.5 (2018-06-21)
### Security (6 changes)
### Security (5 changes)
- Fix XSS vulnerability for table of content generation.
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
- HTML escape branch name in project graphs page.
- HTML escape the name of the user in ProjectsHelper#link_to_member.
- Don't show events from internal projects for anonymous users in public feed.
- XSS fix to use safe_params instead of params in url_for helpers.
### Other (1 change)
- Replacing gollum libraries for gitlab custom libs. !18343
## 10.7.5 (2018-05-28)
## 10.8.4 (2018-06-06)
- No changes.
## 10.8.3 (2018-05-30)
### Fixed (4 changes)
- Replace Gitlab::REVISION with Gitlab.revision and handle installations without a .git directory. !19125
- Fix encoding of branch names on compare and new merge request page. !19143
- Fix remote mirror database inconsistencies when upgrading from EE to CE. !19196
- Fix local storage not being cleared after creating a new issue.
### Performance (1 change)
- Memoize Gitlab::Database.version.
## 10.8.2 (2018-05-28)
### Security (3 changes)
@ -34,11 +62,195 @@ entry.
- Fixed bug that allowed importing arbitrary project attributes.
## 10.7.4 (2018-05-21)
## 10.8.1 (2018-05-23)
### Fixed (1 change)
### Fixed (9 changes)
- Allow CommitStatus class to use presentable methods. !18979
- Fix corrupted environment pages with unathorized proxy url. !18989
- Fixes deploy token variables on Ci::Build. !19047
- Fix project mirror database inconsistencies when upgrading from EE to CE. !19109
- Render 404 when prometheus adapter is disabled in Prometheus metrics controller. !19110
- Fix error when deleting an empty list of refs.
- Fixed U2F login when used with LDAP.
- Bump prometheus-client-mmap to 0.9.3 to fix nil exception error.
- Fix system hook not firing for blocked users when LDAP sign-in is used.
## 10.8.0 (2018-05-22)
### Security (3 changes, 1 of them is from the community)
- Update faraday_middlewar to 0.12.2. !18397 (Takuya Noguchi)
- Serve archive requests with the correct file in all cases.
- Sanitizes user name to avoid XSS attacks.
### Fixed (47 changes, 11 of them are from the community)
- Refactor CSS to eliminate vertical misalignment of login nav. !16275 (Takuya Noguchi)
- Fix pipeline status in branch/tag tree page. !17995
- Allow group owner to enable runners from subgroups (#41981). !18009
- Fix template selector menu visibility when toggling preview mode in file edit view. !18118 (Fabian Schneider)
- Fix confirmation modal for deleting a protected branch. !18176 (Paul Bonaud @PaulRbR)
- Triggering custom hooks by Wiki UI edit. !18251
- Now `rake cache:clear` will also clear pipeline status cache. !18257
- Fix `joined` information on project members page. !18290 (Fabian Schneider)
- Fix missing namespace for some internal users. !18357
- Show shared projects on group page. !18390
- Restore label underline color. !18407 (George Tsiolis)
- Fix undefined `html_escape` method during markdown rendering. !18418
- Fix unassign slash command preview. !18447
- Correct text and functionality for delete user / delete user and contributions modal. !18463 (Marc Schwede)
- Fix discussions API setting created_at for notable in a group or notable in a project in a group with owners. !18464
- Don't include lfs_file_locks data in export bundle. !18495
- Reset milestone filter when clicking "Any Milestone" in dashboard. !18531
- Ensure member notifications are sent after the member actual creation/update in the DB. !18538
- Update links to /ci/lint with ones to project ci/lint. !18539 (Takuya Noguchi)
- Fix tabs container styles to make RSS button clickable. !18559
- Raise NoRepository error for non-valid repositories when calculating repository checksum. !18594
- Don't automatically remove artifacts for pages jobs after pages:deploy has run. !18628
- Increase new issue metadata form margin. !18630 (George Tsiolis)
- Add loading icon padding for pipeline environments. !18631 (George Tsiolis)
- ShaAttribute no longer stops startup if database is missing. !18726
- Fix close keyboard shortcuts dialog using the keyboard shortcut. !18783 (Lars Greiss)
- Fixes database inconsistencies between Community and Enterprise Edition on import state. !18811
- Add database foreign key constraint between pipelines and build. !18822
- Fix finding wiki pages when they have invalidly-encoded content. !18856
- Fix outdated Web IDE welcome copy. !18861
- fixed copy to blipboard button in embed bar of snippets. !18923 (haseebeqx)
- Disables RBAC on nginx-ingress. !18947
- Correct skewed Kubernetes popover illustration. !18949
- Resolve Import/Export ci_cd_settings error updating the project. !46049
- Fix project creation for user endpoint when jobs_enabled parameter supplied.
- 46210 Display logo and user dropdown on mobile for terms page and fix styling.
- Adds illustration for when job log was erased.
- Ensure web hook 'blocked URL' errors are stored in web hook logs and properly surfaced to the user.
- Make toggle markdown preview shortcut only toggle selected field.
- Verifiy if pipeline has commit idetails and render information in MR widget when branch is deleted.
- Fixed inconsistent protected branch pill baseline.
- Fix setting Gitlab metrics content types.
- Display only generic message on merge error to avoid exposing any potentially sensitive or user unfriendly backend messages.
- Fix label links update on project transfer.
- Breaks commit not found message in pipelines table.
- Adjust issue boards list header label text color.
- Prevent pipeline actions in dropdown to redirct to a new page.
### Changed (35 changes, 15 of them are from the community)
- Improve tooltips in collapsed right sidebar. !17714
- Partition job_queue_duration_seconds with jobs_running_for_project. !17730
- For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors). !17884 (Roger Rüttimann)
- Use RFC 3676 mail signature delimiters. !17979 (Enrico Scholz)
- Add sha filter to pipelines list API. !18125
- New CI Job live-trace architecture. !18169
- Make project deploy keys table more clearly structured. !18279
- Remove green background from unlock button in admin area. !18288
- Renamed Overview to Project in the contextual navigation at a project level. !18295 (Constance Okoghenun)
- Load branches on new merge request page asynchronously. !18315
- Create settings section for autodevops. !18321
- Add a comma to the time estimate system notes. !18326
- Enable specifying variables when executing a manual pipeline. !18440
- Fix size and position for fork icon. !18449 (George Tsiolis)
- Refactored activity calendar. !18469 (Enrico Scholz)
- Small improvements to repository checks. !18484
- Add 2FA filter to users API for admins only. !18503
- Align project avatar on small viewports. !18513 (George Tsiolis)
- Show group and project LFS settings in the interface to Owners and Masters. !18562
- Update environment item action buttons icons. !18632 (George Tsiolis)
- Update timeline icon for description edit. !18633 (George Tsiolis)
- Revert discussion counter height. !18656 (George Tsiolis)
- Improve quick actions summary preview. !18659 (George Tsiolis)
- Change font for tables inside diff discussions. !18660 (George Tsiolis)
- Add padding to profile description. !18663 (George Tsiolis)
- Break issue title for board card title and issuable header text. !18674 (George Tsiolis)
- Adds push mirrors to GitLab Community Edition. !18715
- Inform the user when there are no project import options available. !18716 (George Tsiolis)
- Improve commit message body rendering and fix responsive compare panels. !18725 (Constance Okoghenun)
- Reconcile project templates with Auto DevOps. !18737
- Remove branch name from the status bar of WebIDE.
- Clean up WebIDE status bar and add useful info.
- Improve interaction on WebIDE commit panel.
- Keep current labels visible when editing them in the sidebar.
- Use VueJS for rendering pipeline stages.
### Performance (26 changes, 11 of them are from the community)
- Move WorkInProgress vue component. !17536 (George Tsiolis)
- Move ReadyToMerge vue component. !17545 (George Tsiolis)
- Move BoardBlankState vue component. !17666 (George Tsiolis)
- Improve DB performance of calculating total artifacts size. !17839
- Add i18n and update specs for UnresolvedDiscussions vue component. !17866 (George Tsiolis)
- Introduce new ProjectCiCdSetting model with group_runners_enabled. !18144
- Move PipelineFailed vue component. !18277 (George Tsiolis)
- Move TimeTrackingEstimateOnlyPane vue component. !18318 (George Tsiolis)
- Move TimeTrackingHelpState vue component. !18319 (George Tsiolis)
- Reduce queries on merge requests list page for merge requests from forks. !18561
- Destroy build_chunks efficiently with FastDestroyAll module. !18575
- Improve performance of a service responsible for creating a pipeline. !18582
- Replace time_ago_in_words with JS-based one. !18607 (Takuya Noguchi)
- Move TimeTrackingNoTrackingPane vue component. !18676 (George Tsiolis)
- Move SidebarTimeTracking vue component. !18677 (George Tsiolis)
- Move TimeTrackingSpentOnlyPane vue component. !18710 (George Tsiolis)
- Detecting tags containing a commit uses Gitaly by default.
- Increase cluster applications installer availability using alpine linux mirrors.
- Compute notification recipients in background jobs.
- Use persisted diff data instead fetching Git on discussions.
- Detecting branchnames containing a commit uses Gitaly by default.
- Detect repository license on Gitaly by default.
- Finish NamespaceService migration to Gitaly.
- Check if a ref exists is done by Gitaly by default.
- Compute Gitlab::Git::Repository#checksum on Gitaly by default.
- Repository#exists? is always executed through Gitaly.
### Added (22 changes, 10 of them are from the community)
- Allow group masters to configure runners for groups. !9646 (Alexis Reigel)
- Adds Embedded Snippets Support. !15695 (haseebeqx)
- Add Copy metadata quick action. !16473 (Mateusz Bajorski)
- Show Runner's description on job's page. !17321
- Add deprecation message to dynamic milestone pages. !17505
- Show new branch/mr button even when branch exists. !17712 (Jacopo Beschi @jacopo-beschi)
- API: add languages of project GET /projects/:id/languages. !17770 (Roger Rüttimann)
- Display active sessions and allow the user to revoke any of it. !17867 (Alexis Reigel)
- Add cron job to email users on issue due date. !17985 (Stuart Nelson)
- Rubocop rule to avoid returning from a block. !18000 (Jacopo Beschi @jacopo-beschi)
- Add the signature verfication badge to the compare view. !18245 (Marc Shaw)
- Expose Deploy Token data as environment varialbes on CI/CD jobs. !18414
- Show group id in group settings. !18482 (George Tsiolis)
- Allow admins to enforce accepting Terms of Service on an instance. !18570
- Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables. !18672
- Add GCP signup offer to cluster index / create pages. !18684
- Output some useful information when running the rails console. !18697
- Display merge commit SHA in merge widget after merge. !18722
- git SHA is now displayed alongside the GitLab version on the Admin Dashboard.
- Expose the target commit ID through the tag API.
- Added fuzzy file finder to web IDE.
- Add discussion API for merge requests and commits.
### Other (22 changes, 8 of them are from the community)
- Replace the `project/issues/milestones.feature` spinach test with an rspec analog. !18300 (@blackst0ne)
- Replace the `project/commits/branches.feature` spinach test with an rspec analog. !18302 (@blackst0ne)
- Replacing gollum libraries for gitlab custom libs. !18343
- Replace the `project/commits/comments.feature` spinach test with an rspec analog. !18356 (@blackst0ne)
- Replace "Click" with "Select" to be more inclusive of people with accessibility requirements. !18386 (Mark Lapierre)
- Remove ahead/behind graphs on project branches on mobile. !18415 (Takuya Noguchi)
- Replace the `project/source/markdown_render.feature` spinach test with an rspec analog. !18525 (@blackst0ne)
- Add missing changelog type to docs. !18526 (@blackst0ne)
- Added Webhook SSRF prevention to documentation. !18532
- Upgrade underscore.js to 1.9.0. !18578
- Add documentation about how to use variables to define deploy policies for staging/production environments. !18675
- Replace the `project/builds/artifacts.feature` spinach test with an rspec analog. !18729 (@blackst0ne)
- Block access to the API & git for users that did not accept enforced Terms of Service. !18816
- Transition to atomic internal ids for all models. !44259
- Removes modal boards store and mixins from global scope.
- Replace GKE acronym with Google Kubernetes Engine.
- Replace vue resource with axios for pipelines details page.
- Enable prometheus monitoring by default.
- Replace vue resource with axios in pipelines table.
- Bump lograge to 0.10.0 and remove monkey patch.
- Improves wording in new pipeline page.
- Gitaly handles repository forks by default.
## 10.7.3 (2018-05-02)
@ -298,6 +510,31 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes.
## 10.6.5 (2018-04-24)
### Security (1 change)
- Sanitizes user name to avoid XSS attacks.
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
- Correct copy text for the promote milestone and label modals. !17726
- Avoid validation errors when running the Pages domain verification service. !17992
- Fix autolinking URLs containing ampersands. !18045
- Fix exceptions raised when migrating pipeline stages in the background. !18076
- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
- Don't show Jump to Discussion button on Issues.
- Fix listing commit branch/tags that contain special characters.
- Fix 404 in group boards when moving issue between lists.
### Performance (1 change)
- Free open file descriptors and libgit2 buffers in UpdatePagesService.
## 10.6.3 (2018-04-03)
### Security (2 changes)
@ -521,6 +758,13 @@ entry.
- Use host URL to build JIRA remote link icon.
## 10.5.8 (2018-04-24)
### Security (1 change)
- Sanitizes user name to avoid XSS attacks.
## 10.5.7 (2018-04-03)
### Security (2 changes)

View file

@ -9,6 +9,10 @@ terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
All Documentation content that resides under the [doc/ directory](/doc) of this
repository is licensed under Creative Commons:
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
_This notice should stay as the first item in the CONTRIBUTING.md file._
---
@ -25,10 +29,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
- [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
@ -125,8 +131,10 @@ 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", ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
- Milestone: ~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].
@ -167,13 +175,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
Subject labels are always all-lowercase.
### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)
### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
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 ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
@ -185,10 +193,10 @@ 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.
### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
Priority labels help us clearly communicate expectations of the work for the
release. There are two levels of priority labels:
Milestone labels help us clearly communicate expectations of the work for the
release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
@ -203,16 +211,46 @@ 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.
### Severity labels (~S1, ~S2, etc.)
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
Bug 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 | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------|
| ~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) | The issue is prominent but does not impact user workflow and a workaround is documented |
#### Specific Priority guidance
| Label | Availability / Performance |
|-------|--------------------------------------------------------------|
| ~P1 | |
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
| ~P3 | The issue is likely to occur in the near future |
| ~P4 | The issue _may_ occur but it's not likely |
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Example |
|-------|------------------------------------------|---------|
| ~S1 | Feature broken, no workaround | Unable to create an issue |
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
| Label | Meaning | Impact of the defect | 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. |
#### Specific Severity guidance
| Label | Security Impact |
|-------|-------------------------------------------------------------------|
| ~S1 | >50% customers impacted (possible company extinction level event) |
| ~S2 | Multiple customers impacted (but not apocalyptic) |
| ~S3 | A single customer impacted |
| ~S4 | No customer impact, or expected impact within 30 days |
### Label for community contributors (~"Accepting Merge Requests")
@ -693,4 +731,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[^1]: Please note that specs other than JavaScript specs are considered backend
code.

View file

@ -1 +1 @@
0.96.1
0.100.1

View file

@ -1 +1 @@
0.8.1
0.9.1

View file

@ -1 +1 @@
4.1.0
4.2.1

17
Gemfile
View file

@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.8'
@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-github', '~> 1.3'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
@ -81,7 +81,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gitlab-gollum-lib', '~> 4.2'
gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API
gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted?
@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
# User agent parsing
gem 'device_detector'
# Cache
gem 'redis-rails', '~> 5.0.2'
@ -282,7 +285,6 @@ gem 'batch-loader', '~> 1.2.1'
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
@ -295,7 +297,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.9.1'
gem 'prometheus-client-mmap', '~> 0.9.3'
gem 'raindrops', '~> 0.18'
end
@ -376,6 +378,7 @@ group :test do
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2'
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
gem 'sham_rack', '~> 1.3.6'
gem 'concurrent-ruby', '~> 1.0.5'
@ -413,7 +416,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.96.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed

View file

@ -143,7 +143,7 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.3)
crass (1.0.4)
creole (0.5.0)
css_parser (1.5.0)
addressable
@ -161,10 +161,11 @@ GEM
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.2.0)
device_detector (1.0.0)
devise (4.4.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1)
railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
devise-two-factor (3.0.0)
@ -206,7 +207,7 @@ GEM
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
faraday_middleware
@ -290,7 +291,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.96.0)
gitaly-proto (0.99.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@ -365,8 +366,8 @@ GEM
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
grape-entity (0.6.0)
activesupport
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-route-helpers (2.1.0)
activesupport
@ -483,10 +484,11 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
lograge (0.5.1)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -546,9 +548,9 @@ GEM
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-github (1.3.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
@ -586,7 +588,7 @@ GEM
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.1)
parser (2.5.0.5)
parser (2.5.1.0)
ast (~> 2.4.0)
parslet (1.5.0)
blankslate (~> 2.0)
@ -601,8 +603,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
@ -636,7 +636,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.9.1)
prometheus-client-mmap (0.9.3)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@ -648,7 +648,7 @@ GEM
pry (>= 0.9.10)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.9)
rack (1.6.10)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@ -696,7 +696,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.18.0)
rake (12.3.0)
rake (12.3.1)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@ -737,8 +737,9 @@ GEM
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.3.1)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
@ -812,7 +813,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.0)
rugged (0.27.1)
safe_yaml (1.0.4)
sanitize (4.6.5)
crass (~> 1.0.2)
@ -943,7 +944,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.3.0)
unicode-display_width (1.3.2)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@ -970,7 +971,7 @@ GEM
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
vmstat (2.3.0)
warden (1.2.6)
warden (1.2.7)
rack (>= 1.0)
webmock (2.3.2)
addressable (>= 2.3.6)
@ -1031,7 +1032,8 @@ DEPENDENCIES
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
devise (~> 4.2)
device_detector
devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
@ -1062,7 +1064,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.96.0)
gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@ -1075,7 +1077,7 @@ DEPENDENCIES
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.11.0)
@ -1116,7 +1118,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0)
@ -1129,14 +1131,13 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.1)
prometheus-client-mmap (~> 0.9.3)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)

View file

@ -69,7 +69,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.100)
attr_encrypted (3.0.3)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
@ -162,6 +162,7 @@ GEM
activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.1)
devise (4.4.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@ -291,9 +292,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.94.0)
gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@ -304,6 +305,17 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
@ -323,17 +335,6 @@ GEM
activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.2.7)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
@ -375,7 +376,7 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.10.0)
grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
@ -554,9 +555,6 @@ GEM
jwt (>= 1.5)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
@ -602,8 +600,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
@ -678,6 +674,10 @@ GEM
bundler (>= 1.3.0)
railties (= 5.0.6)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
actionview (~> 5.x, >= 5.0.1)
activesupport (~> 5.x)
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
rails-dom-testing (2.0.3)
@ -748,7 +748,7 @@ GEM
retriable (3.1.1)
rinku (2.0.4)
rotp (2.1.2)
rouge (2.2.1)
rouge (3.1.1)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7)
@ -874,7 +874,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-notifier (1.5.1)
spinach (0.10.1)
spinach (0.8.10)
colorize
gherkin-ruby (>= 0.3.2)
json
@ -1002,7 +1002,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@ -1031,6 +1031,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5)
device_detector
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
@ -1062,14 +1063,14 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0)
gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
@ -1078,7 +1079,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.10.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@ -1119,7 +1120,6 @@ DEPENDENCIES
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
@ -1130,7 +1130,6 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
@ -1145,6 +1144,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
rails (= 5.0.6)
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1)
rainbow (~> 2.2)
@ -1161,7 +1161,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)

27
LICENSE
View file

@ -1,25 +1,12 @@
Copyright (c) 2011-2017 GitLab B.V.
Copyright GitLab B.V.
With regard to the GitLab Software:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
For all third party components incorporated into the GitLab Software, those
components 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.

View file

@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
## Licensing
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.
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).

View file

@ -1 +1 @@
10.7.7
10.8.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

View file

@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
const {
name,
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
this.childNodes,
childNode => childNode.nodeType === 3,
);
const isEmojiUnicode =
this.childNodes &&
Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
const emojiSpriteLinkTag = document.createElement('link');
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
document.head.appendChild(emojiSpriteLinkTag);
gon.emoji_sprites_css_added = true;
}
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);

View file

@ -94,7 +94,7 @@ export default class FileTemplateMediator {
const hash = urlPieces[1];
if (hash === 'preview') {
this.hideTemplateSelectorMenu();
} else if (hash === 'editor') {
} else if (hash === 'editor' && !this.typeSelector.isHidden()) {
this.showTemplateSelectorMenu();
}
});

View file

@ -32,6 +32,10 @@ export default class FileTemplateSelector {
}
}
isHidden() {
return this.$wrapper.hasClass('hidden');
}
getToggleText() {
return this.$dropdownToggleText.text();
}

View file

@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
import boardBlankState from './board_blank_state';
import BoardBlankState from './board_blank_state.vue';
import './board_delete';
const Store = gl.issueBoards.BoardsStore;
@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
boardBlankState,
BoardBlankState,
},
props: {
list: Object,

View file

@ -1,42 +1,11 @@
<script>
/* global ListLabel */
import _ from 'underscore';
import Cookies from 'js-cookie';
const Store = gl.issueBoards.BoardsStore;
export default {
template: `
<div class="board-blank-state">
<p>
Add the following default lists to your Issue Board with one click:
</p>
<ul class="board-blank-state-list">
<li v-for="label in predefinedLabels">
<span
class="label-color"
:style="{ backgroundColor: label.color }">
</span>
{{ label.title }}
</li>
</ul>
<p>
Starting out with the default set of lists will get you right on the way to making the most of your board.
</p>
<button
class="btn btn-create btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists">
Add default lists
</button>
<button
class="btn btn-default btn-block"
type="button"
@click.stop="clearBlankState">
Nevermind, I'll use my own
</button>
</div>
`,
data() {
return {
predefinedLabels: [
@ -89,3 +58,41 @@ export default {
clearBlankState: Store.removeBlankState.bind(Store),
},
};
</script>
<template>
<div class="board-blank-state">
<p>
Add the following default lists to your Issue Board with one click:
</p>
<ul class="board-blank-state-list">
<li
v-for="(label, index) in predefinedLabels"
:key="index"
>
<span
class="label-color"
:style="{ backgroundColor: label.color }">
</span>
{{ label.title }}
</li>
</ul>
<p>
Starting out with the default set of lists will get you
right on the way to making the most of your board.
</p>
<button
class="btn btn-create btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists">
Add default lists
</button>
<button
class="btn btn-default btn-block"
type="button"
@click.stop="clearBlankState">
Nevermind, I'll use my own
</button>
</div>
</template>

View file

@ -1,9 +1,9 @@
import Vue from 'vue';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalEmptyState = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
mixins: [modalMixin],
data() {
return ModalStore.store;
},

View file

@ -3,11 +3,11 @@ import Flash from '../../../flash';
import { __ } from '../../../locale';
import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalFooter = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,

View file

@ -1,11 +1,11 @@
import Vue from 'vue';
import modalFilters from './filters';
import './tabs';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
mixins: [modalMixin],
props: {
projectId: {
type: Number,

View file

@ -7,8 +7,7 @@ import './header';
import './list';
import './footer';
import './empty_state';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
gl.issueBoards.IssuesModal = Vue.extend({
props: {

View file

@ -2,8 +2,7 @@
import Vue from 'vue';
import bp from '../../../breakpoints';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalList = Vue.extend({
props: {

View file

@ -1,6 +1,5 @@
import Vue from 'vue';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() {

View file

@ -1,9 +1,9 @@
import Vue from 'vue';
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalTabs = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
mixins: [modalMixin],
data() {
return ModalStore.store;
},

View file

@ -17,9 +17,9 @@ import './models/milestone';
import './models/project';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
import './mixins/modal_mixins';
import modalMixin from './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
import './components/board';
@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
export default () => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {};
@ -176,7 +175,7 @@ export default () => {
gl.IssueBoardsModalAddBtn = new Vue({
el: document.getElementById('js-add-issues-btn'),
mixins: [gl.issueBoards.ModalMixins],
mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,

View file

@ -1,6 +1,6 @@
const ModalStore = gl.issueBoards.ModalStore;
import ModalStore from '../stores/modal_store';
gl.issueBoards.ModalMixins = {
export default {
methods: {
toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle;

View file

@ -1,6 +1,3 @@
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
class ModalStore {
constructor() {
this.store = {
@ -95,4 +92,4 @@ class ModalStore {
}
}
gl.issueBoards.ModalStore = new ModalStore();
export default new ModalStore();

View file

@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
setDisableDeleteButton(e) {
if (this.$deleteBtn.is('[disabled]')) {
e.preventDefault();
e.stopPropagation();
return false;
}
return true;
}
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');

View file

@ -1,20 +1,24 @@
import Flash from '../flash';
import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import createFlash from '~/flash';
import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer();
// The empty state won't have a clusterList
if (clusterList) {
setupToggleButtons(
document.querySelector('.js-clusters-list'),
(value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
.catch((err) => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
throw err;
}),
setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
err => {
createFlash(__('Something went wrong on our end.'));
throw err;
},
),
);
}
};

View file

@ -1,96 +1,102 @@
<script>
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import {
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { APPLICATION_INSTALLED, INGRESS } from '../constants';
export default {
components: {
applicationRow,
clipboardButton,
export default {
components: {
applicationRow,
clipboardButton,
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}">
),
),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
);
},
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(s__(
},
false,
);
},
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
the hosting provider your Kubernetes cluster is installed on. If you are using
Google Kubernetes Engine, you can %{pricingLink}.`,
),
),
{
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
},
false,
);
},
false,
);
const externalIpParagraph = sprintf(
_.escape(s__(
const externalIpParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
),
),
{
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
},
false,
);
return `
return `
<p>
${extraCostParagraph}
</p>
@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
},
prometheusDescription() {
return sprintf(
_.escape(s__(
},
prometheusDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
)), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
),
),
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
);
},
},
false,
);
},
};
},
};
</script>
<template>
@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"

View file

@ -0,0 +1,27 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function gcpSignupOffer() {
const alertEl = document.querySelector('.gcp-signup-offer');
if (!alertEl) {
return;
}
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
closeButtonEl.addEventListener('click', () => {
axios
.post(dismissEndpoint, {
feature_name: featureId,
})
.then(() => {
$(alertEl).alert('close');
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
});
}

View file

@ -55,22 +55,20 @@
},
methods: {
successCallback(resp) {
return resp.json().then((response) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.setCommonData(pipelines);
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data;
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: {
pipelines: response,
},
});
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: {
pipelines: resp.data,
},
});
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
},
};

View file

@ -1,86 +0,0 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
import $ from 'jquery';
import { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare {
constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
$('.js-compare-dropdown').each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
selectable: true,
fieldName: $dropdown.data('fieldName'),
filterable: true,
id: function(obj, $el) {
return $el.data('id');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
},
clicked: function(e, el) {
if ($dropdown.is('.js-target-branch')) {
return _this.getTargetHtml();
} else if ($dropdown.is('.js-source-branch')) {
return _this.getSourceHtml();
} else if ($dropdown.is('.js-target-project')) {
return _this.getTargetProject();
}
}
});
};
})(this));
this.initialState();
}
initialState() {
this.getSourceHtml();
this.getTargetHtml();
}
getTargetProject() {
$('.mr_target_commit').empty();
return axios.get(this.opts.targetProjectUrl, {
params: {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
},
}).then(({ data }) => {
$('.js-target-branch-dropdown .dropdown-content').html(data);
});
}
getSourceHtml() {
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
}
getTargetHtml() {
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val()
});
}
static sendAjax(url, loading, target, params) {
const $target = $(target);
loading.show();
$target.empty();
return axios.get(url, {
params,
}).then(({ data }) => {
loading.hide();
$target.html(data);
const className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
});
}
}

View file

@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete() {
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
axios.get($dropdown.data('refsUrl'), {
params: {
ref: $dropdown.data('ref'),
search: term,
},
}).then(({ data }) => {
callback(data);
}).catch(() => flash(__('Error fetching refs')));
const params = {
ref: $dropdown.data('ref'),
search: term,
};
if (limitTo) {
params.find = limitTo;
}
axios
.get($dropdown.data('refsUrl'), {
params,
})
.then(({ data }) => {
if (limitTo) {
callback(data[capitalizeFirstCharacter(limitTo)] || []);
} else {
callback(data);
}
})
.catch(() => flash(__('Error fetching refs')));
},
selectable: true,
filterable: true,
@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) {
var link;
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
} else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
link = $('<a />')
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
.attr('data-ref', ref);
return $('<li />').append(link);
}
},
@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
}
},
clicked: () => clickHandler($dropdown),
});
$filterInput.on('keyup', (e) => {
$filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open');
});
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
$dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');

View file

@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
} else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
Flash('Failed to check if a new branch can be created.');
Flash(__('Failed to check related branches.'));
});
}
@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide');
}
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
}

View file

@ -1,55 +1,50 @@
<script>
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../eventhub';
export default {
components: {
loadingIcon,
export default {
components: {
loadingIcon,
},
props: {
deployKey: {
type: Object,
required: true,
},
props: {
deployKey: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
type: {
type: String,
required: true,
},
data() {
return {
isLoading: false,
};
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
methods: {
doAction() {
this.isLoading = true;
},
data() {
return {
isLoading: false,
};
},
methods: {
doAction() {
this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
},
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
},
};
},
};
</script>
<template>
<button
class="btn btn-sm prepend-left-10"
class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
{{ text }}
<slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"

View file

@ -1,80 +1,115 @@
<script>
import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import keysPanel from './keys_panel.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { s__ } from '~/locale';
import Flash from '~/flash';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
export default {
components: {
keysPanel,
loadingIcon,
export default {
components: {
KeysPanel,
LoadingIcon,
NavigationTabs,
},
props: {
endpoint: {
type: String,
required: true,
},
props: {
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
},
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
computed: {
hasKeys() {
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
},
created() {
this.service = new DeployKeysService(this.endpoint);
},
data() {
return {
currentTab: 'enabled_keys',
isLoading: false,
store: new DeployKeysStore(),
};
},
scopes: {
enabled_keys: s__('DeployKeys|Enabled deploy keys'),
available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
},
computed: {
tabs() {
return Object.keys(this.$options.scopes).map(scope => {
const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
return {
name: this.$options.scopes[scope],
scope,
isActive: scope === this.currentTab,
count,
};
});
},
mounted() {
this.fetchKeys();
hasKeys() {
return Object.keys(this.keys).length;
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
keys() {
return this.store.keys;
},
methods: {
fetchKeys() {
this.isLoading = true;
},
created() {
this.service = new DeployKeysService(this.endpoint);
this.service.getKeys()
.then((data) => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => new Flash('Error getting deploy keys'));
},
enableKey(deployKey) {
this.service.enableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys())
.then(callback)
.catch(() => new Flash('Error removing deploy key'));
} else {
callback();
}
},
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
methods: {
onChangeTab(tab) {
this.currentTab = tab;
},
};
fetchKeys() {
this.isLoading = true;
return this.service
.getKeys()
.then(data => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => {
this.isLoading = false;
this.store.keys = {};
return new Flash(s__('DeployKeys|Error getting deploy keys'));
});
},
enableKey(deployKey) {
this.service
.enableKey(deployKey.id)
.then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
.then(callback)
.catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
} else {
callback();
}
},
},
};
</script>
<template>
@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
label="Loading deploy keys"
:label="s__('DeployKeys|Loading deploy keys')"
/>
<div v-else-if="hasKeys">
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<div class="fade-left">
<i
class="fa fa-angle-left"
aria-hidden="true"
>
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
aria-hidden="true"
>
</i>
</div>
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="deployKeys"
/>
</div>
<keys-panel
title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
:keys="keys.enabled_keys"
:project-id="projectId"
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store"
:endpoint="endpoint"
/>
</div>
</template>
</div>
</template>

View file

@ -1,111 +1,235 @@
<script>
import actionBtn from './action_btn.vue';
import { getTimeago } from '../../lib/utils/datetime_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
actionBtn,
import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
icon,
},
directives: {
tooltip,
},
mixins: [timeagoMixin],
props: {
deployKey: {
type: Object,
required: true,
},
directives: {
tooltip,
store: {
type: Object,
required: true,
},
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
endpoint: {
type: String,
required: true,
},
computed: {
timeagoDate() {
return getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
projectId: {
type: String,
required: false,
default: null,
},
methods: {
isEnabled(id) {
return this.store.findEnabledKey(id) !== undefined;
},
tooltipTitle(project) {
return project.can_push ? 'Write access allowed' : 'Read access only';
},
},
data() {
return {
projectsExpanded: false,
};
},
computed: {
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
};
projects() {
const projects = [...this.deployKey.deploy_keys_projects];
if (this.projectId !== null) {
const indexOfCurrentProject = _.findIndex(
projects,
project =>
project &&
project.project &&
project.project.id &&
project.project.id.toString() === this.projectId,
);
if (indexOfCurrentProject > -1) {
const currentProject = projects.splice(indexOfCurrentProject, 1);
currentProject[0].project.full_name = s__('DeployKeys|Current project');
return currentProject.concat(projects);
}
}
return projects;
},
firstProject() {
return _.head(this.projects);
},
restProjects() {
return _.tail(this.projects);
},
restProjectsTooltip() {
return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
count: this.restProjects.length,
});
},
restProjectsLabel() {
return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
},
isEnabled() {
return this.store.isEnabled(this.deployKey.id);
},
isRemovable() {
return (
this.store.isEnabled(this.deployKey.id) &&
this.deployKey.destroyed_when_orphaned &&
this.deployKey.almost_orphaned
);
},
isExpandable() {
return !this.projectsExpanded && this.restProjects.length > 1;
},
isExpanded() {
return this.projectsExpanded || this.restProjects.length === 1;
},
},
methods: {
projectTooltipTitle(project) {
return project.can_push
? s__('DeployKeys|Write access allowed')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
this.projectsExpanded = !this.projectsExpanded;
},
},
};
</script>
<template>
<div>
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon"
>
</i>
</div>
<div class="deploy-key-content key-list-item-info">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="description qa-key-fingerprint">
{{ deployKey.fingerprint }}
<div class="gl-responsive-table-row deploy-key">
<div class="table-section section-40">
<div
role="rowheader"
class="table-mobile-header">
{{ s__('DeployKeys|Deploy key') }}
</div>
<div class="table-mobile-content">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="fingerprint qa-key-fingerprint">
{{ deployKey.fingerprint }}
</div>
</div>
</div>
<div class="deploy-key-content prepend-left-default deploy-key-projects">
<a
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
:key="i"
class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="tooltipTitle(deployKeysProject)"
v-tooltip
>
{{ deployKeysProject.project.full_name }}
<i
v-if="!deployKeysProject.can_push"
aria-hidden="true"
class="fa fa-lock"
>
</i>
</a>
<div class="table-section section-30 section-wrap">
<div
role="rowheader"
class="table-mobile-header">
{{ s__('DeployKeys|Project usage') }}
</div>
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
class="label deploy-project-label"
:title="projectTooltipTitle(firstProject)"
v-tooltip
>
<span>
{{ firstProject.project.full_name }}
</span>
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a
v-if="isExpandable"
class="label deploy-project-label"
@click="toggleExpanded"
:title="restProjectsTooltip"
v-tooltip
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
v-else-if="isExpanded"
v-for="deployKeysProject in restProjects"
:key="deployKeysProject.project.full_path"
class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
v-tooltip
>
<span>
{{ deployKeysProject.project.full_name }}
</span>
<icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
</a>
</template>
<span
v-else
class="text-secondary">{{ __('None') }}</span>
</div>
</div>
<div class="deploy-key-content">
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-sm"
:href="editDeployKeyPath"
>
Edit
</a>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"
/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove"
/>
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable"
/>
<div class="table-section section-15 text-right">
<div
role="rowheader"
class="table-mobile-header">
{{ __('Created') }}
</div>
<div class="table-mobile-content text-secondary key-created-at">
<span
:title="tooltipTitle(deployKey.created_at)"
v-tooltip>
<icon name="calendar"/>
<span>{{ timeFormated(deployKey.created_at) }}</span>
</span>
</div>
</div>
<div class="table-section section-15 table-button-footer deploy-key-actions">
<div class="btn-group table-action-buttons">
<action-btn
v-if="!isEnabled"
:deploy-key="deployKey"
type="enable"
>
{{ __('Enable') }}
</action-btn>
<a
v-if="deployKey.can_edit"
class="btn btn-default text-secondary"
:href="editDeployKeyPath"
:title="__('Edit')"
data-container="body"
v-tooltip
>
<icon name="pencil"/>
</a>
<action-btn
v-if="isRemovable"
:deploy-key="deployKey"
btn-css-class="btn-danger"
type="remove"
:title="__('Remove')"
data-container="body"
v-tooltip
>
<icon name="remove"/>
</action-btn>
<action-btn
v-else-if="isEnabled"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable"
:title="__('Disable')"
data-container="body"
v-tooltip
>
<icon name="cancel"/>
</action-btn>
</div>
</div>
</div>
</template>

View file

@ -1,62 +1,68 @@
<script>
import key from './key.vue';
import deployKey from './key.vue';
export default {
components: {
key,
export default {
components: {
deployKey,
},
props: {
keys: {
type: Array,
required: true,
},
props: {
title: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
store: {
type: Object,
required: true,
},
};
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
};
</script>
<template>
<div class="deploy-keys-panel">
<h5>
{{ title }}
({{ keys.length }})
</h5>
<ul
class="well-list"
v-if="keys.length"
>
<li
<div class="deploy-keys-panel table-holder">
<template v-if="keys.length > 0">
<div
role="row"
class="gl-responsive-table-row table-row-header">
<div
role="rowheader"
class="table-section section-40">
{{ s__('DeployKeys|Deploy key') }}
</div>
<div
role="rowheader"
class="table-section section-30">
{{ s__('DeployKeys|Project usage') }}
</div>
<div
role="rowheader"
class="table-section section-15 text-right">
{{ __('Created') }}
</div>
</div>
<deploy-key
v-for="deployKey in keys"
:key="deployKey.id"
>
<key
:deploy-key="deployKey"
:store="store"
:endpoint="endpoint"
/>
</li>
</ul>
:deploy-key="deployKey"
:store="store"
:endpoint="endpoint"
:project-id="projectId"
/>
</template>
<div
class="settings-message text-center"
v-else-if="showHelpBox"
v-else
>
No deploy keys found. Create one with the form above.
{{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
</template>

View file

@ -1,21 +1,24 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
export default () => new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
};
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
},
});
},
});
export default () =>
new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
projectId: this.$options.el.dataset.projectId,
};
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
projectId: this.projectId,
},
});
},
});

View file

@ -7,21 +7,24 @@ export default class DeployKeysService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
this.resource = Vue.resource(
`${this.endpoint}{/id}`,
{},
{
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
});
);
}
getKeys() {
return this.resource.get()
.then(response => response.json());
return this.resource.get().then(response => response.json());
}
enableKey(id) {

View file

@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
findEnabledKey(id) {
return this.keys.enabled_keys.find(key => key.id === id);
isEnabled(id) {
return this.keys.enabled_keys.some(key => key.id === id);
}
}

View file

@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
const hasDueDate = this.displayedDate !== 'No due date';
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
return this.$loading.fadeOut();
});
}

View file

@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [],
flags: [],
};
Object.keys(emojiMap).forEach((name) => {
Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
const fallbackSpriteAttribute = opts.sprite
? `data-fallback-sprite-class="${fallbackSpriteClass}"`
: '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);

View file

@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] ||
const hasColor =
imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
.length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize);
canvas.height = (numTestEntries * fontSize);
canvas.width = 2 * fontSize;
canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
testMapKeys.forEach((testKey) => {
testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
[].concat(testEntry).forEach(emojiUnicode => {
ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1;
});
});
@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas
const resultMap = {};
let readIndex = 0;
testMapKeys.forEach((testKey) => {
testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData(
0,
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
.data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
window.localStorage.setItem(
'gl-emoji-unicode-support-map',
JSON.stringify(unicodeSupportMap),
);
}
}

View file

@ -43,6 +43,7 @@
<div class="environments-container">
<loading-icon
class="prepend-top-default"
label="Loading environments"
v-if="isLoading"
size="3"

View file

@ -1,5 +1,5 @@
<script>
import playIconSvg from 'icons/_icon_play.svg';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@ -8,9 +8,9 @@
directives: {
tooltip,
},
components: {
loadingIcon,
Icon,
},
props: {
actions: {
@ -19,20 +19,16 @@
default: () => [],
},
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@ -65,7 +61,10 @@
:disabled="isLoading"
>
<span>
<span v-html="playIconSvg"></span>
<icon
name="play"
:size="12"
/>
<i
class="fa fa-caret-down"
aria-hidden="true"
@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
>
<span v-html="playIconSvg"></span>
<icon
name="play"
:size="12"
/>
<span>
{{ action.name }}
</span>

View file

@ -1,4 +1,5 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
@ -6,6 +7,9 @@
* Renders the external url link in environments table.
*/
export default {
components: {
Icon,
},
directives: {
tooltip,
},
@ -15,7 +19,6 @@
required: true,
},
},
computed: {
title() {
return s__('Environments|Open');
@ -34,10 +37,9 @@
:aria-label="title"
:href="externalUrl"
>
<i
class="fa fa-external-link"
aria-hidden="true"
>
</i>
<icon
name="external-link"
:size="12"
/>
</a>
</template>

View file

@ -2,20 +2,22 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
monitoringUrl: {
type: String,
required: true,
},
},
computed: {
title() {
return 'Monitoring';
@ -33,10 +35,9 @@
:title="title"
:aria-label="title"
>
<i
class="fa fa-area-chart"
aria-hidden="true"
>
</i>
<icon
name="chart"
:size="12"
/>
</a>
</template>

View file

@ -12,7 +12,6 @@
components: {
loadingIcon,
},
props: {
retryUrl: {
type: String,
@ -24,13 +23,11 @@
default: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
this.isLoading = true;

View file

@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import terminalIconSvg from 'icons/_icon_terminal.svg';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
terminalPath: {
type: String,
@ -18,13 +20,6 @@
default: '',
},
},
data() {
return {
terminalIconSvg,
};
},
computed: {
title() {
return 'Terminal';
@ -40,7 +35,10 @@
:title="title"
:aria-label="title"
:href="terminalPath"
v-html="terminalIconSvg"
>
<icon
name="terminal"
:size="12"
/>
</a>
</template>

View file

@ -1,19 +1,19 @@
import $ from 'jquery';
import _ from 'underscore';
import {
getSelector,
togglePopover,
inserted,
mouseenter,
mouseleave,
} from './feature_highlight_helper';
import {
togglePopover,
mouseenter,
debouncedMouseleave,
} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
`,
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave)
.on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll);
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
window.addEventListener('scroll', hideOnScroll, { once: true });
})
// Display feature highlight
.removeAttr('disabled');

View file

@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import Flash from '../flash';
import LazyLoader from '../lazy_loader';
import { togglePopover } from '../shared/popover';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId,
@ -27,23 +17,6 @@ export function dismiss(highlightId) {
this.hide();
}
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
togglePopover.call($featureHighlight, false);
}
}
export function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = togglePopover.call($featureHighlight, true);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
}
export function inserted() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;

View file

@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) {
if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true;
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
} else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
} else {
this.isLoadingData[at] = false;
}
}

View file

@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form');
const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
const params = parseQueryStringIntoObject(form.serialize());
return axios.get(form.data('signaturesPath'), { params })
const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);

View file

@ -0,0 +1,106 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { activityBarViews } from '../constants';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
computed: {
...mapGetters(['currentProject', 'hasChanges']),
...mapState(['currentActivityView']),
goBackUrl() {
return document.referrer || this.currentProject.web_url;
},
},
methods: {
...mapActions(['updateActivityBarView']),
},
activityBarViews,
};
</script>
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
<a
v-tooltip
data-container="body"
data-placement="right"
:href="goBackUrl"
class="ide-sidebar-link"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
>
<icon
:size="16"
name="go-back"
/>
</a>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
>
<icon
name="code"
/>
</button>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-review-mode"
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
@click.prevent="updateActivityBarView($options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
>
<icon
name="file-modified"
/>
</button>
</li>
<li v-show="hasChanges">
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
>
<icon
name="commit"
/>
</button>
</li>
</ul>
</nav>
</template>

View file

@ -1,31 +1,88 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
export default {
components: {
icon,
Icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
showTooltip: {
type: Boolean,
required: false,
default: false,
},
showStagedIcon: {
type: Boolean,
required: false,
default: false,
},
forceModifiedIcon: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
changedIcon() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile && !this.forceModifiedIcon
? `file-addition${suffix}`
: `file-modified${suffix}`;
},
stagedIcon() {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
return `multi-${this.changedIcon}`;
return `multi-${this.changedIcon} pull-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
const type = this.file.tempFile ? 'addition' : 'modification';
if (this.file.changed && !this.file.staged) {
return sprintf(__('Unstaged %{type}'), {
type,
});
} else if (!this.file.changed && this.file.staged) {
return sprintf(__('Staged %{type}'), {
type,
});
} else if (this.file.changed && this.file.staged) {
return sprintf(__('Unstaged and staged %{type}'), {
type: pluralize(type),
});
}
return undefined;
},
},
};
</script>
<template>
<icon
:name="changedIcon"
:size="12"
:css-classes="`ide-file-changed-icon ${changedIconClass}`"
/>
<span
v-tooltip
:title="tooltipTitle"
data-container="body"
data-placement="right"
class="ide-file-changed-icon"
>
<icon
v-if="file.changed || file.tempFile || file.staged"
:name="changedIcon"
:size="12"
:css-classes="changedIconClass"
/>
</span>
</template>

View file

@ -1,41 +1,39 @@
<script>
import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
export default {
components: {
RadioGroup,
export default {
components: {
RadioGroup,
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` },
false,
);
},
computed: {
...mapState([
'currentBranchId',
]),
newMergeRequestHelpText() {
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` },
false,
);
},
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
disableMergeRequestRadio() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
},
mounted() {
if (this.disableMergeRequestRadio) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
methods: {
...mapActions('commit', ['updateCommitAction']),
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
</script>
<template>
@ -53,13 +51,12 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
:help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
:help-text="newMergeRequestHelpText"
:disabled="disableMergeRequestRadio"
/>
</div>
</template>

View file

@ -0,0 +1,36 @@
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
},
};
</script>
<template>
<div
v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
<div
class="ide-commit-empty-state-container"
>
<div class="svg-content svg-80">
<img :src="noChangesStateSvgPath" />
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,171 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
export default {
components: {
Actions,
LoadingButton,
CommitMessageField,
SuccessMessage,
},
data() {
return {
isCompact: true,
componentHeight: null,
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
overviewText() {
return sprintf(
__(
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
},
);
},
},
watch: {
currentActivityView() {
if (this.lastCommitMsg) {
this.isCompact = false;
} else {
this.isCompact = !(
this.currentActivityView === activityBarViews.commit &&
window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
);
}
},
lastCommitMsg() {
this.isCompact =
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
},
},
methods: {
...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
toggleIsSmall() {
this.updateActivityBarView(activityBarViews.commit)
.then(() => {
this.isCompact = !this.isCompact;
})
.catch(e => {
throw e;
});
},
beforeEnterTransition() {
const elHeight = this.isCompact
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
},
enterTransition() {
this.$nextTick(() => {
const elHeight = this.isCompact
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
});
},
afterEndTransition() {
this.componentHeight = null;
},
},
activityBarViews,
};
</script>
<template>
<div
class="multi-file-commit-form"
:class="{
'is-compact': isCompact,
'is-full': !isCompact
}"
:style="{
height: componentHeight ? `${componentHeight}px` : null,
}"
>
<transition
name="commit-form-slide-up"
@before-enter="beforeEnterTransition"
@enter="enterTransition"
@after-enter="afterEndTransition"
>
<div
v-if="isCompact"
class="commit-form-compact"
ref="compactEl"
>
<button
type="button"
:disabled="!hasChanges"
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
{{ __('Commit') }}
</button>
<p
class="text-center"
v-html="overviewText"
></p>
</div>
<form
v-if="!isCompact"
class="form-horizontal"
@submit.prevent.stop="commitChanges"
ref="formEl"
>
<transition name="fade">
<success-message
v-show="lastCommitMsg"
/>
</transition>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
<button
v-else
type="button"
class="btn btn-default btn-sm pull-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
</button>
</div>
</form>
</transition>
</div>
</template>

View file

@ -1,66 +1,128 @@
<script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
export default {
components: {
icon,
listItem,
listCollapsed,
export default {
components: {
Icon,
ListItem,
},
directives: {
tooltip,
},
props: {
title: {
type: String,
required: true,
},
props: {
title: {
type: String,
required: true,
},
fileList: {
type: Array,
required: true,
},
fileList: {
type: Array,
required: true,
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
isCommitInfoShown() {
return this.rightPanelCollapsed || this.fileList.length;
},
iconName: {
type: String,
required: true,
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
},
action: {
type: String,
required: true,
},
};
actionBtnText: {
type: String,
required: true,
},
itemActionComponent: {
type: String,
required: true,
},
stagedList: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
showActionButton: false,
};
},
computed: {
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
});
},
},
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
setShowActionButton(show) {
this.showActionButton = show;
},
},
};
</script>
<template>
<div
:class="{
'multi-file-commit-list': isCommitInfoShown
}"
class="ide-commit-list-container"
>
<list-collapsed
v-if="rightPanelCollapsed"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
<header
class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
>
<div
class="multi-file-commit-panel-header-title"
>
<li
v-for="file in fileList"
:key="file.key"
<icon
v-once
:name="iconName"
:size="18"
/>
{{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
<list-item
:file="file"
/>
</li>
</ul>
</template>
{{ fileList.length }}
</span>
<button
v-show="showActionButton"
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
>
{{ actionBtnText }}
</button>
</div>
</header>
<ul
v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</div>
</template>

View file

@ -1,35 +1,110 @@
<script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { sprintf, n__, __ } from '~/locale';
export default {
components: {
icon,
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
files: {
type: Array,
required: true,
},
computed: {
...mapGetters([
'addedFiles',
'modifiedFiles',
]),
iconName: {
type: String,
required: true,
},
};
title: {
type: String,
required: true,
},
},
computed: {
addedFilesLength() {
return this.files.filter(f => f.tempFile).length;
},
modifiedFilesLength() {
return this.files.filter(f => !f.tempFile).length;
},
addedFilesIconClass() {
return this.addedFilesLength ? 'multi-file-addition' : '';
},
modifiedFilesClass() {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
});
},
modifiedTooltip() {
return sprintf(
n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
{ type: this.title.toLowerCase() },
);
},
titleTooltip() {
return sprintf(__('%{title} changes'), { title: this.title });
},
additionIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
},
modifiedIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
},
},
};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
<icon
name="file-addition"
:size="18"
css-classes="multi-file-addition append-bottom-10"
/>
{{ addedFiles.length }}
<icon
name="file-modified"
:size="18"
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
/>
{{ modifiedFiles.length }}
<div
v-tooltip
:title="titleTooltip"
data-container="body"
data-placement="left"
class="append-bottom-15"
>
<icon
v-once
:name="iconName"
:size="18"
/>
</div>
<div
v-tooltip
:title="additionsTooltip"
data-container="body"
data-placement="left"
class="append-bottom-10"
>
<icon
:name="additionIconName"
:size="18"
:css-classes="addedFilesIconClass"
/>
</div>
{{ addedFilesLength }}
<div
v-tooltip
:title="modifiedTooltip"
data-container="body"
data-placement="left"
class="prepend-top-10 append-bottom-10"
>
<icon
:name="modifiedIconName"
:size="18"
:css-classes="modifiedFilesClass"
/>
</div>
{{ modifiedFilesLength }}
</div>
</template>

View file

@ -1,34 +1,70 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
export default {
components: {
Icon,
StageButton,
UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
actionComponent: {
type: String,
required: true,
},
keyPrefix: {
type: String,
required: false,
default: '',
},
stagedList: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
const prefix = this.stagedList ? '-solid' : '';
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
methods: {
...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
openFileInEditor(file) {
return this.openPendingTab(file).then(changeViewer => {
...mapActions([
'discardFileChanges',
'updateViewer',
'openPendingTab',
'unstageChange',
'stageChange',
]),
openFileInEditor() {
return this.openPendingTab({
file: this.file,
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
this.updateViewer(viewerTypes.diff);
}
});
},
fileAction() {
if (this.file.staged) {
this.unstageChange(this.file.path);
} else {
this.stageChange(this.file.path);
}
},
},
};
</script>
@ -38,7 +74,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
@click="openFileInEditor(file)">
@dblclick="fileAction"
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@ -47,12 +85,9 @@ export default {
/>{{ file.path }}
</span>
</button>
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file.path)"
>
Discard
</button>
<component
:is="actionComponent"
:path="file.path"
/>
</div>
</template>

View file

@ -0,0 +1,130 @@
<script>
import { __, sprintf } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
export default {
directives: {
popover,
},
components: {
Icon,
},
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
scrollTop: 0,
isFocused: false,
};
},
computed: {
allLines() {
return this.text.split('\n').map((line, i) => ({
text: line.substr(0, this.getLineLength(i)) || ' ',
highlightedText: line.substr(this.getLineLength(i)),
}));
},
},
methods: {
handleScroll() {
if (this.$refs.textarea) {
this.$nextTick(() => {
this.scrollTop = this.$refs.textarea.scrollTop;
});
}
},
getLineLength(i) {
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
},
onInput(e) {
this.$emit('input', e.target.value);
},
updateIsFocused(isFocused) {
this.isFocused = isFocused;
},
},
popoverOptions: {
trigger: 'hover',
placement: 'top',
content: sprintf(
__(`
The character highligher helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
),
},
};
</script>
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
class="md-area"
:class="{
'is-focused': isFocused
}"
>
<div
v-once
class="md-header"
>
<ul class="nav-links">
<li>
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
class="help-block prepend-left-10"
>
<icon
name="question"
/>
</span>
</li>
</ul>
</div>
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
>
<div
v-for="(line, index) in allLines"
:key="index"
>
<span
v-text="line.text"
>
</span><mark
v-show="line.highlightedText"
v-text="line.highlightedText"
>
</mark>
</div>
</div>
</div>
<textarea
class="note-textarea ide-commit-message-textarea"
name="commit-message"
:placeholder="__('Write a commit message...')"
:value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
ref="textarea"
>
</textarea>
</div>
</div>
</fieldset>
</template>

View file

@ -1,82 +1,78 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
props: {
value: {
type: String,
required: true,
},
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
required: false,
default: null,
},
checked: {
type: Boolean,
required: false,
default: false,
},
showInput: {
type: Boolean,
required: false,
default: false,
},
helpText: {
type: String,
required: false,
default: null,
},
label: {
type: String,
required: false,
default: null,
},
computed: {
...mapState('commit', [
'commitAction',
]),
...mapGetters('commit', [
'newBranchName',
]),
checked: {
type: Boolean,
required: false,
default: false,
},
methods: {
...mapActions('commit', [
'updateCommitAction',
'updateBranchName',
]),
showInput: {
type: Boolean,
required: false,
default: false,
},
};
disabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
return this.disabled
? __('This option is disabled while you still have unstaged changes')
: '';
},
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
},
};
</script>
<template>
<fieldset>
<label>
<label
v-tooltip
:title="tooltipTitle"
:class="{
'is-disabled': disabled
}"
>
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
:checked="checked"
v-once
:checked="commitAction === value"
:disabled="disabled"
/>
<span class="prepend-left-10">
<template v-if="label">
{{ label }}
</template>
<slot v-else></slot>
<span
v-if="helpText"
v-tooltip
class="help-block inline"
:title="helpText"
v-if="label"
class="ide-radio-label"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
>
</i>
{{ label }}
</span>
<slot v-else></slot>
</span>
</label>
<div
@ -85,7 +81,7 @@
>
<input
type="text"
class="form-control"
class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>

View file

@ -0,0 +1,59 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions(['stageChange', 'discardFileChanges']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
v-tooltip
type="button"
class="btn btn-blank append-right-5"
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
data-container="body"
@click.stop="stageChange(path)"
>
<icon
name="mobile-issue-close"
:size="12"
/>
</button>
<button
v-tooltip
type="button"
class="btn btn-blank"
:aria-label="__('Discard changes')"
:title="__('Discard changes')"
data-container="body"
@click.stop="discardFileChanges(path)"
>
<icon
name="remove"
:size="12"
/>
</button>
</div>
</template>

View file

@ -0,0 +1,33 @@
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['lastCommitMsg', 'committedStateSvgPath']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel-success-message"
aria-live="assertive"
>
<div class="svg-content svg-80">
<img
:src="committedStateSvgPath"
alt=""
/>
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,45 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions(['unstageChange']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
v-tooltip
type="button"
class="btn btn-blank"
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
data-container="body"
@click="unstageChange(path)"
>
<icon
name="history"
:size="12"
/>
</button>
</div>
</template>

View file

@ -1,28 +1,15 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
mergeRequestId: {
type: Number,
required: true,
},
},
@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
viewerTypes,
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
class="btn btn-link"
data-toggle="dropdown"
>
<template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
{{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
@click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
'is-active': viewer === 'editor',
'is-active': viewer === $options.viewerTypes.mr,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
@click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
'is-active': viewer === 'diff',
'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>

View file

@ -0,0 +1,243 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue';
import router from '../../ide_router';
import {
MAX_FILE_FINDER_RESULTS,
FILE_FINDER_ROW_HEIGHT,
FILE_FINDER_EMPTY_ROW_HEIGHT,
} from '../../constants';
import {
UP_KEY_CODE,
DOWN_KEY_CODE,
ENTER_KEY_CODE,
ESC_KEY_CODE,
} from '../../../lib/utils/keycodes';
export default {
components: {
Item,
VirtualList,
},
data() {
return {
focusedIndex: 0,
searchText: '',
mouseOver: false,
cancelMouseOver: false,
};
},
computed: {
...mapGetters(['allBlobs']),
...mapState(['fileFindVisible', 'loading']),
filteredBlobs() {
const searchText = this.searchText.trim();
if (searchText === '') {
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
}
return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
key: 'path',
maxResults: MAX_FILE_FINDER_RESULTS,
});
},
filteredBlobsLength() {
return this.filteredBlobs.length;
},
listShowCount() {
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
},
listHeight() {
return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
},
showClearInputButton() {
return this.searchText.trim() !== '';
},
},
watch: {
fileFindVisible() {
this.$nextTick(() => {
if (!this.fileFindVisible) {
this.searchText = '';
} else {
this.focusedIndex = 0;
if (this.$refs.searchInput) {
this.$refs.searchInput.focus();
}
}
});
},
searchText() {
this.focusedIndex = 0;
},
focusedIndex() {
if (!this.mouseOver) {
this.$nextTick(() => {
const el = this.$refs.virtualScrollList.$el;
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
if (this.focusedIndex === 0) {
// if index is the first index, scroll straight to start
el.scrollTop = 0;
} else if (this.focusedIndex === this.filteredBlobsLength - 1) {
// if index is the last index, scroll to the end
el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
} else if (scrollTop >= bottom + el.scrollTop) {
// if element is off the bottom of the scroll list, scroll down one item
el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
} else if (scrollTop < el.scrollTop) {
// if element is off the top of the scroll list, scroll up one item
el.scrollTop = scrollTop;
}
});
}
},
},
methods: {
...mapActions(['toggleFileFinder']),
clearSearchInput() {
this.searchText = '';
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
},
onKeydown(e) {
switch (e.keyCode) {
case UP_KEY_CODE:
e.preventDefault();
this.mouseOver = false;
this.cancelMouseOver = true;
if (this.focusedIndex > 0) {
this.focusedIndex -= 1;
} else {
this.focusedIndex = this.filteredBlobsLength - 1;
}
break;
case DOWN_KEY_CODE:
e.preventDefault();
this.mouseOver = false;
this.cancelMouseOver = true;
if (this.focusedIndex < this.filteredBlobsLength - 1) {
this.focusedIndex += 1;
} else {
this.focusedIndex = 0;
}
break;
default:
break;
}
},
onKeyup(e) {
switch (e.keyCode) {
case ENTER_KEY_CODE:
this.openFile(this.filteredBlobs[this.focusedIndex]);
break;
case ESC_KEY_CODE:
this.toggleFileFinder(false);
break;
default:
break;
}
},
openFile(file) {
this.toggleFileFinder(false);
router.push(`/project${file.url}`);
},
onMouseOver(index) {
if (!this.cancelMouseOver) {
this.mouseOver = true;
this.focusedIndex = index;
}
},
onMouseMove(index) {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
},
};
</script>
<template>
<div
class="ide-file-finder-overlay"
@mousedown.self="toggleFileFinder(false)"
>
<div
class="dropdown-menu diff-file-changes ide-file-finder show"
>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
:placeholder="__('Search files')"
autocomplete="off"
v-model="searchText"
ref="searchInput"
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
:class="{
hidden: showClearInputButton
}"
></i>
<i
role="button"
:aria-label="__('Clear search input')"
class="fa fa-times dropdown-input-clear"
:class="{
show: showClearInputButton
}"
@click="clearSearchInput"
></i>
</div>
<div>
<virtual-list
:size="listHeight"
:remain="listShowCount"
wtag="ul"
ref="virtualScrollList"
>
<template v-if="filteredBlobsLength">
<li
v-for="(file, index) in filteredBlobs"
:key="file.key"
>
<item
class="disable-hover"
:file="file"
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
@click="openFile"
@mouseover="onMouseOver"
@mousemove="onMouseMove"
/>
</li>
</template>
<li
v-else
class="dropdown-menu-empty-item"
>
<div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
<template v-if="loading">
{{ __('Loading...') }}
</template>
<template v-else>
{{ __('No files found.') }}
</template>
</div>
</li>
</virtual-list>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,113 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
export default {
components: {
ChangedFileIcon,
FileIcon,
},
props: {
file: {
type: Object,
required: true,
},
focused: {
type: Boolean,
required: true,
},
searchText: {
type: String,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
pathWithEllipsis() {
const path = this.file.path;
return path.length < MAX_PATH_LENGTH
? path
: `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
},
nameSearchTextOccurences() {
return fuzzaldrinPlus.match(this.file.name, this.searchText);
},
pathSearchTextOccurences() {
return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
},
},
methods: {
clickRow() {
this.$emit('click', this.file);
},
mouseOverRow() {
this.$emit('mouseover', this.index);
},
mouseMove() {
this.$emit('mousemove', this.index);
},
},
};
</script>
<template>
<button
type="button"
class="diff-changed-file"
:class="{
'is-focused': focused,
}"
@click.prevent="clickRow"
@mouseover="mouseOverRow"
@mousemove="mouseMove"
>
<file-icon
:file-name="file.name"
:size="16"
css-classes="diff-file-changed-icon append-right-8"
/>
<span class="diff-changed-file-content append-right-8">
<strong
class="diff-changed-file-name"
>
<span
v-for="(char, index) in file.name.split('')"
:key="index + char"
:class="{
highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
}"
v-text="char"
>
</span>
</strong>
<span
class="diff-changed-file-path prepend-top-5"
>
<span
v-for="(char, index) in pathWithEllipsis.split('')"
:key="index + char"
:class="{
highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
}"
v-text="char"
>
</span>
</span>
</span>
<span
v-if="file.changed || file.tempFile"
class="diff-changed-stats"
>
<changed-file-icon
:file="file"
/>
</span>
</button>
</template>

View file

@ -1,35 +1,31 @@
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
ideStatusBar,
repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
IdeSidebar,
RepoTabs,
IdeStatusBar,
RepoEditor,
FindFile,
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
'emptyStateSvgPath',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
@ -42,67 +38,93 @@ export default {
});
return returnValue;
};
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
},
};
</script>
<template>
<div
class="ide-view"
>
<ide-sidebar />
<article class="ide">
<div
class="multi-file-edit-pane"
class="ide-view"
>
<template
v-if="activeFile"
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar />
<div
class="multi-file-edit-pane"
>
<repo-tabs
:active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
<ide-status-bar
:file="activeFile"
/>
</template>
<template
v-else
>
<div
v-once
class="ide-empty-state"
<template
v-if="activeFile"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
<repo-tabs
:active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
</template>
<template
v-else
>
<div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
</div>
<div class="col-xs-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
</h4>
<p>
You can select a file in the left sidebar to begin
editing and use the right sidebar to commit your changes.
</p>
<div class="col-xs-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
</h4>
<p>
Select a file from the left sidebar to begin editing.
Afterwards, you'll be able to commit your changes.
</p>
</div>
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
<ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
<ide-status-bar
:file="activeFile"
/>
</div>
</article>
</template>

View file

@ -1,84 +0,0 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
<template>
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed"
>
<div
v-if="changedFiles.length"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</resizable-panel>
</template>

View file

@ -1,43 +0,0 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
projectUrl: {
type: String,
required: true,
},
},
computed: {
goBackUrl() {
return document.referrer || this.projectUrl;
},
},
};
</script>
<template>
<nav
class="ide-external-links"
v-once
>
<p>
<a
:href="goBackUrl"
class="ide-sidebar-link"
>
<icon
:size="16"
class="append-right-8"
name="go-back"
/>
<span class="ide-external-links-text">
{{ s__('Go back') }}
</span>
</a>
</p>
</nav>
</template>

View file

@ -1,47 +0,0 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title str-truncated ref-name">
<icon
name="branch"
:size="12"
/>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""
/>
</div>
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>

View file

@ -1,65 +0,0 @@
<script>
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import BranchesTree from './ide_project_branches_tree.vue';
import ExternalLinks from './ide_external_links.vue';
export default {
components: {
BranchesTree,
ExternalLinks,
ProjectAvatarImage,
Identicon,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="projects-sidebar">
<div class="context-header">
<a
:title="project.name"
:href="project.web_url"
>
<div
v-if="project.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="project.path"
:img-src="project.avatar_url"
:img-alt="project.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="project.id"
:entity-name="project.name"
/>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<external-links
:project-url="project.web_url"
/>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="branch in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"
/>
</div>
</div>
</template>

View file

@ -1,41 +0,0 @@
<script>
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
export default {
components: {
RepoFile,
SkeletonLoadingContainer,
},
props: {
tree: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div
class="ide-file-list"
>
<template v-if="tree.loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
:key="n"
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
<repo-file
v-for="file in tree.tree"
:key="file.key"
:file="file"
:level="0"
/>
</template>
</div>
</template>

View file

@ -0,0 +1,62 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import IdeTreeList from './ide_tree_list.vue';
import EditorModeDropdown from './editor_mode_dropdown.vue';
import { viewerTypes } from '../constants';
export default {
components: {
IdeTreeList,
EditorModeDropdown,
},
computed: {
...mapGetters(['currentMergeRequest']),
...mapState(['viewer']),
showLatestChangesText() {
return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
},
showMergeRequestText() {
return this.currentMergeRequest && this.viewer === viewerTypes.mr;
},
},
mounted() {
this.$nextTick(() => {
this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
:viewer-type="viewer"
header-class="ide-review-header"
:disable-action-dropdown="true"
>
<template
slot="header"
>
<div class="ide-review-button-holder">
{{ __('Review') }}
<editor-mode-dropdown
v-if="currentMergeRequest"
:viewer="viewer"
:merge-request-id="currentMergeRequest.iid"
@click="updateViewer"
/>
</div>
<div class="prepend-top-5 ide-review-sub-header">
<template v-if="showLatestChangesText">
{{ __('Latest changes') }}
</template>
<template v-else-if="showMergeRequestText">
{{ __('Merge request') }}
(<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
</template>
</div>
</template>
</ide-tree-list>
</template>

View file

@ -1,36 +1,82 @@
<script>
import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import { activityBarViews } from '../constants';
export default {
components: {
projectTree,
icon,
panelResizer,
skeletonLoadingContainer,
ResizablePanel,
export default {
directives: {
tooltip,
},
components: {
Icon,
PanelResizer,
SkeletonLoadingContainer,
ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
},
data() {
return {
showTooltip: false,
};
},
computed: {
...mapState([
'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges)
);
},
computed: {
...mapState([
'loading',
]),
...mapGetters([
'projectsWithTrees',
]),
branchTooltipTitle() {
return this.showTooltip ? this.currentBranchId : undefined;
},
};
},
watch: {
currentBranchId() {
this.$nextTick(() => {
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
},
};
</script>
<template>
<resizable-panel
:collapsible="false"
:initial-width="290"
:initial-width="340"
side="left"
>
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
<project-tree
v-for="project in projectsWithTrees"
:key="project.id"
:project="project"
/>
<template v-else>
<div class="context-header ide-context-header">
<a
:href="currentProject.web_url"
>
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="currentProject.id"
:entity-name="currentProject.name"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
<div
class="sidebar-context-title ide-sidebar-branch-title"
ref="branchId"
v-tooltip
:title="branchTooltipTitle"
>
<icon
name="branch"
css-classes="append-right-5"
/>{{ currentBranchId }}
</div>
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
:is="currentActivityView"
/>
</div>
<commit-form />
</template>
</div>
</resizable-panel>
</template>

View file

@ -1,11 +1,14 @@
<script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
userAvatarImage,
},
directives: {
tooltip,
@ -14,47 +17,93 @@ export default {
props: {
file: {
type: Object,
required: true,
required: false,
default: null,
},
},
data() {
return {
lastCommitFormatedAge: null,
};
},
computed: {
...mapGetters(['currentProject', 'lastCommit']),
},
mounted() {
this.startTimer();
},
beforeDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
},
methods: {
startTimer() {
this.intervalId = setInterval(() => {
this.commitAgeUpdate();
}, 1000);
},
commitAgeUpdate() {
if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
}
},
getCommitPath(shortSha) {
return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
};
</script>
<template>
<div class="ide-status-bar">
<div class="ref-name">
<footer class="ide-status-bar">
<div
class="ide-status-branch"
v-if="lastCommit && lastCommitFormatedAge"
>
<icon
name="branch"
:size="12"
name="commit"
/>
{{ file.branchId }}
<a
v-tooltip
class="commit-sha"
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
>{{ lastCommit.short_id }}</a>
by
{{ lastCommit.author_name }}
<time
v-tooltip
data-placement="top"
data-container="body"
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
>
{{ lastCommitFormatedAge }}
</time>
</div>
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
<a
v-tooltip
:title="file.lastCommit.message"
:href="file.lastCommit.url"
>
{{ timeFormated(file.lastCommit.updatedAt) }} by
{{ file.lastCommit.author }}
</a>
</div>
</div>
<div class="text-right">
<div
v-if="file"
class="ide-status-file"
>
{{ file.name }}
</div>
<div class="text-right">
<div
v-if="file"
class="ide-status-file"
>
{{ file.eol }}
</div>
<div
class="text-right"
v-if="!file.binary">
class="ide-status-file"
v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
<div
v-if="file"
class="ide-status-file"
>
{{ file.fileLanguage }}
</div>
</div>
</footer>
</template>

View file

@ -0,0 +1,42 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import NewDropdown from './new_dropdown/index.vue';
import IdeTreeList from './ide_tree_list.vue';
export default {
components: {
NewDropdown,
IdeTreeList,
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
},
mounted() {
if (this.activeFile && this.activeFile.pending) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
}
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
viewer-type="editor"
>
<template
slot="header"
>
{{ __('Edit') }}
<new-dropdown
:project-id="currentProject.name_with_namespace"
:branch="currentBranchId"
/>
</template>
</ide-tree-list>
</template>

View file

@ -0,0 +1,76 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
export default {
components: {
Icon,
RepoFile,
SkeletonLoadingContainer,
NewDropdown,
},
props: {
viewerType: {
type: String,
required: true,
},
headerClass: {
type: String,
required: false,
default: null,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree']),
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
},
mounted() {
this.updateViewer(this.viewerType);
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<div
class="ide-file-list"
>
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-for="n in 3"
:key="n"
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
<header
class="ide-tree-header"
:class="headerClass"
>
<slot name="header"></slot>
</header>
<repo-file
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:disable-action-dropdown="disableActionDropdown"
/>
</template>
</div>
</template>

View file

@ -16,8 +16,8 @@ export default {
<icon
name="git-merge"
v-tooltip
title="__('Part of merge request changes')"
css-classes="ide-file-changed-icon"
:title="__('Part of merge request changes')"
css-classes="append-right-8"
:size="12"
/>
</template>

View file

@ -1,49 +1,55 @@
<script>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
export default {
components: {
icon,
newModal,
upload,
export default {
components: {
icon,
newModal,
upload,
},
props: {
branch: {
type: String,
required: true,
},
props: {
branch: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
path: {
type: String,
required: false,
default: '',
},
data() {
return {
openModal: false,
modalType: '',
dropdownOpen: false,
};
},
data() {
return {
openModal: false,
modalType: '',
dropdownOpen: false,
};
},
watch: {
dropdownOpen() {
this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView();
});
},
methods: {
...mapActions([
'createTempEntry',
]),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
this.dropdownOpen = false;
},
hideModal() {
this.openModal = false;
},
openDropdown() {
this.dropdownOpen = !this.dropdownOpen;
},
},
methods: {
...mapActions(['createTempEntry']),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
this.dropdownOpen = false;
},
};
hideModal() {
this.openModal = false;
},
openDropdown() {
this.dropdownOpen = !this.dropdownOpen;
},
},
};
</script>
<template>
@ -71,7 +77,10 @@
css-classes="pull-left"
/>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<ul
class="dropdown-menu dropdown-menu-right"
ref="dropdownMenu"
>
<li>
<a
href="#"

View file

@ -40,13 +40,6 @@ export default {
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
@ -82,8 +75,8 @@ export default {
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
<label class="label-light col-sm-3 ide-new-modal-label">
{{ __('Name') }}
</label>
<div class="col-sm-9">
<input

View file

@ -1,72 +1,65 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
import { activityBarViews } from '../constants';
export default {
components: {
DeprecatedModal,
icon,
commitFilesList,
Actions,
LoadingButton,
Icon,
CommitFilesList,
EmptyState,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'changedFiles',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', [
'commitButtonDisabled',
'discardDraftButtonDisabled',
'branchName',
]),
statusSvg() {
return this.lastCommitMsg
? this.committedStateSvgPath
: this.noChangesStateSvgPath;
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
watch: {
hasChanges() {
if (!this.hasChanges) {
this.updateActivityBarView(activityBarViews.edit);
}
},
},
mounted() {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
})
.then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
})
.catch(e => {
throw e;
});
}
},
methods: {
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
this.commitChanges(),
);
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@ -75,9 +68,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
:class="{
'multi-file-commit-empty-state-container': !changedFiles.length
}"
>
<deprecated-modal
id="ide-create-branch-modal"
@ -91,82 +81,30 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
<commit-files-list
title="Staged"
:file-list="changedFiles"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<template
v-if="changedFiles.length"
v-if="showStageUnstageArea"
>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message"
name="commit-message"
:value="commitMessage"
:placeholder="__('Write a commit message...')"
@input="updateCommitMessage($event.target.value)"
>
</textarea>
</div>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
</div>
</form>
<commit-files-list
class="is-first"
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
action="stageAllChanges"
:action-btn-text="__('Stage all')"
item-action-component="stage-button"
/>
<commit-files-list
icon-name="staged"
:title="__('Staged')"
:file-list="stagedFiles"
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
:staged-list="true"
/>
</template>
<div
v-else-if="!rightPanelCollapsed"
class="row js-empty-state"
>
<div class="col-xs-10 col-xs-offset-1">
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
</div>
<div class="col-xs-10 col-xs-offset-1">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg">
</p>
</div>
</div>
</div>
<empty-state
v-if="unusedSeal"
/>
</div>
</template>

View file

@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
@ -19,8 +20,14 @@ export default {
},
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']),
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@ -36,10 +43,29 @@ export default {
},
},
watch: {
file(oldVal, newVal) {
file(newVal, oldVal) {
if (oldVal.pending) {
this.removePendingTab(oldVal);
}
// Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) {
if (oldVal.key !== this.file.key) {
this.initMonaco();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
}
}
},
currentActivityView() {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
}
},
rightPanelCollapsed() {
@ -77,7 +103,7 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
'removePendingTab',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@ -89,14 +115,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
@ -108,10 +126,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
@ -120,9 +138,14 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.file);
const head = this.getStagedFile(this.file.path);
if (this.viewer === 'mrdiff') {
this.model = this.editor.createModel(
this.file,
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
if (this.viewer === viewerTypes.mr && this.file.mrChange) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
@ -163,6 +186,7 @@ export default {
});
},
},
viewerTypes,
};
</script>
@ -171,16 +195,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
<div class="ide-mode-tabs clearfix">
<div class="ide-mode-tabs clearfix" >
<ul
class="nav-links pull-left"
v-if="!shouldHideEditor">
v-if="!shouldHideEditor && isEditModeActive"
>
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'">
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
@ -207,6 +232,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
>
</div>
<content-viewer

View file

@ -1,22 +1,29 @@
<script>
import { mapActions } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import { mapActions, mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
import ChangedFileIcon from './changed_file_icon.vue';
import MrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
directives: {
tooltip,
},
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
changedFileIcon,
mrFileIcon,
SkeletonLoadingContainer,
NewDropdown,
FileStatusIcon,
FileIcon,
ChangedFileIcon,
MrFileIcon,
Icon,
},
props: {
file: {
@ -27,8 +34,41 @@ export default {
type: Number,
required: true,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters([
'getChangesInFolder',
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
folderStagedCount() {
return this.getStagedFilesCountForPath(this.file.path);
},
changesCount() {
return this.getChangesInFolder(this.file.path);
},
folderChangesTooltip() {
if (this.changesCount === 0) return undefined;
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
});
},
isTree() {
return this.file.type === 'tree';
},
@ -48,23 +88,30 @@ export default {
'is-open': this.file.opened,
};
},
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
this.$el.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
},
methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
return this.updateDelayViewerUpdated(true).then(() => {
router.push(`/project${this.file.url}`);
});
router.push(`/project${this.file.url}`);
},
},
};
@ -97,17 +144,36 @@ export default {
:file="file"
/>
</span>
<span class="pull-right">
<span class="pull-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
<span
v-if="showTreeChangesCount"
class="ide-tree-changes"
>
{{ changesCount }}
<icon
v-tooltip
:title="folderChangesTooltip"
data-container="body"
data-placement="right"
name="file-modified"
:size="12"
css-classes="prepend-left-5 multi-file-modified"
/>
</span>
<changed-file-icon
v-else-if="showChangedFileIcon"
:file="file"
v-if="file.changed || file.tempFile"
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
class="pull-right"
/>
</span>
<new-dropdown
v-if="isTree"
v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"

View file

@ -26,13 +26,18 @@ export default {
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
return this.tab.changed ? !this.tabMouseOver : false;
if (this.tab.pending) return true;
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@ -42,18 +47,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
this.openPendingTab(tab);
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
if (this.tab.changed) {
if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
if (this.tab.changed) {
if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
@ -63,32 +68,15 @@ export default {
<template>
<li
:class="{
active: tab.active
}"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
>
<icon
v-if="!showChangedIcon"
name="close"
:size="12"
/>
<changed-file-icon
v-else
:file="tab"
/>
</button>
<div
class="multi-file-tab"
:class="{
active: tab.active
}"
:title="tab.url"
>
<file-icon
@ -100,5 +88,23 @@ export default {
:file="tab"
/>
</div>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
:disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
name="close"
:size="12"
/>
<changed-file-icon
v-else
:file="tab"
:force-modified-icon="true"
/>
</button>
</li>
</template>

View file

@ -32,16 +32,6 @@ export default {
default: '',
},
},
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div>
</template>

View file

@ -0,0 +1,24 @@
// Fuzzy file finder
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
export const activityBarViews = {
edit: 'ide-tree',
commit: 'commit-section',
review: 'ide-review',
};
export const viewerTypes = {
mr: 'mrdiff',
edit: 'editor',
diff: 'diff',
};

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