Commit 419518df authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/improve-mrwbs-and-todos-for-pipelines

* master: (221 commits)
  Add CHANGELOG entry for 8.12.6
  Added 'Download' button to snippet view
  Merge branch 'api-fix-project-group-sharing' into 'security'
  Add 8.12.5, 8.11.9, and 8.10.12 CHANGELOG entries
  FIx JS bug with select2 because of missing `data-field` attribute in select box.
  Remove pointless `.vagrant_enabled` file
  allow multiple labels commands
  Move some CHANGELOG entries to the 8.13.0 part
  Move operations/ to new location
  Move health check docs under user/admin_area/monitoring
  Make guests unable to view MRs
  Add examples of fake tokens to be used in docs
  Remove duplicate CHANGELOG entry
  Allow browsing branches that end with '.atom'
  Refactor the SubGit/SVN documentation
  Document the new CI_DEBUG_TRACE variable
  Remove redundant images
  changed the scss for the top line connectors to be exactly centered
  Rearrange GitLab basics READMEs
  New images for GitLab basics "Create MR" docs
  ...

Conflicts:
  app/models/commit_status.rb
parents 6f7afaa8 d3a98380
...@@ -19,6 +19,8 @@ variables: ...@@ -19,6 +19,8 @@ variables:
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- mkdir -p tmp/tests
- mount -t tmpfs tmpfs tmp/tests || echo "tmpfs mount failed, falling back to disc"
- bundle --version - bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack - retry gem install knapsack
......
...@@ -2,42 +2,71 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -2,42 +2,71 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page
- Update runner version only when updating contacted_at - Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Use gitlab-shell v3.6.2 (GIT TRACE logging) - Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Updating verbiage on git basics to be more intuitive
- Clarify documentation for Runners API (Gennady Trafimenkov)
- Change user & group landing page routing from /u/:username to /:username
- Prevent running GfmAutocomplete setup for each diff note !6569
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Fix Error 500 when viewing old merge requests with bad diff data
- Speed-up group milestones show page - Speed-up group milestones show page
- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment - Keep refs for each deployment
- Allow browsing branches that end with '.atom'
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Update Gitlab Shell to fix some problems with moving projects between storages
- Cache rendered markdown in the database, rather than Redis
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods - Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments - Fix VueJS template tags being rendered in code comments
- Added copy file path button to merge request diff files
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps)
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604 - Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu) - Add configurable email subject suffix (Fu Xu)
- Added tooltip to fork count on project show page. (Justin DiPierro)
- Use a ConnectionPool for Rails.cache on Sidekiq servers - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend` - Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users. - Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier) - Preserve label filters when sorting !6136 (Joseph Frazier)
- MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid - Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier) - Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day - Trending projects now only show public projects and the list of projects is cached for a day
- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
...@@ -45,22 +74,44 @@ v 8.13.0 (unreleased) ...@@ -45,22 +74,44 @@ v 8.13.0 (unreleased)
- Add broadcast messages and alerts below sub-nav - Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view - Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile - Add organization field to user profile
- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- Fix deploy status responsiveness error !6633
- Make searching for commits case insensitive
- Fix resolved discussion display in side-by-side diff view !6575 - Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
- Reduce queries needed to find users using their SSH keys when pushing commits - Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted - Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Retouch environments list and deployments list
- Add multiple command support for all label related slash commands !6780 (barthc)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined) - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Allow empty merge requests !6384 (Artem Sidorenko)
- Grouped pipeline dropdown is a scrollable container - Grouped pipeline dropdown is a scrollable container
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
v 8.12.5 (unreleased) - Fix a typo in doc/api/labels.md
- API: all unknown routing will be handled with 404 Not Found
- Make guests unable to view MRs on private projects
v 8.12.6
- Update mailroom to 0.8.1 in Gemfile.lock !6814
v 8.12.5
- Switch from request to env in ::API::Helpers. !6615
- Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread. !6714
- Improve issue load time performance by avoiding ORDER BY in find_by call. !6724
- Add a new gitlab:users:clear_all_authentication_tokens task. !6745
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.12.4 v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell) - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
...@@ -287,6 +338,10 @@ v 8.12.0 ...@@ -287,6 +338,10 @@ v 8.12.0
- Fix non-master branch readme display in tree view - Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs - Add UX improvements for merge request version diffs
v 8.11.9
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.11.8 v 8.11.8
- Respect the fork_project permission when forking projects - Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests - Set a restrictive CORS policy on the API for credentialed requests
...@@ -512,6 +567,10 @@ v 8.11.0 ...@@ -512,6 +567,10 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7 - Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done - Simplify SQL queries of marking a todo as done
v 8.10.12
- Don't send Private-Token (API authentication) headers to Sentry
- Share projects via the API only with groups the authenticated user can access
v 8.10.11 v 8.10.11
- Respect the fork_project permission when forking projects - Respect the fork_project permission when forking projects
- Set a restrictive CORS policy on the API for credentialed requests - Set a restrictive CORS policy on the API for credentialed requests
......
...@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it ...@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting merge requests`. Please include screenshots or to be marked as `Accepting merge requests`. Please include screenshots or
wireframes if the feature will also change the UI. wireframes if the feature will also change the UI.
Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
[github.com][github-mr-tracker].
If you are new to GitLab development (or web development in general), see the If you are new to GitLab development (or web development in general), see the
[I want to contribute!](#i-want-to-contribute) section to get you started with [I want to contribute!](#i-want-to-contribute) section to get you started with
...@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge ...@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows: request is as follows:
1. Fork the project into your personal space on GitLab.com 1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`. 1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG) 1. Add your changes to the [CHANGELOG](CHANGELOG):
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] 1. If you are fixing a ~regression issue, you can add your entry to the next
patch release (e.g. `8.12.5` if current version is `8.12.4`)
1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
current version is `8.12.4`
1. Please add your entry at a random place among the entries of the targeted
release
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by 1. If you have multiple commits please combine them into one commit by
[squashing them][git-squash] [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
...@@ -258,7 +264,7 @@ request is as follows: ...@@ -258,7 +264,7 @@ request is as follows:
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it, see the [merge request description format] used to achieve it, see the [merge request description format]
(#merge-request-description-format) (#merge-request-description-format)
1. If the MR changes the UI it should include before and after screenshots 1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages, 1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R` `grep css-class ./app -R`
1. Link any relevant [issues][ce-tracker] in the merge request description and 1. Link any relevant [issues][ce-tracker] in the merge request description and
...@@ -270,7 +276,9 @@ request is as follows: ...@@ -270,7 +276,9 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md) [shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the 1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md). [shared files guidelines](doc/development/shared_files.md).
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/). 1. When writing commit messages please follow
[these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
[guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all 1. If your merge request adds one or more migrations, make sure to execute all
migrations on a fresh database before the MR is reviewed. If the review leads migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete. to large changes in the MR, do this again once the review is complete.
...@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria ...@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria
When having your code reviewed and when reviewing merge requests please take the When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account. [code review guidelines](doc/development/code_review.md) into account.
### Merge request description format
Please submit merge requests using the following template in the merge request
description area. Copy-paste it to retain the markdown format.
```
## What does this MR do?
## Are there points in the code the reviewer needs to double check?
## Why was this MR needed?
## What are the relevant issue numbers?
## Screenshots (if relevant)
```
### Contribution acceptance criteria ### Contribution acceptance criteria
1. The change is as small as possible 1. The change is as small as possible
...@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format. ...@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format.
aforementioned failing test aforementioned failing test
1. Your MR initially contains a single commit (please use `git rebase -i` to 1. Your MR initially contains a single commit (please use `git rebase -i` to
squash commits) squash commits)
1. Your changes can merge without problems (if not please merge `master`, never 1. Your changes can merge without problems (if not please rebase if you're the
rebase commits pushed to the remote server) only one working on your feature branch, otherwise, merge `master`)
1. Does not break any existing functionality 1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine 1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed) things, send separate merge requests if needed)
...@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format. ...@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings. entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass - Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant. refactoring modifications may leave style non-compliant.
1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error. 1. If the merge request adds any new libraries (gems, JavaScript libraries,
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases ## Changes for Stable Releases
...@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests [accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests [accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests [gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit [gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits [git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed [closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
......
...@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0' ...@@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -323,7 +324,7 @@ gem 'newrelic_rpm', '~> 3.16' ...@@ -323,7 +324,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0' gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.8' gem 'mail_room', '~> 0.8.1'
gem 'email_reply_parser', '~> 0.5.8' gem 'email_reply_parser', '~> 0.5.8'
......
...@@ -399,7 +399,7 @@ GEM ...@@ -399,7 +399,7 @@ GEM
systemu (~> 2.6.2) systemu (~> 2.6.2)
mail (2.6.4) mail (2.6.4)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.8.0) mail_room (0.8.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
...@@ -745,6 +745,9 @@ GEM ...@@ -745,6 +745,9 @@ GEM
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
turbolinks (2.5.3) turbolinks (2.5.3)
coffee-rails coffee-rails
tzinfo (1.2.2) tzinfo (1.2.2)
...@@ -886,7 +889,7 @@ DEPENDENCIES ...@@ -886,7 +889,7 @@ DEPENDENCIES
license_finder (~> 2.1.0) license_finder (~> 2.1.0)
licensee (~> 8.0.0) licensee (~> 8.0.0)
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.8) mail_room (~> 0.8.1)
method_source (~> 0.8) method_source (~> 0.8)
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
...@@ -971,6 +974,7 @@ DEPENDENCIES ...@@ -971,6 +974,7 @@ DEPENDENCIES
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
truncato (~> 0.7.8)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
......
# GitLab # GitLab
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
......
...@@ -21,16 +21,14 @@ ...@@ -21,16 +21,14 @@
}; };
Activities.prototype.toggleFilter = function(sender) { Activities.prototype.toggleFilter = function(sender) {
var event_filters, filter; var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active"); $('.event-filter .active').removeClass("active");
event_filters = $.cookie("event_filter"); $.cookie("event_filter", filter, {
filter = sender.attr("id").split("_")[0];
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
path: gon.relative_url_root || '/' path: gon.relative_url_root || '/'
}); });
if (event_filters !== filter) {
return sender.closest('li').toggleClass("active"); sender.closest('li').toggleClass("active");
}
}; };
return Activities; return Activities;
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
}, },
data () { data () {
return { return {
filters: Store.state.filters filters: Store.state.filters,
showIssueForm: false
}; };
}, },
watch: { watch: {
...@@ -33,6 +34,11 @@ ...@@ -33,6 +34,11 @@
deep: true deep: true
} }
}, },
methods: {
showNewIssueForm() {
this.showIssueForm = !this.showIssueForm;
}
},
ready () { ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
......
...@@ -8,10 +8,8 @@ ...@@ -8,10 +8,8 @@
data () { data () {
return { return {
predefinedLabels: [ predefinedLabels: [
new ListLabel({ title: 'Development', color: '#5CB85C' }), new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Testing', color: '#F0AD4E' }), new ListLabel({ title: 'Doing', color: '#5CB85C' })
new ListLabel({ title: 'Production', color: '#FF5F00' }),
new ListLabel({ title: 'Ready', color: '#FF0000' })
] ]
} }
}, },
......
//= require ./board_card //= require ./board_card
//= require ./board_new_issue
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -8,14 +9,16 @@ ...@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
components: { components: {
'board-card': gl.issueBoards.BoardCard 'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
}, },
props: { props: {
disabled: Boolean, disabled: Boolean,
list: Object, list: Object,
issues: Array, issues: Array,
loading: Boolean, loading: Boolean,
issueLinkBase: String issueLinkBase: String,
showIssueForm: Boolean
}, },
data () { data () {
return { return {
...@@ -73,7 +76,7 @@ ...@@ -73,7 +76,7 @@
group: 'issues', group: 'issues',
sort: false, sort: false,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count', filter: '.board-list-count, .is-disabled',
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
......
(() => {
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
showIssueForm: Boolean
},
data() {
return {
title: '',
error: false
};
},
watch: {
showIssueForm () {
this.$els.input.focus();
}
},
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
labels
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
this.showIssueForm = true;
});
this.cancel();
},
cancel() {
this.showIssueForm = false;
this.title = '';
}
}
});
})();
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging', fallbackClass: 'is-dragging',
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip', filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
......
...@@ -87,6 +87,17 @@ class List { ...@@ -87,6 +87,17 @@ class List {
}); });
} }
newIssue (issue) {
this.addIssue(issue);
this.issuesSize++;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
});
}
createIssues (data) { createIssues (data) {
data.forEach((issueObj) => { data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj)); this.addIssue(new ListIssue(issueObj));
......
...@@ -58,4 +58,10 @@ class BoardService { ...@@ -58,4 +58,10 @@ class BoardService {
to_list_id to_list_id
}); });
} }
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
});
}
}; };
...@@ -126,6 +126,9 @@ ...@@ -126,6 +126,9 @@
new TreeView(); new TreeView();
} }
break; break;
case 'projects:pipelines:show':
new gl.Pipelines();
break;
case 'groups:activity': case 'groups:activity':
new Activities(); new Activities();
break; break;
......
...@@ -52,37 +52,27 @@ ...@@ -52,37 +52,27 @@
} }
} }
}, },
setup: function(input) { setup: _.debounce(function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input. // Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input'); this.input = input || $('.js-gfm-input');
// destroy previous instances // destroy previous instances
this.destroyAtWho(); this.destroyAtWho();
// set up instances // set up instances
this.setupAtWho(); this.setupAtWho();
if (this.dataSource) {
if (!this.dataLoading && !this.cachedData) { if (this.dataSource && !this.dataLoading && !this.cachedData) {
this.dataLoading = true; this.dataLoading = true;
setTimeout((function(_this) { return this.fetchData(this.dataSource)
return function() { .done((data) => {
var fetch; this.dataLoading = false;
fetch = _this.fetchData(_this.dataSource); this.loadData(data);
return fetch.done(function(data) { });
_this.dataLoading = false; };
return _this.loadData(data);
}); if (this.cachedData != null) {
}; return this.loadData(this.cachedData);
// We should wait until initializations are done
// and only trigger the last .setup since
// The previous .dataSource belongs to the previous issuable
// and the last one will have the **proper** .dataSource property
// TODO: Make this a singleton and turn off events when moving to another page
})(this), 1000);
}
if (this.cachedData != null) {
return this.loadData(this.cachedData);
}
} }
}, }, 1000),
setupAtWho: function() { setupAtWho: function() {
// Emoji // Emoji
this.input.atwho({ this.input.atwho({
......
...@@ -738,6 +738,7 @@ ...@@ -738,6 +738,7 @@
return false; return false;
} }
if (currentKeyCode === 13 && currentIndex !== -1) { if (currentKeyCode === 13 && currentIndex !== -1) {
e.preventDefault();
_this.selectRowAtIndex(); _this.selectRowAtIndex();
} }
}; };
......
...@@ -38,6 +38,11 @@ ...@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() { gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0]; return $('body').data('page').split(':')[0];
}; };
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
return jQuery.timefor = function(time, suffix, expiredLabel) { return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor; var suffixFromNow, timefor;
if (!time) { if (!time) {
......
...@@ -61,6 +61,9 @@ ...@@ -61,6 +61,9 @@
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this); this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this); this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this); this.showTab = bind(this.showTab, this);
...@@ -93,7 +96,7 @@ ...@@ -93,7 +96,7 @@
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
} else if (action === 'diffs') { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
...@@ -170,8 +173,9 @@ ...@@ -170,8 +173,9 @@
action = 'notes'; action = 'notes';
} }
this.currentAction = action; this.currentAction = action;
// Remove a trailing '/commits' or '/diffs' // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') { if (action !== 'notes') {
new_state += "/" + action; new_state += "/" + action;
...@@ -210,8 +214,13 @@ ...@@ -210,8 +214,13 @@
if (this.diffsLoaded) { if (this.diffsLoaded) {
return; return;
} }
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({ return this._get({
url: (source + ".json") + this._location.search, url: (url.pathname + ".json") + this._location.search,
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$('#diffs').html(data.html); $('#diffs').html(data.html);
...@@ -223,7 +232,7 @@ ...@@ -223,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') { if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
...@@ -324,6 +333,10 @@ ...@@ -324,6 +333,10 @@
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
}; };
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
};
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
var $wrapper = $('.content-wrapper .container-fluid'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) { if (this.fixedLayoutPref === null) {
......
(function() { ((global) => {
function toggleGraph() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); class Pipelines {
constructor() {
$(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
this.addMarginToBuildColumns();
}
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); toggleGraph() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
}
addMarginToBuildColumns() {
const $secondChildBuildNode = $('.build:nth-child(2)');
if ($secondChildBuildNode.length) {
const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
const $previousColumn = $multiBuildColumn.prev('.stage-column');
$multiBuildColumn.addClass('left-margin');
$firstChildBuildNode.addClass('left-connector');
$previousColumn.each(function() {
$this = $(this);
if ($('.build', $this).length === 1) $this.addClass('no-margin');
});
}
$('.pipeline-graph').removeClass('hidden');
}
} }
$(document).on('click', '.toggle-pipeline-btn', toggleGraph); global.Pipelines = Pipelines;
})();
})(window.gl || (window.gl = {}));
...@@ -89,7 +89,7 @@ content on the Users#show page. ...@@ -89,7 +89,7 @@ content on the Users#show page.
const action = $target.data('action'); const action = $target.data('action');
const source = $target.attr('href'); const source = $target.attr('href');
this.setTab(source, action); this.setTab(source, action);
return this.setCurrentAction(action); return this.setCurrentAction(source, action);
} }
activateTab(action) { activateTab(action) {
...@@ -142,14 +142,9 @@ content on the Users#show page. ...@@ -142,14 +142,9 @@ content on the Users#show page.
.toggle(status); .toggle(status);
} }
setCurrentAction(action) { setCurrentAction(source, action) {
const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`); let new_state = source
let new_state = this._location.pathname;
new_state = new_state.replace(/\/+$/, ''); new_state = new_state.replace(/\/+$/, '');
new_state = new_state.replace(regExp, '');
if (action !== this.defaultAction) {
new_state += `/${action}`;
}
new_state += this._location.search + this._location.hash; new_state += this._location.search + this._location.hash;
history.replaceState({ history.replaceState({
turbolinks: true, turbolinks: true,
......
...@@ -71,8 +71,8 @@ ...@@ -71,8 +71,8 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
}); });
}; };
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
@include border-radius($avatar_radius); border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1); border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline { &.avatar-inline {
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
} }
&.avatar-tile { &.avatar-tile {
@include border-radius(0); border-radius: 0;
border: none; border: none;
} }
......
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
} }
.identicon { .identicon {
@include border-radius(50%); border-radius: 50%;
} }
} }
......
@mixin btn-default { @mixin btn-default {
@include border-radius(3px); border-radius: 3px;
font-size: $gl-font-size; font-size: $gl-font-size;
font-weight: 500; font-weight: 500;
padding: $gl-vert-padding $gl-btn-padding; padding: $gl-vert-padding $gl-btn-padding;
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
&:active { &:active {
outline: none; outline: none;
background-color: $btn-active-gray; background-color: $btn-active-gray;
@include box-shadow($gl-btn-active-background); box-shadow: $gl-btn-active-background;
} }
} }
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
&:active, &:active,
&.active { &.active {
@include box-shadow ($gl-btn-active-background); box-shadow: $gl-btn-active-background;
background-color: $dark; background-color: $dark;
border-color: $border-dark; border-color: $border-dark;
...@@ -194,10 +194,17 @@ ...@@ -194,10 +194,17 @@
pointer-events: none !important; pointer-events: none !important;
} }
.caret { .fa-caret-down,
.fa-caret-up {
margin-left: 5px; margin-left: 5px;
} }
&.dropdown-toggle {
.fa-caret-down {
margin-left: 3px;
}
}
svg { svg {
height: 15px; height: 15px;
width: 15px; width: 15px;
...@@ -272,7 +279,7 @@ ...@@ -272,7 +279,7 @@
} }
.active { .active {
@include box-shadow($gl-btn-active-background); box-shadow: $gl-btn-active-background;
border: 1px solid #c6cacf !important; border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important; background-color: #e4e7ed !important;
......
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
}
.btn-group {
.caret {
margin-left: 0;
}
}
.dropdown { .dropdown {
position: relative; position: relative;
......
...@@ -73,7 +73,7 @@ label { ...@@ -73,7 +73,7 @@ label {
} }
.form-control { .form-control {
@include box-shadow(none); box-shadow: none;
border-radius: 3px; border-radius: 3px;
padding: $gl-vert-padding $gl-input-padding; padding: $gl-vert-padding $gl-input-padding;
} }
...@@ -81,10 +81,10 @@ label { ...@@ -81,10 +81,10 @@ label {
.select-wrapper { .select-wrapper {
position: relative; position: relative;
.caret { .fa-caret-down {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: $gl-padding; top: 10px;
color: $gray-darkest; color: $gray-darkest;
pointer-events: none; pointer-events: none;
} }
......
...@@ -57,6 +57,10 @@ header { ...@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active { &:hover, &:focus, &:active {
background-color: $background-color; background-color: $background-color;
} }
.fa-caret-down {
font-size: 15px;
}
} }
.navbar-toggle { .navbar-toggle {
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
margin-top: 5px; margin-top: 5px;
} }
@include border-radius(3px); border-radius: 3px;
display: block; display: block;
float: left; float: left;
margin-right: 10px; margin-right: 10px;
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
} }
.markdown-area { .markdown-area {
@include border-radius(0); border-radius: 0;
background: #fff; background: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
min-height: 140px; min-height: 140px;
......
/**
* Generic mixins
*/
@mixin box-shadow($shadow) {
box-shadow: $shadow;
}
@mixin border-radius($radius) {
border-radius: $radius;
}
/** /**
* Prefilled mixins * Prefilled mixins
* Mixins with fixed values * Mixins with fixed values
......
...@@ -133,5 +133,5 @@ ...@@ -133,5 +133,5 @@
font-size: 20px; font-size: 20px;
color: #777; color: #777;
z-index: 100; z-index: 100;
@include box-shadow(0 1px 2px #ddd); box-shadow: 0 1px 2px #ddd;
} }
...@@ -21,7 +21,14 @@ ...@@ -21,7 +21,14 @@
padding-right: 10px; padding-right: 10px;
b { b {
@extend .caret; display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
color: $gray-darkest; color: $gray-darkest;
} }
} }
...@@ -39,8 +46,8 @@ ...@@ -39,8 +46,8 @@
} }
.select2-drop { .select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
@include border-radius ($border-radius-default); border-radius: $border-radius-default;
border: none; border: none;
min-width: 175px; min-width: 175px;
} }
...@@ -65,7 +72,7 @@ ...@@ -65,7 +72,7 @@
.select2-container-active { .select2-container-active {
.select2-choice, .select2-choices { .select2-choice, .select2-choices {
@include box-shadow(none); box-shadow: none;
} }
} }
...@@ -75,13 +82,13 @@ ...@@ -75,13 +82,13 @@
outline: 0; outline: 0;
background-image: none; background-image: none;
background-color: $white-dark; background-color: $white-dark;
@include box-shadow($gl-btn-active-gradient); box-shadow: $gl-btn-active-gradient;
} }
} }
.select2-container-multi { .select2-container-multi {
.select2-choices { .select2-choices {
@include border-radius($border-radius-default); border-radius: $border-radius-default;
border-color: $input-border; border-color: $input-border;
background: none; background: none;
...@@ -116,7 +123,7 @@ ...@@ -116,7 +123,7 @@
&.select2-container-active .select2-choices, &.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices { &.select2-dropdown-open .select2-choices {
border-color: $border-white-normal; border-color: $border-white-normal;
@include box-shadow($gl-btn-active-gradient); box-shadow: $gl-btn-active-gradient;
} }
} }
...@@ -150,7 +157,7 @@ ...@@ -150,7 +157,7 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 0 bottom 6px; background-position: right 0 bottom 6px;
border: 1px solid $input-border; border: 1px solid $input-border;
@include border-radius($border-radius-default); border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus { &:focus {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
&.page-sidebar-pinned { &.page-sidebar-pinned {
.sidebar-wrapper { .sidebar-wrapper {
@include box-shadow(none); box-shadow: none;
} }
} }
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
width: 0; width: 0;
overflow: hidden; overflow: hidden;
transition: width $sidebar-transition-duration; transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent); box-shadow: 2px 0 16px 0 $black-transparent;
} }
} }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
.count { .count {
float: right; float: right;
padding: 0 8px; padding: 0 8px;
@include border-radius(6px); border-radius: 6px;
} }
} }
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
font-size: 13px; font-size: 13px;
line-height: 1.6em; line-height: 1.6em;
overflow-x: auto; overflow-x: auto;
@include border-radius(2px); border-radius: 2px;
} }
p > code { p > code {
......
...@@ -17,8 +17,10 @@ $white-normal: #ededed; ...@@ -17,8 +17,10 @@ $white-normal: #ededed;
$white-dark: #ececec; $white-dark: #ececec;
$gray-light: #fafafa; $gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5; $gray-normal: #f5f5f5;
$gray-dark: #ededed; $gray-dark: #ededed;
$gray-darker: #eee;
$gray-darkest: #c9c9c9; $gray-darkest: #c9c9c9;
$green-light: #38ae67; $green-light: #38ae67;
...@@ -33,6 +35,8 @@ $blue-medium-light: #3498cb; ...@@ -33,6 +35,8 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf; $blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4; $blue-medium-dark: #2d86b4;
$blue-light-transparent: rgba(44, 159, 216, 0.05);
$orange-light: #fc8a51; $orange-light: #fc8a51;
$orange-normal: #e75e40; $orange-normal: #e75e40;
$orange-dark: #ce5237; $orange-dark: #ce5237;
...@@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f; ...@@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f;
$gl-font-size: 15px; $gl-font-size: 15px;
$gl-title-color: #333; $gl-title-color: #333;
$gl-text-color: #5c5c5c; $gl-text-color: #5c5c5c;
$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2; $gl-text-green: #4a2;
$gl-text-red: #d12f19; $gl-text-red: #d12f19;
$gl-text-orange: #d90; $gl-text-orange: #d90;
......
...@@ -162,6 +162,10 @@ lex ...@@ -162,6 +162,10 @@ lex
list-style: none; list-style: none;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
&.is-smaller {
height: calc(100% - 185px);
}
} }
.board-list-loading { .board-list-loading {
...@@ -233,3 +237,31 @@ lex ...@@ -233,3 +237,31 @@ lex
margin-right: 5px; margin-right: 5px;
} }
} }
.board-new-issue-form {
margin: 5px;
}
.board-issue-count-holder {
margin-top: -3px;
.btn {
line-height: 12px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.board-issue-count {
padding-right: 10px;
padding-left: 10px;
line-height: 21px;
border-radius: $border-radius-base;
border: 1px solid $border-color;
&.has-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-width: 1px 0 1px 1px;
}
}
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
.bordered-box { .bordered-box {
border: 1px solid $border-color; border: 1px solid $border-color;
@include border-radius($border-radius-default); border-radius: $border-radius-default;
} }
......
.file-editor { .file-editor {
#editor { #editor {
border: none; border: none;
@include border-radius(0); border-radius: 0;
height: 500px; height: 500px;
margin: 0; margin: 0;
padding: 0; padding: 0;
......
.environments-container,
.deployments-container {
width: 100%;
overflow: auto;
}
.environments { .environments {
.deployment-column {
.avatar {
float: none;
}
}
.commit-title { .commit-title {
margin: 0; margin: 0;
...@@ -9,6 +20,7 @@ ...@@ -9,6 +20,7 @@
width: 12px; width: 12px;
} }
.external-url,
.dropdown-new { .dropdown-new {
color: $table-text-gray; color: $table-text-gray;
} }
...@@ -21,16 +33,35 @@ ...@@ -21,16 +33,35 @@
} }
} }
.build-link,
.branch-name { .branch-name {
color: $gl-dark-link-color; color: $gl-dark-link-color;
} }
.deployment {
.build-column {
.build-link {
color: $gl-dark-link-color;
}
.avatar {
float: none;
}
}
}
} }
.table.builds.environments { .table.builds.environments {
min-width: 500px;
.icon-container { .icon-container {
width: 20px; width: 20px;
text-align: center; text-align: center;
} }
.branch-commit {
.commit-id {
margin-right: 0;
}
}
} }
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
float: right; float: right;
border: 1px solid #eee; border: 1px solid #eee;
padding: 5px; padding: 5px;
@include border-radius(5px); border-radius: 5px;
background: $gray-light; background: $gray-light;
margin-left: 10px; margin-left: 10px;
top: -6px; top: -6px;
......
.suggest-colors { .suggest-colors {
margin-top: 5px; margin-top: 5px;
a { a {
@include border-radius(4px); border-radius: 4px;
width: 30px; width: 30px;
height: 30px; height: 30px;
display: inline-block; display: inline-block;
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
overflow: hidden; overflow: hidden;
a { a {
@include border-radius(0); border-radius: 0;
width: (100% / 7); width: (100% / 7);
margin-right: 0; margin-right: 0;
margin-bottom: -5px; margin-bottom: -5px;
...@@ -59,6 +59,13 @@ ...@@ -59,6 +59,13 @@
width: 200px; width: 200px;
margin-bottom: 0; margin-bottom: 0;
} }
.label {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
} }
.label-description { .label-description {
......
...@@ -73,12 +73,12 @@ ...@@ -73,12 +73,12 @@
height: auto; height: auto;
&.top { &.top {
@include border-radius(5px 5px 0 0); border-radius: 5px 5px 0 0;
margin-bottom: 0; margin-bottom: 0;
} }
&.bottom { &.bottom {
@include border-radius(0 0 5px 5px); border-radius: 0 0 5px 5px;
border-top: 0; border-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
} }
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
&.middle { &.middle {
border-top: 0; border-top: 0;
margin-bottom: 0; margin-bottom: 0;
@include border-radius(0); border-radius: 0;
} }
&:active, &:focus { &:active, &:focus {
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
background: $background-color; background: $background-color;
color: $gl-gray; color: $gl-gray;
border: 1px solid $border-color; border: 1px solid $border-color;
@include border-radius(2px); border-radius: 2px;
form { form {
margin-bottom: 0; margin-bottom: 0;
...@@ -204,6 +204,18 @@ ...@@ -204,6 +204,18 @@
word-break: break-all; word-break: break-all;
} }
.commits-empty {
text-align: center;
h4 {
padding-top: 20px;
padding-bottom: 10px;
}
svg {
width: 230px;
}
}
.mr-list { .mr-list {
.merge-request { .merge-request {
padding: 10px 15px; padding: 10px 15px;
......
...@@ -334,7 +334,7 @@ ul.notes { ...@@ -334,7 +334,7 @@ ul.notes {
.add-diff-note { .add-diff-note {
margin-top: -4px; margin-top: -4px;
@include border-radius(40px); border-radius: 40px;
background: #fff; background: #fff;
padding: 4px; padding: 4px;
font-size: 16px; font-size: 16px;
......
...@@ -229,9 +229,12 @@ ...@@ -229,9 +229,12 @@
.fa { .fa {
color: $table-text-gray; color: $table-text-gray;
margin-right: 6px;
font-size: 14px; font-size: 14px;
} }
svg, .fa {
margin-right: 0;
}
} }
.btn-remove { .btn-remove {
...@@ -272,18 +275,8 @@ ...@@ -272,18 +275,8 @@
.toggle-pipeline-btn { .toggle-pipeline-btn {
background-color: $gray-dark; background-color: $gray-dark;
.caret {
border-top: none;
border-bottom: 4px solid;
}
&.graph-collapsed { &.graph-collapsed {
background-color: $white-light; background-color: $white-light;
.caret {
border-bottom: none;
border-top: 4px solid;
}
} }
} }
...@@ -310,16 +303,41 @@ ...@@ -310,16 +303,41 @@
.stage-column { .stage-column {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-right: 65px;
&:not(:last-child) {
margin-right: 44px;
}
&.left-margin {
&:not(:first-child) {
margin-left: 44px;
.left-connector {
&::before {
content: '';
position: absolute;
top: 48%;
left: -48px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
}
}
}
}
&.no-margin {
margin: 0;
}
li { li {
list-style: none; list-style: none;
} }
.stage-name { .stage-name {
margin-bottom: 15px; margin: 0 0 15px 10px;
font-weight: bold; font-weight: bold;
width: 150px; width: 176px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -328,17 +346,23 @@ ...@@ -328,17 +346,23 @@
.build { .build {
border: 1px solid $border-color; border: 1px solid $border-color;
position: relative; position: relative;
padding: 6px 10px; padding: 7px 10px 8px;
border-radius: 30px; border-radius: 30px;
width: 150px; width: 186px;
margin-bottom: 10px; margin-bottom: 10px;
&:hover {
background-color: $gray-lighter;
.dropdown-menu-toggle {
background-color: transparent;
}
}
&.playable { &.playable {
background-color: $gray-light;
svg { svg {
height: 12px; height: 13px;
width: 12px; width: 20px;
position: relative; position: relative;
top: 1px; top: 1px;
...@@ -349,10 +373,20 @@ ...@@ -349,10 +373,20 @@
} }
.build-content { .build-content {
width: 130px; display: -ms-flexbox;
display: -webkit-flex;
display: flex;
width: 164px;
.ci-status-icon {
svg {
height: 20px;
width: 20px;
}
}
.ci-status-text { .ci-status-text {
width: 110px; width: 135px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -363,44 +397,53 @@ ...@@ -363,44 +397,53 @@
} }
a { a {
color: $layout-link-gray; color: $gl-text-color-light;
text-decoration: none; text-decoration: none;
&:hover {
.ci-status-text {
text-decoration: underline;
}
}
} }
.dropdown-menu-toggle { .dropdown-menu-toggle {
border: none; border: none;
width: auto; width: auto;
padding: 0; padding: 0;
color: $layout-link-gray; color: $gl-text-color-light;
flex-grow: 1;
.ci-status-text { .ci-status-text {
width: 80px; max-width: 112px;
width: auto;
} }
} }
.grouped-pipeline-dropdown { .grouped-pipeline-dropdown {
padding: 8px 0; padding: 8px 0;
width: 200px; width: 186px;
left: auto; left: auto;
right: -214px; right: -197px;
top: -9px; top: -9px;
max-height: 245px; max-height: 245px;
overflow-y: scroll; overflow-y: scroll;
a:hover { a {
.ci-status-text { color: $gl-text-color;
text-decoration: none; padding: 7px 8px 8px;
&:hover {
background-color: $blue-light-transparent;
border-radius: 3px;
.ci-status-text {
text-decoration: none;
}
} }
} }
svg {
width: 14px;
height: 14px;
}
.ci-status-text { .ci-status-text {
width: 145px; width: 112px;
} }
.arrow { .arrow {
...@@ -433,9 +476,10 @@ ...@@ -433,9 +476,10 @@
} }
.badge { .badge {
background-color: $gray-dark; background-color: $gray-darker;
color: $layout-link-gray; color: $gl-text-color-light;
font-weight: normal; font-weight: normal;
margin-left: $btn-xs-side-margin;
} }
} }
...@@ -449,10 +493,10 @@ ...@@ -449,10 +493,10 @@
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
top: 50%; top: 48%;
right: -69px; right: -48px;
border-top: 2px solid $border-color; border-top: 2px solid $border-color;
width: 69px; width: 48px;
height: 1px; height: 1px;
} }
} }
...@@ -461,25 +505,25 @@ ...@@ -461,25 +505,25 @@
&:not(:first-child) { &:not(:first-child) {
&::after, &::before { &::after, &::before {
content: ''; content: '';
top: -47px; top: -49px;
position: absolute; position: absolute;
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
width: 20px; width: 25px;
height: 65px; height: 69px;
} }
// Right connecting curves // Right connecting curves
&::after { &::after {
right: -20px; right: -25px;
border-right: 2px solid $border-color; border-right: 2px solid $border-color;
border-radius: 0 0 15px; border-radius: 0 0 20px;
} }
// Left connecting curves // Left connecting curves
&::before { &::before {
left: -20px; left: -25px;
border-left: 2px solid $border-color; border-left: 2px solid $border-color;
border-radius: 0 0 0 15px; border-radius: 0 0 0 20px;
} }
} }
...@@ -487,7 +531,7 @@ ...@@ -487,7 +531,7 @@
&:nth-child(2) { &:nth-child(2) {
&::after, &::before { &::after, &::before {
height: 29px; height: 29px;
top: -10px; top: -9px;
} }
.curve { .curve {
display: block; display: block;
...@@ -545,20 +589,20 @@ ...@@ -545,20 +589,20 @@
width: 21px; width: 21px;
height: 25px; height: 25px;
position: absolute; position: absolute;
top: -29px; top: -32px;
border-top: 2px solid $border-color; border-top: 2px solid $border-color;
} }
&::after { &::after {
left: -39px; left: -44px;
border-right: 2px solid $border-color; border-right: 2px solid $border-color;
border-radius: 0 15px; border-radius: 0 20px;
} }
&::before { &::before {
right: -39px; right: -44px;
border-left: 2px solid $border-color; border-left: 2px solid $border-color;
border-radius: 15px 0 0; border-radius: 20px 0 0;
} }
} }
} }
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
.profile-user-bio { .profile-user-bio {
// Limits the width of the user bio for readability. // Limits the width of the user bio for readability.
max-width: 600px; max-width: 600px;
margin: 15px auto 0; margin: 10px auto;
padding: 0 16px; padding: 0 16px;
} }
...@@ -213,29 +213,22 @@ ...@@ -213,29 +213,22 @@
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
} }
.profile-header { .profile-header {
margin: 0 auto; margin: 0 auto;
.avatar-holder { .avatar-holder {
width: 90px; width: 90px;
display: inline-block; margin: 0 auto 10px;
}
.user-info {
display: inline-block;
text-align: left;
vertical-align: middle;
margin-left: 15px;
.handle {
color: $gl-gray-light;
}
.member-date {
margin-bottom: 4px;
}
} }
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
} }
...@@ -258,10 +251,6 @@ ...@@ -258,10 +251,6 @@
} }
} }
.user-profile-nav {
margin-top: 15px;
}
table.u2f-registrations { table.u2f-registrations {
th:not(:last-child), td:not(:last-child) { th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent; border-right: solid 1px transparent;
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
text-align: center; text-align: center;
.preview { .preview {
@include border-radius(4px); border-radius: 4px;
height: 80px; height: 80px;
margin-bottom: 10px; margin-bottom: 10px;
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
width: 160px; width: 160px;
img { img {
@include border-radius(4px); border-radius: 4px;
max-width: 100%; max-width: 100%;
} }
......
...@@ -354,7 +354,7 @@ a.deploy-project-label { ...@@ -354,7 +354,7 @@ a.deploy-project-label {
justify-content: flex-start; justify-content: flex-start;
.fork-thumbnail { .fork-thumbnail {
@include border-radius($border-radius-base); border-radius: $border-radius-base;
background-color: $white-light; background-color: $white-light;
border: 1px solid $border-white-light; border: 1px solid $border-white-light;
height: 202px; height: 202px;
...@@ -371,7 +371,7 @@ a.deploy-project-label { ...@@ -371,7 +371,7 @@ a.deploy-project-label {
background-color: $gray-light; background-color: $gray-light;
border: 1px solid $gray-dark; border: 1px solid $gray-dark;
margin: 0 auto; margin: 0 auto;
@include border-radius(50%); border-radius: 50%;
i { i {
font-size: 100px; font-size: 100px;
color: $gray-dark; color: $gray-dark;
...@@ -390,7 +390,7 @@ a.deploy-project-label { ...@@ -390,7 +390,7 @@ a.deploy-project-label {
} }
img { img {
@include border-radius(50%); border-radius: 50%;
max-width: 100px; max-width: 100px;
} }
} }
...@@ -496,7 +496,7 @@ pre.light-well { ...@@ -496,7 +496,7 @@ pre.light-well {
} }
.light-well { .light-well {
@include border-radius (2px); border-radius: 2px;
color: #5b6169; color: #5b6169;
font-size: 13px; font-size: 13px;
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
margin-right: 10px; margin-right: 10px;
border: 1px solid #eee; border: 1px solid #eee;
white-space: nowrap; white-space: nowrap;
@include border-radius(4px); border-radius: 4px;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
......
...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end end
def preview def preview
@message = broadcast_message_params[:message] @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end end
protected protected
......
...@@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base ...@@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base
end end
def event_filter def event_filter
filters = cookies['event_filter'].split(',') if cookies['event_filter'].present? # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
@event_filter ||= EventFilter.new(filters) @event_filter ||= EventFilter.new(filters)
end end
......
module Ci
class ApplicationController < ::ApplicationController
def self.railtie_helpers_paths
"app/helpers/ci"
end
end
end
module Ci module Ci
class LintsController < ApplicationController class LintsController < ::ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
def show def show
......
module Ci module Ci
class ProjectsController < Ci::ApplicationController class ProjectsController < ::ApplicationController
before_action :project before_action :project
before_action :no_cache, only: [:badge] before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index] before_action :authorize_read_project!, except: [:badge, :index]
......
...@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = TrendingProjectsFinder.new.execute @projects = filter_projects(Project.trending)
@projects = filter_projects(@projects)
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
......
class NamespacesController < ApplicationController
skip_before_action :authenticate_user!
def show
namespace = Namespace.find_by(path: params[:id])
if namespace
if namespace.is_a?(Group)
group = namespace
else
user = namespace.owner
end
end
if user
redirect_to user_path(user)
elsif group && can?(current_user, :read_group, group)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
else
render_404
end
end
end
...@@ -2,6 +2,7 @@ module Projects ...@@ -2,6 +2,7 @@ module Projects
module Boards module Boards
class IssuesController < Boards::ApplicationController class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index] before_action :authorize_read_issue!, only: [:index]
before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update] before_action :authorize_update_issue!, only: [:update]
def index def index
...@@ -9,16 +10,23 @@ module Projects ...@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page]) issues = issues.page(params[:page])
render json: { render json: {
issues: issues.as_json( issues: serialize_as_json(issues),
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}),
size: issues.total_count size: issues.total_count
} }
end end
def create
list = project.board.lists.find(params[:list_id])
service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
issue = service.execute(list)
if issue.valid?
render json: serialize_as_json(issue)
else
render json: issue.errors, status: :unprocessable_entity
end
end
def update def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params) service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
...@@ -43,6 +51,10 @@ module Projects ...@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project) return render_403 unless can?(current_user, :read_issue, project)
end end
def authorize_create_issue!
return render_403 unless can?(current_user, :admin_issue, project)
end
def authorize_update_issue! def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue) return render_403 unless can?(current_user, :update_issue, issue)
end end
...@@ -54,6 +66,19 @@ module Projects ...@@ -54,6 +66,19 @@ module Projects
def move_params def move_params
params.permit(:id, :from_list_id, :to_list_id) params.permit(:id, :from_list_id, :to_list_id)
end end
def issue_params
params.require(:issue).permit(:title).merge(request: request)
end
def serialize_as_json(resource)
resource.as_json(
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
end end
end end
end end
...@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def issue def issue
@noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
......
...@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
] ]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
...@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -29,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -29,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request # Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
def index def index
...@@ -210,29 +214,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -210,29 +214,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def new def new
apply_diff_view_cookie! define_new_vars
end
build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)). def new_diffs
group(:commit_id).count respond_to do |format|
format.html do
define_new_vars
render "new"
end
format.json do
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
end
end
end end
def create def create
...@@ -355,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -355,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render layout: false render layout: false
end end
def assign_related_issues
result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
respond_to do |format|
format.html do
case result[:count]
when 0
flash[:error] = "Failed to assign you issues related to the merge request"
when 1
flash[:notice] = "1 issue has been assigned to you"
else
flash[:notice] = "#{result[:count]} issues have been assigned to you"
end
redirect_to(merge_request_path(@merge_request))
end
end
end
def ci_status def ci_status
pipeline = @merge_request.pipeline pipeline = @merge_request.pipeline
if pipeline if pipeline
...@@ -490,6 +510,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -490,6 +510,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
def invalid_mr def invalid_mr
# Render special view for MR with removed target branch # Render special view for MR with removed target branch
render 'invalid' render 'invalid'
...@@ -521,7 +562,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -521,7 +562,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end end
def compared_diff_version def compared_diff_version
......
...@@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController
def show def show
@tag = @repository.find_tag(params[:id]) @tag = @repository.find_tag(params[:id])
return render_404 unless @tag
@release = @project.releases.find_or_initialize_by(tag: @tag.name) @release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.target) @commit = @repository.commit(@tag.target)
end end
......
class SnippetsController < ApplicationController class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
# Allow read snippet # Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw] before_action :authorize_read_snippet!, only: [:show, :raw, :download]
# Allow modify snippet # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_update_snippet!, only: [:edit, :update]
...@@ -12,7 +12,7 @@ class SnippetsController < ApplicationController ...@@ -12,7 +12,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet # Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy] before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw] skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
layout 'snippets' layout 'snippets'
respond_to :html respond_to :html
...@@ -75,6 +75,14 @@ class SnippetsController < ApplicationController ...@@ -75,6 +75,14 @@ class SnippetsController < ApplicationController
) )
end end
def download
send_data(
@snippet.content,
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
protected protected
def snippet def snippet
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder
# current_user - The currently logged in User, if any.
# last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end
private
def cache_key_for(months)
"trending_projects/#{months}"
end
end
...@@ -16,7 +16,7 @@ module AppearancesHelper ...@@ -16,7 +16,7 @@ module AppearancesHelper
end end
def brand_text def brand_text
markdown(brand_item.description) markdown_field(brand_item, :description)
end end
def brand_item def brand_item
......
...@@ -11,18 +11,6 @@ module ApplicationSettingsHelper ...@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled? current_application_settings.signin_enabled?
end end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def after_sign_up_text
current_application_settings.after_sign_up_text
end
def shared_runners_text
current_application_settings.shared_runners_text
end
def user_oauth_applications? def user_oauth_applications?
current_application_settings.user_oauth_applications current_application_settings.user_oauth_applications
end end
......
...@@ -4,15 +4,18 @@ module AvatarsHelper ...@@ -4,15 +4,18 @@ module AvatarsHelper
user: commit_or_event.author, user: commit_or_event.author,
user_name: commit_or_event.author_name, user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email, user_email: commit_or_event.author_email,
css_class: 'hidden-xs'
})) }))
end end
def user_avatar(options = {}) def user_avatar(options = {})
avatar_size = options[:size] || 16 avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
avatar = image_tag( avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size), avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}", class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name, title: user_name,
data: { container: 'body' } data: { container: 'body' }
......
...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper ...@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present? return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message.message) icon('bullhorn') << ' ' << render_broadcast_message(message)
end end
end end
...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper ...@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end end
end end
def render_broadcast_message(message) def render_broadcast_message(broadcast_message)
Banzai.render(message, pipeline: :broadcast_message).html_safe Banzai.render_field(broadcast_message, :message).html_safe
end end
end end
...@@ -15,10 +15,11 @@ module ButtonHelper ...@@ -15,10 +15,11 @@ module ButtonHelper
# #
# See http://clipboardjs.com/#usage # See http://clipboardjs.com/#usage
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
class: "btn btn-clipboard", class: "btn #{css_class}",
data: data, data: data,
type: :button, type: :button,
title: "Copy to Clipboard" title: "Copy to Clipboard"
......
...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper ...@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return "" if body.blank?
escaped_body = if body.start_with?('<img') context = {
body project: @project,
else current_user: (current_user if defined?(current_user)),
escape_once(body) pipeline: :single_line,
end }
gfm_body = Banzai.render(body, context)
user = current_user if defined?(current_user)
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a' if fragment.children.size == 1 && fragment.children[0].name == 'a'
...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper ...@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project context[:project] ||= @project
html = Banzai.render(text, context) html = Banzai.render(text, context)
banzai_postprocess(html, context)
end
context.merge!( def markdown_field(object, field)
current_user: (current_user if defined?(current_user)), object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present?
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
end end
def asciidoc(text) def asciidoc(text)
...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper ...@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon]) icon(options[:icon])
end end
end end
# Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context)
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
project_wiki: @project_wiki,
ref: @ref
)
Banzai.post_process(html, context)
end
end end
...@@ -113,14 +113,13 @@ module IssuesHelper ...@@ -113,14 +113,13 @@ module IssuesHelper
end end
end end
def award_user_list(awards, current_user) def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award| names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name award.user == current_user ? 'You' : award.user.name
end end
# Take first 9 OR current user + first 9
current_user_name = names.delete('You') current_user_name = names.delete('You')
names = names.first(9).insert(0, current_user_name).compact names = names.insert(0, current_user_name).compact.first(limit)
names << "#{awards.size - names.size} more." if awards.size > names.size names << "#{awards.size - names.size} more." if awards.size > names.size
......
...@@ -72,6 +72,19 @@ module MergeRequestsHelper ...@@ -72,6 +72,19 @@ module MergeRequestsHelper
) )
end end
def mr_assign_issues_link
issues = MergeRequests::AssignIssuesService.new(@project,
current_user,
merge_request: @merge_request,
closes_issues: mr_closes_issues
).assignable_issues
path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
if issues.present?
pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
end
end
def source_branch_with_namespace(merge_request) def source_branch_with_namespace(merge_request)
branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
......
...@@ -153,8 +153,18 @@ module SearchHelper ...@@ -153,8 +153,18 @@ module SearchHelper
search_path(options) search_path(options)
end end
# Sanitize html generated after parsing markdown from issue description or comment # Sanitize a HTML field for search display. Most tags are stripped out and the
def search_md_sanitize(html) # maximum length is set to 200 characters.
def search_md_sanitize(object, field)
html = markdown_field(object, field)
html = Truncato.truncate(
html,
count_tags: false,
count_tail: false,
max_length: 200
)
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code)) sanitize(html, tags: %w(a p ol ul li pre code))
end end
end end
class AbuseReport < ActiveRecord::Base class AbuseReport < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :message, pipeline: :single_line
belongs_to :reporter, class_name: 'User' belongs_to :reporter, class_name: 'User'
belongs_to :user belongs_to :user
...@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base ...@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' } validates :user_id, uniqueness: { message: 'has already been reported' }
# For CacheMarkdownField
alias_method :author, :reporter
def remove_user(deleted_by:) def remove_user(deleted_by:)
user.block user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
......
class Appearance < ActiveRecord::Base class Appearance < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
validates :title, presence: true validates :title, presence: true
validates :description, presence: true validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte } validates :logo, file_size: { maximum: 1.megabyte }
......
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
...@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array serialize :domain_whitelist, Array
serialize :domain_blacklist, Array serialize :domain_blacklist, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
cache_markdown_field :after_sign_up_text
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay, validates :session_expire_delay,
......
class BroadcastMessage < ActiveRecord::Base class BroadcastMessage < ActiveRecord::Base
include CacheMarkdownField
include Sortable include Sortable
cache_markdown_field :message, pipeline: :broadcast_message
validates :message, presence: true validates :message, presence: true
validates :starts_at, presence: true validates :starts_at, presence: true
validates :ends_at, presence: true validates :ends_at, presence: true
......
class CommitStatus < ActiveRecord::Base class CommitStatus < ActiveRecord::Base
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue
self.table_name = 'ci_builds' self.table_name = 'ci_builds'
...@@ -85,21 +86,24 @@ class CommitStatus < ActiveRecord::Base ...@@ -85,21 +86,24 @@ class CommitStatus < ActiveRecord::Base
end end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
commit_status.pipeline.try do |pipeline| return if transition.loopback?
break if transition.loopback?
commit_status.run_after_commit do
if commit_status.complete? pipeline.try do |pipeline|
PipelineProcessWorker.perform_async(pipeline.id) if complete?
PipelineProcessWorker.perform_async(pipeline.id)
else
PipelineUpdateWorker.perform_async(pipeline.id)
end
end end
PipelineUpdateWorker.perform_async(pipeline.id)
end end
true
end end
after_transition any => :failed do |commit_status| after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
.new(pipeline.project, nil).execute(self)
end
end end
end end
......
# This module takes care of updating cache columns for Markdown-containing
# fields. Use like this in the body of your class:
#
# include CacheMarkdownField
# cache_markdown_field :foo
# cache_markdown_field :bar
# cache_markdown_field :baz, pipeline: :single_line
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
extend Forwardable
def initialize
@data = {}
end
def_delegators :@data, :[], :[]=
def_delegator :@data, :keys, :markdown_fields
def html_field(markdown_field)
"#{markdown_field}_html"
end
def html_fields
markdown_fields.map {|field| html_field(field) }
end
end
# Dynamic registries don't really work in Rails as it's not guaranteed that
# every class will be loaded, so hardcode the list.
CACHING_CLASSES = %w[
AbuseReport
Appearance
ApplicationSetting
BroadcastMessage
Issue
Label
MergeRequest
Milestone
Namespace
Note
Project
Release
Snippet
]
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
extend ActiveSupport::Concern
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
context
end
# Allow callers to look up the cache field name, rather than hardcoding it
def markdown_cache_field_for(field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(field)
cached_markdown_fields.html_field(field)
end
# Always exclude _html fields from attributes (including serialization).
# They contain unredacted HTML, which would be a security issue
alias_method :attributes_before_markdown_cache, :attributes
def attributes
attrs = attributes_before_markdown_cache
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
end
class_methods do
private
# Specify that a field is markdown. Its rendered output will be cached in
# a corresponding _html field. Any custom rendering options may be provided
# as a context.
def cache_markdown_field(markdown_field, context = {})
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field)
cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
__send__("#{html_field}=", html)
true
end
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
!invalidations.empty?
end
before_save cache_method, if: invalidation_method
end
end
end
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include CacheMarkdownField
include Participable include Participable
include Mentionable include Mentionable
include Subscribable include Subscribable
...@@ -13,6 +14,9 @@ module Issuable ...@@ -13,6 +14,9 @@ module Issuable
include Awardable include Awardable
included do included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
......
...@@ -68,8 +68,10 @@ class Event < ActiveRecord::Base ...@@ -68,8 +68,10 @@ class Event < ActiveRecord::Base
true true
elsif issue? || issue_note? elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target) Ability.allowed?(user, :read_issue, note? ? note_target : target)
elsif merge_request? || merge_request_note?
Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
else else
((merge_request? || note?) && target.present?) || milestone? milestone?
end end
end end
...@@ -280,6 +282,10 @@ class Event < ActiveRecord::Base ...@@ -280,6 +282,10 @@ class Event < ActiveRecord::Base
note? && target && target.for_issue? note? && target && target.for_issue?
end end
def merge_request_note?
note? && target && target.for_merge_request?
end
def project_snippet_note? def project_snippet_note?
target.for_snippet? target.for_snippet?
end end
...@@ -335,7 +341,7 @@ class Event < ActiveRecord::Base ...@@ -335,7 +341,7 @@ class Event < ActiveRecord::Base
# update the project. Only one query should actually perform the update, # update the project. Only one query should actually perform the update,
# hence we add the extra WHERE clause for last_activity_at. # hence we add the extra WHERE clause for last_activity_at.
Project.unscoped.where(id: project_id). Project.unscoped.where(id: project_id).
where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
update_all(last_activity_at: created_at) update_all(last_activity_at: created_at)
end end
......
...@@ -4,6 +4,10 @@ class GlobalLabel ...@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label delegate :color, :description, to: :@first_label
def for_display
@first_label
end
def self.build_collection(labels) def self.build_collection(labels)
labels = labels.group_by(&:title) labels = labels.group_by(&:title)
......
...@@ -4,6 +4,10 @@ class GlobalMilestone ...@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
def for_display
@first_milestone
end
def self.build_collection(milestones) def self.build_collection(milestones)
milestones = milestones.group_by(&:title) milestones = milestones.group_by(&:title)
...@@ -17,6 +21,7 @@ class GlobalMilestone ...@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title @title = title
@name = title @name = title
@milestones = milestones @milestones = milestones
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end end
def safe_title def safe_title
......
class Label < ActiveRecord::Base class Label < ActiveRecord::Base
include CacheMarkdownField
include Referable include Referable
include Subscribable include Subscribable
...@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base ...@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label') None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '') Any = LabelStruct.new('Any Label', '')
cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = '#428BCA' DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
......
...@@ -103,7 +103,12 @@ class Member < ActiveRecord::Base ...@@ -103,7 +103,12 @@ class Member < ActiveRecord::Base
} }
if member.request? if member.request?
::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute ::Members::ApproveAccessRequestService.new(
source,
current_user,
id: member.id,
access_level: access_level
).execute
else else
member.save member.save
end end
......
...@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars # Temporary fields to store compare vars
# when creating new merge request # when creating new merge request
attr_accessor :can_be_created, :compare_commits, :compare attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
...@@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_size def diff_size
merge_request_diff.size diffs(diff_options).size
end end
def diff_base_commit def diff_base_commit
......
...@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500 # Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100 COMMITS_SAFE_SIZE = 100
# Valid types of serialized diffs allowed by Gitlab::Git::Diff
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
belongs_to :merge_request belongs_to :merge_request
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
...@@ -170,6 +173,15 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -170,6 +173,15 @@ class MergeRequestDiff < ActiveRecord::Base
private private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
# Avoid an error 500 by ignoring bad elements. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
def valid_raw_diff?(raw)
return false unless raw.respond_to?(:each)
raw.any? { |element| VALID_CLASSES.include?(element.class) }
end
def dump_commits(commits) def dump_commits(commits)
commits.map(&:to_hash) commits.map(&:to_hash)
end end
...@@ -200,7 +212,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -200,7 +212,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def load_diffs(raw, options) def load_diffs(raw, options)
if raw.respond_to?(:each) if valid_raw_diff?(raw)
if paths = options[:paths] if paths = options[:paths]
raw = raw.select do |diff| raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
......
...@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base ...@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include CacheMarkdownField
include InternalId include InternalId
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
include Milestoneish include Milestoneish
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project belongs_to :project
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
......
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
include CacheMarkdownField
include Sortable include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
...@@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base ...@@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base
def clean_path(path) def clean_path(path)
path = path.dup path = path.dup
# Get the email username by removing everything after an `@` sign. # Get the email username by removing everything after an `@` sign.
path.gsub!(/@.*\z/, "") path.gsub!(/@.*\z/, "")
# Usernames can't end in .git, so remove it.
path.gsub!(/\.git\z/, "")
# Remove dashes at the start of the username.
path.gsub!(/\A-+/, "")
# Remove periods at the end of the username.
path.gsub!(/\.+\z/, "")
# Remove everything that's not in the list of allowed characters. # Remove everything that's not in the list of allowed characters.
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
# Remove trailing violations ('.atom', '.git', or '.')
path.gsub!(/(\.atom|\.git|\.)*\z/, "")
# Remove leading violations ('-')
path.gsub!(/\A\-+/, "")
# Users with the great usernames of "." or ".." would end up with a blank username. # Users with the great usernames of "." or ".." would end up with a blank username.
# Work around that by setting their username to "blank", followed by a counter. # Work around that by setting their username to "blank", followed by a counter.
......
...@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base ...@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
include Awardable include Awardable
include Importable include Importable
include FasterCacheKeys include FasterCacheKeys
include CacheMarkdownField
cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by # Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer. # Banzai::ObjectRenderer.
attr_accessor :note_html attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by # An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer # Banzai::ObjectRenderer
......
...@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include AccessRequestable include AccessRequestable
include CacheMarkdownField
include Referable include Referable
include Sortable include Sortable
include AfterCommitQueue include AfterCommitQueue
...@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base ...@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base
UNKNOWN_IMPORT_URL = 'http://unknown.git' UNKNOWN_IMPORT_URL = 'http://unknown.git'
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false default_value_for :archived, false
...@@ -372,19 +375,9 @@ class Project < ActiveRecord::Base ...@@ -372,19 +375,9 @@ class Project < ActiveRecord::Base
%r{(?<project>#{name_pattern}/#{name_pattern})} %r{(?<project>#{name_pattern}/#{name_pattern})}
end end
def trending(since = 1.month.ago) def trending
# By counting in the JOIN we don't expose the GROUP BY to the outer query. joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
# This means that calls such as "any?" and "count" just return a number of reorder('trending_projects.id ASC')
# the total count, instead of the counts grouped per project as a Hash.
join_body = "INNER JOIN (
SELECT project_id, COUNT(*) AS amount
FROM notes
WHERE created_at >= #{sanitize(since)}
AND system IS FALSE
GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id"
joins(join_body).reorder('join_note_counts.amount DESC')
end end
def cached_count def cached_count
......
...@@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base ...@@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
belongs_to :group belongs_to :group
validates :project_id, presence: true validates :project_id, presence: true
validates :group_id, presence: true validates :group, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" } validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
......
class Release < ActiveRecord::Base class Release < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
belongs_to :project belongs_to :project
validates :description, :project, :tag, presence: true validates :description, :project, :tag, presence: true
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment