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
This commit is contained in:
commit
419518df67
488 changed files with 8026 additions and 3688 deletions
|
@ -19,6 +19,8 @@ variables:
|
|||
before_script:
|
||||
- source ./scripts/prepare_build.sh
|
||||
- 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
|
||||
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
|
||||
- retry gem install knapsack
|
||||
|
|
61
CHANGELOG
61
CHANGELOG
|
@ -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)
|
||||
- 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
|
||||
- Add link from system note to compare with previous version
|
||||
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
|
||||
- Add `/projects/visible` API endpoint (Ben Boeckel)
|
||||
- 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
|
||||
- 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)
|
||||
- Fix Error 500 when viewing old merge requests with bad diff data
|
||||
- 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
|
||||
- Allow browsing branches that end with '.atom'
|
||||
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
|
||||
- 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
|
||||
- Simplify Mentionable concern instance methods
|
||||
- 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
|
||||
- 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)
|
||||
- Add Issue Board API support (andrebsguedes)
|
||||
- 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
|
||||
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
|
||||
- Add word-wrap to issue title on issue and milestone boards (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)
|
||||
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Fix that manual jobs would no longer block jobs in the next stage. !6604
|
||||
- Add configurable email subject suffix (Fu Xu)
|
||||
- Added tooltip to fork count on project show page. (Justin DiPierro)
|
||||
- Use a ConnectionPool for Rails.cache on Sidekiq servers
|
||||
- Replace `alias_method_chain` with `Module#prepend`
|
||||
- Enable GitLab Import/Export for non-admin users.
|
||||
- Preserve label filters when sorting !6136 (Joseph Frazier)
|
||||
- MergeRequest#new form load diff asynchronously
|
||||
- Only update issuable labels if they have been changed
|
||||
- Take filters in account in issuable counters. !6496
|
||||
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
|
||||
- Prevent flash alert text from being obscured when container is fluid
|
||||
- Append issue template to existing description !6149 (Joseph Frazier)
|
||||
- Trending projects now only show public projects and the list of projects is cached for a day
|
||||
- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
|
||||
- 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)
|
||||
- Fix Long commit messages overflow viewport in file tree
|
||||
- Revert avoid touching file system on Build#artifacts?
|
||||
|
@ -45,22 +74,44 @@ v 8.13.0 (unreleased)
|
|||
- Add broadcast messages and alerts below sub-nav
|
||||
- Better empty state for Groups view
|
||||
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
|
||||
- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
|
||||
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
|
||||
- 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
|
||||
- Optimize GitHub importing for speed and memory
|
||||
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
|
||||
- Notify the Merger about merge after successful build (Dimitris Karakasilis)
|
||||
- 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
|
||||
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Fix broken repository 500 errors in project list
|
||||
- Fix Pipeline list commit column width should be adjusted
|
||||
- Close todos when accepting merge requests via the API !6486 (tonygambone)
|
||||
- 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)
|
||||
- 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)
|
||||
- Allow empty merge requests !6384 (Artem Sidorenko)
|
||||
- Grouped pipeline dropdown is a scrollable container
|
||||
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
|
||||
- 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.5 (unreleased)
|
||||
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
|
||||
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
|
||||
|
@ -287,6 +338,10 @@ v 8.12.0
|
|||
- Fix non-master branch readme display in tree view
|
||||
- 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
|
||||
- Respect the fork_project permission when forking projects
|
||||
- Set a restrictive CORS policy on the API for credentialed requests
|
||||
|
@ -512,6 +567,10 @@ v 8.11.0
|
|||
- Update gitlab_git gem to 10.4.7
|
||||
- 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
|
||||
- Respect the fork_project permission when forking projects
|
||||
- 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
|
|||
to be marked as `Accepting merge requests`. Please include screenshots or
|
||||
wireframes if the feature will also change the UI.
|
||||
|
||||
Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at
|
||||
[github.com][github-mr-tracker].
|
||||
Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
|
||||
|
||||
If you are new to GitLab development (or web development in general), see the
|
||||
[I want to contribute!](#i-want-to-contribute) section to get you started with
|
||||
|
@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge
|
|||
request is as follows:
|
||||
|
||||
1. Fork the project into your personal space on GitLab.com
|
||||
1. Create a feature branch, branch away from `master`.
|
||||
1. 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. Add your changes to the [CHANGELOG](CHANGELOG)
|
||||
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
|
||||
1. Add your changes to the [CHANGELOG](CHANGELOG):
|
||||
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
|
||||
[squashing them][git-squash]
|
||||
1. Push the commit(s) to your fork
|
||||
|
@ -258,7 +264,7 @@ request is as follows:
|
|||
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]
|
||||
(#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,
|
||||
`grep css-class ./app -R`
|
||||
1. Link any relevant [issues][ce-tracker] in the merge request description and
|
||||
|
@ -270,7 +276,9 @@ request is as follows:
|
|||
[shell command guidelines](doc/development/shell_commands.md)
|
||||
1. If your code creates new files on disk please read the
|
||||
[shared files guidelines](doc/development/shared_files.md).
|
||||
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
|
||||
1. When writing commit messages please follow
|
||||
[these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
||||
[guidelines](http://chris.beams.io/posts/git-commit/).
|
||||
1. If your merge request adds one or more migrations, make sure to execute all
|
||||
migrations on a fresh database before the MR is reviewed. If the review leads
|
||||
to large changes in the MR, do this again once the review is complete.
|
||||
|
@ -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
|
||||
[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
|
||||
|
||||
1. The change is as small as possible
|
||||
|
@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format.
|
|||
aforementioned failing test
|
||||
1. Your MR initially contains a single commit (please use `git rebase -i` to
|
||||
squash commits)
|
||||
1. Your changes can merge without problems (if not please merge `master`, never
|
||||
rebase commits pushed to the remote server)
|
||||
1. Your changes can merge without problems (if not please rebase if you're the
|
||||
only one working on your feature branch, otherwise, merge `master`)
|
||||
1. Does not break any existing functionality
|
||||
1. Fixes one specific issue or implements one specific feature (do not combine
|
||||
things, send separate merge requests if needed)
|
||||
|
@ -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.
|
||||
- Don't touch neighbouring lines. As an exception, automatic mass
|
||||
refactoring modifications may leave style non-compliant.
|
||||
1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
|
||||
1. 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
|
||||
|
||||
|
@ -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-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
|
||||
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
|
||||
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
|
||||
[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
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.6.3
|
||||
3.6.4
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.8.2
|
||||
0.8.4
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -110,6 +110,7 @@ gem 'creole', '~> 0.5.0'
|
|||
gem 'wikicloth', '0.8.1'
|
||||
gem 'asciidoctor', '~> 1.5.2'
|
||||
gem 'rouge', '~> 2.0'
|
||||
gem 'truncato', '~> 0.7.8'
|
||||
|
||||
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
|
||||
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
|
||||
|
@ -323,7 +324,7 @@ gem 'newrelic_rpm', '~> 3.16'
|
|||
|
||||
gem 'octokit', '~> 4.3.0'
|
||||
|
||||
gem 'mail_room', '~> 0.8'
|
||||
gem 'mail_room', '~> 0.8.1'
|
||||
|
||||
gem 'email_reply_parser', '~> 0.5.8'
|
||||
|
||||
|
|
|
@ -399,7 +399,7 @@ GEM
|
|||
systemu (~> 2.6.2)
|
||||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
mail_room (0.8.0)
|
||||
mail_room (0.8.1)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.3)
|
||||
mimemagic (0.3.0)
|
||||
|
@ -745,6 +745,9 @@ GEM
|
|||
tilt (2.0.5)
|
||||
timecop (0.8.1)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
truncato (0.7.8)
|
||||
htmlentities (~> 4.3.1)
|
||||
nokogiri (~> 1.6.1)
|
||||
turbolinks (2.5.3)
|
||||
coffee-rails
|
||||
tzinfo (1.2.2)
|
||||
|
@ -886,7 +889,7 @@ DEPENDENCIES
|
|||
license_finder (~> 2.1.0)
|
||||
licensee (~> 8.0.0)
|
||||
loofah (~> 2.0.3)
|
||||
mail_room (~> 0.8)
|
||||
mail_room (~> 0.8.1)
|
||||
method_source (~> 0.8)
|
||||
minitest (~> 5.7.0)
|
||||
mousetrap-rails (~> 1.4.6)
|
||||
|
@ -971,6 +974,7 @@ DEPENDENCIES
|
|||
test_after_commit (~> 0.4.2)
|
||||
thin (~> 1.7.0)
|
||||
timecop (~> 0.8.0)
|
||||
truncato (~> 0.7.8)
|
||||
turbolinks (~> 2.5.0)
|
||||
u2f (~> 0.2.1)
|
||||
uglifier (~> 2.7.2)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# GitLab
|
||||
|
||||
[![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)
|
||||
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](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)
|
||||
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
|
||||
|
||||
|
|
|
@ -21,16 +21,14 @@
|
|||
};
|
||||
|
||||
Activities.prototype.toggleFilter = function(sender) {
|
||||
var event_filters, filter;
|
||||
var filter = sender.attr("id").split("_")[0];
|
||||
|
||||
$('.event-filter .active').removeClass("active");
|
||||
event_filters = $.cookie("event_filter");
|
||||
filter = sender.attr("id").split("_")[0];
|
||||
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
|
||||
$.cookie("event_filter", filter, {
|
||||
path: gon.relative_url_root || '/'
|
||||
});
|
||||
if (event_filters !== filter) {
|
||||
return sender.closest('li').toggleClass("active");
|
||||
}
|
||||
|
||||
sender.closest('li').toggleClass("active");
|
||||
};
|
||||
|
||||
return Activities;
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
filters: Store.state.filters
|
||||
filters: Store.state.filters,
|
||||
showIssueForm: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -33,6 +34,11 @@
|
|||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showNewIssueForm() {
|
||||
this.showIssueForm = !this.showIssueForm;
|
||||
}
|
||||
},
|
||||
ready () {
|
||||
const options = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
disabled: this.disabled,
|
||||
|
|
|
@ -8,10 +8,8 @@
|
|||
data () {
|
||||
return {
|
||||
predefinedLabels: [
|
||||
new ListLabel({ title: 'Development', color: '#5CB85C' }),
|
||||
new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
|
||||
new ListLabel({ title: 'Production', color: '#FF5F00' }),
|
||||
new ListLabel({ title: 'Ready', color: '#FF0000' })
|
||||
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
|
||||
new ListLabel({ title: 'Doing', color: '#5CB85C' })
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
//= require ./board_card
|
||||
//= require ./board_new_issue
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
@ -8,14 +9,16 @@
|
|||
|
||||
gl.issueBoards.BoardList = Vue.extend({
|
||||
components: {
|
||||
'board-card': gl.issueBoards.BoardCard
|
||||
'board-card': gl.issueBoards.BoardCard,
|
||||
'board-new-issue': gl.issueBoards.BoardNewIssue
|
||||
},
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
list: Object,
|
||||
issues: Array,
|
||||
loading: Boolean,
|
||||
issueLinkBase: String
|
||||
issueLinkBase: String,
|
||||
showIssueForm: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -73,7 +76,7 @@
|
|||
group: 'issues',
|
||||
sort: false,
|
||||
disabled: this.disabled,
|
||||
filter: '.board-list-count',
|
||||
filter: '.board-list-count, .is-disabled',
|
||||
onStart: (e) => {
|
||||
const card = this.$refs.issue[e.oldIndex];
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
(() => {
|
||||
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 @@
|
|||
fallbackClass: 'is-dragging',
|
||||
fallbackOnBody: true,
|
||||
ghostClass: 'is-ghost',
|
||||
filter: '.has-tooltip',
|
||||
filter: '.has-tooltip, .btn',
|
||||
delay: gl.issueBoards.touchEnabled ? 100 : 0,
|
||||
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
|
||||
scrollSpeed: 20,
|
||||
|
|
|
@ -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) {
|
||||
data.forEach((issueObj) => {
|
||||
this.addIssue(new ListIssue(issueObj));
|
||||
|
|
|
@ -58,4 +58,10 @@ class BoardService {
|
|||
to_list_id
|
||||
});
|
||||
}
|
||||
|
||||
newIssue (id, issue) {
|
||||
return this.issues.save({ id }, {
|
||||
issue
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -126,6 +126,9 @@
|
|||
new TreeView();
|
||||
}
|
||||
break;
|
||||
case 'projects:pipelines:show':
|
||||
new gl.Pipelines();
|
||||
break;
|
||||
case 'groups:activity':
|
||||
new Activities();
|
||||
break;
|
||||
|
|
|
@ -52,37 +52,27 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
setup: function(input) {
|
||||
setup: _.debounce(function(input) {
|
||||
// Add GFM auto-completion to all input fields, that accept GFM input.
|
||||
this.input = input || $('.js-gfm-input');
|
||||
// destroy previous instances
|
||||
this.destroyAtWho();
|
||||
// set up instances
|
||||
this.setupAtWho();
|
||||
if (this.dataSource) {
|
||||
if (!this.dataLoading && !this.cachedData) {
|
||||
this.dataLoading = true;
|
||||
setTimeout((function(_this) {
|
||||
return function() {
|
||||
var fetch;
|
||||
fetch = _this.fetchData(_this.dataSource);
|
||||
return fetch.done(function(data) {
|
||||
_this.dataLoading = false;
|
||||
return _this.loadData(data);
|
||||
});
|
||||
};
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (this.dataSource && !this.dataLoading && !this.cachedData) {
|
||||
this.dataLoading = true;
|
||||
return this.fetchData(this.dataSource)
|
||||
.done((data) => {
|
||||
this.dataLoading = false;
|
||||
this.loadData(data);
|
||||
});
|
||||
};
|
||||
|
||||
if (this.cachedData != null) {
|
||||
return this.loadData(this.cachedData);
|
||||
}
|
||||
},
|
||||
}, 1000),
|
||||
setupAtWho: function() {
|
||||
// Emoji
|
||||
this.input.atwho({
|
||||
|
|
|
@ -738,6 +738,7 @@
|
|||
return false;
|
||||
}
|
||||
if (currentKeyCode === 13 && currentIndex !== -1) {
|
||||
e.preventDefault();
|
||||
_this.selectRowAtIndex();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -38,6 +38,11 @@
|
|||
gl.utils.getPagePath = function() {
|
||||
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) {
|
||||
var suffixFromNow, timefor;
|
||||
if (!time) {
|
||||
|
|
|
@ -61,6 +61,9 @@
|
|||
function MergeRequestTabs(opts) {
|
||||
this.opts = opts != null ? opts : {};
|
||||
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
|
||||
|
||||
this.buildsLoaded = this.opts.buildsLoaded || false;
|
||||
|
||||
this.setCurrentAction = bind(this.setCurrentAction, this);
|
||||
this.tabShown = bind(this.tabShown, this);
|
||||
this.showTab = bind(this.showTab, this);
|
||||
|
@ -93,7 +96,7 @@
|
|||
this.loadCommits($target.attr('href'));
|
||||
this.expandView();
|
||||
this.resetViewContainer();
|
||||
} else if (action === 'diffs') {
|
||||
} else if (this.isDiffAction(action)) {
|
||||
this.loadDiff($target.attr('href'));
|
||||
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
|
||||
this.shrinkView();
|
||||
|
@ -170,8 +173,9 @@
|
|||
action = 'notes';
|
||||
}
|
||||
this.currentAction = action;
|
||||
// Remove a trailing '/commits' or '/diffs'
|
||||
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
|
||||
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
|
||||
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'
|
||||
if (action !== 'notes') {
|
||||
new_state += "/" + action;
|
||||
|
@ -210,8 +214,13 @@
|
|||
if (this.diffsLoaded) {
|
||||
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({
|
||||
url: (source + ".json") + this._location.search,
|
||||
url: (url.pathname + ".json") + this._location.search,
|
||||
success: (function(_this) {
|
||||
return function(data) {
|
||||
$('#diffs').html(data.html);
|
||||
|
@ -223,7 +232,7 @@
|
|||
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
|
||||
$('#diffs .js-syntax-highlight').syntaxHighlight();
|
||||
$('#diffs .diff-file').singleFileDiff();
|
||||
if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
|
||||
if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
|
||||
_this.expandViewContainer();
|
||||
}
|
||||
_this.diffsLoaded = true;
|
||||
|
@ -324,6 +333,10 @@
|
|||
return $('.inline-parallel-buttons a.active').data('view-type');
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.isDiffAction = function(action) {
|
||||
return action === 'diffs' || action === 'new/diffs'
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.expandViewContainer = function() {
|
||||
var $wrapper = $('.content-wrapper .container-fluid');
|
||||
if (this.fixedLayoutPref === null) {
|
||||
|
|
|
@ -1,15 +1,40 @@
|
|||
(function() {
|
||||
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');
|
||||
((global) => {
|
||||
|
||||
$($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.
|
|||
const action = $target.data('action');
|
||||
const source = $target.attr('href');
|
||||
this.setTab(source, action);
|
||||
return this.setCurrentAction(action);
|
||||
return this.setCurrentAction(source, action);
|
||||
}
|
||||
|
||||
activateTab(action) {
|
||||
|
@ -142,14 +142,9 @@ content on the Users#show page.
|
|||
.toggle(status);
|
||||
}
|
||||
|
||||
setCurrentAction(action) {
|
||||
const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`);
|
||||
let new_state = this._location.pathname;
|
||||
setCurrentAction(source, action) {
|
||||
let new_state = source
|
||||
new_state = new_state.replace(/\/+$/, '');
|
||||
new_state = new_state.replace(regExp, '');
|
||||
if (action !== this.defaultAction) {
|
||||
new_state += `/${action}`;
|
||||
}
|
||||
new_state += this._location.search + this._location.hash;
|
||||
history.replaceState({
|
||||
turbolinks: true,
|
||||
|
|
|
@ -71,8 +71,8 @@
|
|||
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> <% } %>');
|
||||
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> <% } %>');
|
||||
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="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
|
||||
return $dropdown.glDropdown({
|
||||
showMenuAbove: showMenuAbove,
|
||||
data: function(term, callback) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
@include border-radius($avatar_radius);
|
||||
border-radius: $avatar_radius;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
|
||||
&.avatar-inline {
|
||||
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
&.avatar-tile {
|
||||
@include border-radius(0);
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
}
|
||||
|
||||
.identicon {
|
||||
@include border-radius(50%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@mixin btn-default {
|
||||
@include border-radius(3px);
|
||||
border-radius: 3px;
|
||||
font-size: $gl-font-size;
|
||||
font-weight: 500;
|
||||
padding: $gl-vert-padding $gl-btn-padding;
|
||||
|
@ -8,7 +8,7 @@
|
|||
&:active {
|
||||
outline: none;
|
||||
background-color: $btn-active-gray;
|
||||
@include box-shadow($gl-btn-active-background);
|
||||
box-shadow: $gl-btn-active-background;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
&:active,
|
||||
&.active {
|
||||
@include box-shadow ($gl-btn-active-background);
|
||||
box-shadow: $gl-btn-active-background;
|
||||
|
||||
background-color: $dark;
|
||||
border-color: $border-dark;
|
||||
|
@ -194,10 +194,17 @@
|
|||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.caret {
|
||||
.fa-caret-down,
|
||||
.fa-caret-up {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&.dropdown-toggle {
|
||||
.fa-caret-down {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
|
@ -272,7 +279,7 @@
|
|||
}
|
||||
|
||||
.active {
|
||||
@include box-shadow($gl-btn-active-background);
|
||||
box-shadow: $gl-btn-active-background;
|
||||
|
||||
border: 1px solid #c6cacf !important;
|
||||
background-color: #e4e7ed !important;
|
||||
|
|
|
@ -1,20 +1,3 @@
|
|||
.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 {
|
||||
position: relative;
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ label {
|
|||
}
|
||||
|
||||
.form-control {
|
||||
@include box-shadow(none);
|
||||
box-shadow: none;
|
||||
border-radius: 3px;
|
||||
padding: $gl-vert-padding $gl-input-padding;
|
||||
}
|
||||
|
@ -81,10 +81,10 @@ label {
|
|||
.select-wrapper {
|
||||
position: relative;
|
||||
|
||||
.caret {
|
||||
.fa-caret-down {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: $gl-padding;
|
||||
top: 10px;
|
||||
color: $gray-darkest;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ header {
|
|||
&:hover, &:focus, &:active {
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.fa-caret-down {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@include border-radius(3px);
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
}
|
||||
|
||||
.markdown-area {
|
||||
@include border-radius(0);
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
min-height: 140px;
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* Generic mixins
|
||||
*/
|
||||
@mixin box-shadow($shadow) {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
|
||||
@mixin border-radius($radius) {
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefilled mixins
|
||||
* Mixins with fixed values
|
||||
|
|
|
@ -133,5 +133,5 @@
|
|||
font-size: 20px;
|
||||
color: #777;
|
||||
z-index: 100;
|
||||
@include box-shadow(0 1px 2px #ddd);
|
||||
box-shadow: 0 1px 2px #ddd;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,14 @@
|
|||
padding-right: 10px;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +46,8 @@
|
|||
}
|
||||
|
||||
.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);
|
||||
@include border-radius ($border-radius-default);
|
||||
box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
|
||||
border-radius: $border-radius-default;
|
||||
border: none;
|
||||
min-width: 175px;
|
||||
}
|
||||
|
@ -65,7 +72,7 @@
|
|||
|
||||
.select2-container-active {
|
||||
.select2-choice, .select2-choices {
|
||||
@include box-shadow(none);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,13 +82,13 @@
|
|||
outline: 0;
|
||||
background-image: none;
|
||||
background-color: $white-dark;
|
||||
@include box-shadow($gl-btn-active-gradient);
|
||||
box-shadow: $gl-btn-active-gradient;
|
||||
}
|
||||
}
|
||||
|
||||
.select2-container-multi {
|
||||
.select2-choices {
|
||||
@include border-radius($border-radius-default);
|
||||
border-radius: $border-radius-default;
|
||||
border-color: $input-border;
|
||||
background: none;
|
||||
|
||||
|
@ -116,7 +123,7 @@
|
|||
&.select2-container-active .select2-choices,
|
||||
&.select2-dropdown-open .select2-choices {
|
||||
border-color: $border-white-normal;
|
||||
@include box-shadow($gl-btn-active-gradient);
|
||||
box-shadow: $gl-btn-active-gradient;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,7 +157,7 @@
|
|||
background-repeat: no-repeat;
|
||||
background-position: right 0 bottom 6px;
|
||||
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;
|
||||
|
||||
&:focus {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
&.page-sidebar-pinned {
|
||||
.sidebar-wrapper {
|
||||
@include box-shadow(none);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
|||
width: 0;
|
||||
overflow: hidden;
|
||||
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 @@
|
|||
.count {
|
||||
float: right;
|
||||
padding: 0 8px;
|
||||
@include border-radius(6px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
font-size: 13px;
|
||||
line-height: 1.6em;
|
||||
overflow-x: auto;
|
||||
@include border-radius(2px);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
p > code {
|
||||
|
|
|
@ -17,8 +17,10 @@ $white-normal: #ededed;
|
|||
$white-dark: #ececec;
|
||||
|
||||
$gray-light: #fafafa;
|
||||
$gray-lighter: #f9f9f9;
|
||||
$gray-normal: #f5f5f5;
|
||||
$gray-dark: #ededed;
|
||||
$gray-darker: #eee;
|
||||
$gray-darkest: #c9c9c9;
|
||||
|
||||
$green-light: #38ae67;
|
||||
|
@ -33,6 +35,8 @@ $blue-medium-light: #3498cb;
|
|||
$blue-medium: #2f8ebf;
|
||||
$blue-medium-dark: #2d86b4;
|
||||
|
||||
$blue-light-transparent: rgba(44, 159, 216, 0.05);
|
||||
|
||||
$orange-light: #fc8a51;
|
||||
$orange-normal: #e75e40;
|
||||
$orange-dark: #ce5237;
|
||||
|
@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f;
|
|||
$gl-font-size: 15px;
|
||||
$gl-title-color: #333;
|
||||
$gl-text-color: #5c5c5c;
|
||||
$gl-text-color-light: #8c8c8c;
|
||||
$gl-text-green: #4a2;
|
||||
$gl-text-red: #d12f19;
|
||||
$gl-text-orange: #d90;
|
||||
|
|
|
@ -162,6 +162,10 @@ lex
|
|||
list-style: none;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.is-smaller {
|
||||
height: calc(100% - 185px);
|
||||
}
|
||||
}
|
||||
|
||||
.board-list-loading {
|
||||
|
@ -233,3 +237,31 @@ lex
|
|||
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 @@
|
|||
|
||||
.bordered-box {
|
||||
border: 1px solid $border-color;
|
||||
@include border-radius($border-radius-default);
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.file-editor {
|
||||
#editor {
|
||||
border: none;
|
||||
@include border-radius(0);
|
||||
border-radius: 0;
|
||||
height: 500px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
.environments-container,
|
||||
.deployments-container {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.environments {
|
||||
.deployment-column {
|
||||
.avatar {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
.commit-title {
|
||||
margin: 0;
|
||||
|
@ -9,6 +20,7 @@
|
|||
width: 12px;
|
||||
}
|
||||
|
||||
.external-url,
|
||||
.dropdown-new {
|
||||
color: $table-text-gray;
|
||||
}
|
||||
|
@ -21,16 +33,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
.build-link,
|
||||
.branch-name {
|
||||
color: $gl-dark-link-color;
|
||||
}
|
||||
|
||||
.deployment {
|
||||
.build-column {
|
||||
|
||||
.build-link {
|
||||
color: $gl-dark-link-color;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table.builds.environments {
|
||||
min-width: 500px;
|
||||
|
||||
.icon-container {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.branch-commit {
|
||||
.commit-id {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
float: right;
|
||||
border: 1px solid #eee;
|
||||
padding: 5px;
|
||||
@include border-radius(5px);
|
||||
border-radius: 5px;
|
||||
background: $gray-light;
|
||||
margin-left: 10px;
|
||||
top: -6px;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.suggest-colors {
|
||||
margin-top: 5px;
|
||||
a {
|
||||
@include border-radius(4px);
|
||||
border-radius: 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
|
@ -17,7 +17,7 @@
|
|||
overflow: hidden;
|
||||
|
||||
a {
|
||||
@include border-radius(0);
|
||||
border-radius: 0;
|
||||
width: (100% / 7);
|
||||
margin-right: 0;
|
||||
margin-bottom: -5px;
|
||||
|
@ -59,6 +59,13 @@
|
|||
width: 200px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.label-description {
|
||||
|
|
|
@ -73,12 +73,12 @@
|
|||
height: auto;
|
||||
|
||||
&.top {
|
||||
@include border-radius(5px 5px 0 0);
|
||||
border-radius: 5px 5px 0 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
@include border-radius(0 0 5px 5px);
|
||||
border-radius: 0 0 5px 5px;
|
||||
border-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
@ -86,7 +86,7 @@
|
|||
&.middle {
|
||||
border-top: 0;
|
||||
margin-bottom: 0;
|
||||
@include border-radius(0);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:active, &:focus {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
background: $background-color;
|
||||
color: $gl-gray;
|
||||
border: 1px solid $border-color;
|
||||
@include border-radius(2px);
|
||||
border-radius: 2px;
|
||||
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
|
@ -204,6 +204,18 @@
|
|||
word-break: break-all;
|
||||
}
|
||||
|
||||
.commits-empty {
|
||||
text-align: center;
|
||||
|
||||
h4 {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
svg {
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.mr-list {
|
||||
.merge-request {
|
||||
padding: 10px 15px;
|
||||
|
|
|
@ -334,7 +334,7 @@ ul.notes {
|
|||
|
||||
.add-diff-note {
|
||||
margin-top: -4px;
|
||||
@include border-radius(40px);
|
||||
border-radius: 40px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
font-size: 16px;
|
||||
|
|
|
@ -229,9 +229,12 @@
|
|||
|
||||
.fa {
|
||||
color: $table-text-gray;
|
||||
margin-right: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
svg, .fa {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
|
@ -272,18 +275,8 @@
|
|||
.toggle-pipeline-btn {
|
||||
background-color: $gray-dark;
|
||||
|
||||
.caret {
|
||||
border-top: none;
|
||||
border-bottom: 4px solid;
|
||||
}
|
||||
|
||||
&.graph-collapsed {
|
||||
background-color: $white-light;
|
||||
|
||||
.caret {
|
||||
border-bottom: none;
|
||||
border-top: 4px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,16 +303,41 @@
|
|||
.stage-column {
|
||||
display: inline-block;
|
||||
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 {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
margin-bottom: 15px;
|
||||
margin: 0 0 15px 10px;
|
||||
font-weight: bold;
|
||||
width: 150px;
|
||||
width: 176px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -328,17 +346,23 @@
|
|||
.build {
|
||||
border: 1px solid $border-color;
|
||||
position: relative;
|
||||
padding: 6px 10px;
|
||||
padding: 7px 10px 8px;
|
||||
border-radius: 30px;
|
||||
width: 150px;
|
||||
width: 186px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-lighter;
|
||||
.dropdown-menu-toggle {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.playable {
|
||||
background-color: $gray-light;
|
||||
|
||||
svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
height: 13px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
|
@ -349,10 +373,20 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
width: 110px;
|
||||
width: 135px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -363,44 +397,53 @@
|
|||
}
|
||||
|
||||
a {
|
||||
color: $layout-link-gray;
|
||||
color: $gl-text-color-light;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
.ci-status-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
border: none;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
color: $layout-link-gray;
|
||||
color: $gl-text-color-light;
|
||||
flex-grow: 1;
|
||||
|
||||
.ci-status-text {
|
||||
width: 80px;
|
||||
max-width: 112px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.grouped-pipeline-dropdown {
|
||||
padding: 8px 0;
|
||||
width: 200px;
|
||||
width: 186px;
|
||||
left: auto;
|
||||
right: -214px;
|
||||
right: -197px;
|
||||
top: -9px;
|
||||
max-height: 245px;
|
||||
overflow-y: scroll;
|
||||
|
||||
a:hover {
|
||||
.ci-status-text {
|
||||
text-decoration: none;
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
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 {
|
||||
width: 145px;
|
||||
width: 112px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
@ -433,9 +476,10 @@
|
|||
}
|
||||
|
||||
.badge {
|
||||
background-color: $gray-dark;
|
||||
color: $layout-link-gray;
|
||||
background-color: $gray-darker;
|
||||
color: $gl-text-color-light;
|
||||
font-weight: normal;
|
||||
margin-left: $btn-xs-side-margin;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -449,10 +493,10 @@
|
|||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -69px;
|
||||
top: 48%;
|
||||
right: -48px;
|
||||
border-top: 2px solid $border-color;
|
||||
width: 69px;
|
||||
width: 48px;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
@ -461,25 +505,25 @@
|
|||
&:not(:first-child) {
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
top: -47px;
|
||||
top: -49px;
|
||||
position: absolute;
|
||||
border-bottom: 2px solid $border-color;
|
||||
width: 20px;
|
||||
height: 65px;
|
||||
width: 25px;
|
||||
height: 69px;
|
||||
}
|
||||
|
||||
// Right connecting curves
|
||||
&::after {
|
||||
right: -20px;
|
||||
right: -25px;
|
||||
border-right: 2px solid $border-color;
|
||||
border-radius: 0 0 15px;
|
||||
border-radius: 0 0 20px;
|
||||
}
|
||||
|
||||
// Left connecting curves
|
||||
&::before {
|
||||
left: -20px;
|
||||
left: -25px;
|
||||
border-left: 2px solid $border-color;
|
||||
border-radius: 0 0 0 15px;
|
||||
border-radius: 0 0 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -487,7 +531,7 @@
|
|||
&:nth-child(2) {
|
||||
&::after, &::before {
|
||||
height: 29px;
|
||||
top: -10px;
|
||||
top: -9px;
|
||||
}
|
||||
.curve {
|
||||
display: block;
|
||||
|
@ -545,20 +589,20 @@
|
|||
width: 21px;
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
top: -29px;
|
||||
top: -32px;
|
||||
border-top: 2px solid $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -39px;
|
||||
left: -44px;
|
||||
border-right: 2px solid $border-color;
|
||||
border-radius: 0 15px;
|
||||
border-radius: 0 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: -39px;
|
||||
right: -44px;
|
||||
border-left: 2px solid $border-color;
|
||||
border-radius: 15px 0 0;
|
||||
border-radius: 20px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
.profile-user-bio {
|
||||
// Limits the width of the user bio for readability.
|
||||
max-width: 600px;
|
||||
margin: 15px auto 0;
|
||||
margin: 10px auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
|
@ -213,29 +213,22 @@
|
|||
}
|
||||
|
||||
.user-profile {
|
||||
|
||||
.cover-controls a {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
margin: 0 auto;
|
||||
|
||||
.avatar-holder {
|
||||
width: 90px;
|
||||
display: inline-block;
|
||||
}
|
||||
.user-info {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
margin-left: 15px;
|
||||
.handle {
|
||||
color: $gl-gray-light;
|
||||
}
|
||||
.member-date {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
|
||||
.cover-block {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
@ -258,10 +251,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-profile-nav {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
table.u2f-registrations {
|
||||
th:not(:last-child), td:not(:last-child) {
|
||||
border-right: solid 1px transparent;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
text-align: center;
|
||||
|
||||
.preview {
|
||||
@include border-radius(4px);
|
||||
border-radius: 4px;
|
||||
|
||||
height: 80px;
|
||||
margin-bottom: 10px;
|
||||
|
@ -47,7 +47,7 @@
|
|||
width: 160px;
|
||||
|
||||
img {
|
||||
@include border-radius(4px);
|
||||
border-radius: 4px;
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
|
@ -354,7 +354,7 @@ a.deploy-project-label {
|
|||
justify-content: flex-start;
|
||||
|
||||
.fork-thumbnail {
|
||||
@include border-radius($border-radius-base);
|
||||
border-radius: $border-radius-base;
|
||||
background-color: $white-light;
|
||||
border: 1px solid $border-white-light;
|
||||
height: 202px;
|
||||
|
@ -371,7 +371,7 @@ a.deploy-project-label {
|
|||
background-color: $gray-light;
|
||||
border: 1px solid $gray-dark;
|
||||
margin: 0 auto;
|
||||
@include border-radius(50%);
|
||||
border-radius: 50%;
|
||||
i {
|
||||
font-size: 100px;
|
||||
color: $gray-dark;
|
||||
|
@ -390,7 +390,7 @@ a.deploy-project-label {
|
|||
}
|
||||
|
||||
img {
|
||||
@include border-radius(50%);
|
||||
border-radius: 50%;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
@ -496,7 +496,7 @@ pre.light-well {
|
|||
}
|
||||
|
||||
.light-well {
|
||||
@include border-radius (2px);
|
||||
border-radius: 2px;
|
||||
|
||||
color: #5b6169;
|
||||
font-size: 13px;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
margin-right: 10px;
|
||||
border: 1px solid #eee;
|
||||
white-space: nowrap;
|
||||
@include border-radius(4px);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
|
|
@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def preview
|
||||
@message = broadcast_message_params[:message]
|
||||
@broadcast_message = BroadcastMessage.new(broadcast_message_params)
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
module Ci
|
||||
class ApplicationController < ::ApplicationController
|
||||
def self.railtie_helpers_paths
|
||||
"app/helpers/ci"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
module Ci
|
||||
class LintsController < ApplicationController
|
||||
class LintsController < ::ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module Ci
|
||||
class ProjectsController < Ci::ApplicationController
|
||||
class ProjectsController < ::ApplicationController
|
||||
before_action :project
|
||||
before_action :no_cache, only: [:badge]
|
||||
before_action :authorize_read_project!, except: [:badge, :index]
|
||||
|
|
|
@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
end
|
||||
|
||||
def trending
|
||||
@projects = TrendingProjectsFinder.new.execute
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = filter_projects(Project.trending)
|
||||
@projects = @projects.page(params[:page])
|
||||
|
||||
respond_to do |format|
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
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
|
|||
module Boards
|
||||
class IssuesController < Boards::ApplicationController
|
||||
before_action :authorize_read_issue!, only: [:index]
|
||||
before_action :authorize_create_issue!, only: [:create]
|
||||
before_action :authorize_update_issue!, only: [:update]
|
||||
|
||||
def index
|
||||
|
@ -9,16 +10,23 @@ module Projects
|
|||
issues = issues.page(params[:page])
|
||||
|
||||
render json: {
|
||||
issues: issues.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] }
|
||||
}),
|
||||
issues: serialize_as_json(issues),
|
||||
size: issues.total_count
|
||||
}
|
||||
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
|
||||
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
|
||||
|
||||
|
@ -43,6 +51,10 @@ module Projects
|
|||
return render_403 unless can?(current_user, :read_issue, project)
|
||||
end
|
||||
|
||||
def authorize_create_issue!
|
||||
return render_403 unless can?(current_user, :admin_issue, project)
|
||||
end
|
||||
|
||||
def authorize_update_issue!
|
||||
return render_403 unless can?(current_user, :update_issue, issue)
|
||||
end
|
||||
|
@ -54,6 +66,19 @@ module Projects
|
|||
def move_params
|
||||
params.permit(:id, :from_list_id, :to_list_id)
|
||||
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
|
||||
|
|
|
@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
protected
|
||||
|
||||
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
|
||||
alias_method :subscribable_resource, :issue
|
||||
alias_method :issuable, :issue
|
||||
|
|
|
@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
before_action :module_enabled
|
||||
before_action :merge_request, only: [
|
||||
: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 :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
|
||||
|
@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
before_action :define_diff_comment_vars, only: [:diffs]
|
||||
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
|
||||
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
|
||||
before_action :apply_diff_view_cookie!, only: [:new_diffs]
|
||||
before_action :build_merge_request, only: [:new, :new_diffs]
|
||||
|
||||
# Allow read any merge_request
|
||||
before_action :authorize_read_merge_request!
|
||||
|
@ -29,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
# Allow modify merge_request
|
||||
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]
|
||||
|
||||
def index
|
||||
|
@ -210,29 +214,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
apply_diff_view_cookie!
|
||||
define_new_vars
|
||||
end
|
||||
|
||||
build_merge_request
|
||||
@noteable = @merge_request
|
||||
def new_diffs
|
||||
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
|
||||
|
||||
@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)).
|
||||
group(:commit_id).count
|
||||
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -355,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
render layout: false
|
||||
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
|
||||
pipeline = @merge_request.pipeline
|
||||
if pipeline
|
||||
|
@ -490,6 +510,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
)
|
||||
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
|
||||
# Render special view for MR with removed target branch
|
||||
render 'invalid'
|
||||
|
@ -521,7 +562,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
def build_merge_request
|
||||
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
|
||||
|
||||
def compared_diff_version
|
||||
|
|
|
@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController
|
|||
def show
|
||||
@tag = @repository.find_tag(params[:id])
|
||||
|
||||
return render_404 unless @tag
|
||||
|
||||
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
|
||||
@commit = @repository.commit(@tag.target)
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
class SnippetsController < ApplicationController
|
||||
include ToggleAwardEmoji
|
||||
|
||||
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
|
||||
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
|
||||
|
||||
# Allow read snippet
|
||||
before_action :authorize_read_snippet!, only: [:show, :raw]
|
||||
before_action :authorize_read_snippet!, only: [:show, :raw, :download]
|
||||
|
||||
# Allow modify snippet
|
||||
before_action :authorize_update_snippet!, only: [:edit, :update]
|
||||
|
@ -12,7 +12,7 @@ class SnippetsController < ApplicationController
|
|||
# Allow destroy snippet
|
||||
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'
|
||||
respond_to :html
|
||||
|
@ -75,6 +75,14 @@ class SnippetsController < ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def download
|
||||
send_data(
|
||||
@snippet.content,
|
||||
type: 'text/plain; charset=utf-8',
|
||||
filename: @snippet.sanitized_file_name
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def snippet
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
# 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
|
|||
end
|
||||
|
||||
def brand_text
|
||||
markdown(brand_item.description)
|
||||
markdown_field(brand_item, :description)
|
||||
end
|
||||
|
||||
def brand_item
|
||||
|
|
|
@ -11,18 +11,6 @@ module ApplicationSettingsHelper
|
|||
current_application_settings.signin_enabled?
|
||||
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?
|
||||
current_application_settings.user_oauth_applications
|
||||
end
|
||||
|
|
|
@ -4,15 +4,18 @@ module AvatarsHelper
|
|||
user: commit_or_event.author,
|
||||
user_name: commit_or_event.author_name,
|
||||
user_email: commit_or_event.author_email,
|
||||
css_class: 'hidden-xs'
|
||||
}))
|
||||
end
|
||||
|
||||
def user_avatar(options = {})
|
||||
avatar_size = options[:size] || 16
|
||||
user_name = options[:user].try(:name) || options[:user_name]
|
||||
css_class = options[:css_class] || ''
|
||||
|
||||
avatar = image_tag(
|
||||
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",
|
||||
title: user_name,
|
||||
data: { container: 'body' }
|
||||
|
|
|
@ -3,7 +3,7 @@ module BroadcastMessagesHelper
|
|||
return unless message.present?
|
||||
|
||||
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
|
||||
|
||||
|
@ -32,7 +32,7 @@ module BroadcastMessagesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def render_broadcast_message(message)
|
||||
Banzai.render(message, pipeline: :broadcast_message).html_safe
|
||||
def render_broadcast_message(broadcast_message)
|
||||
Banzai.render_field(broadcast_message, :message).html_safe
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,10 +15,11 @@ module ButtonHelper
|
|||
#
|
||||
# See http://clipboardjs.com/#usage
|
||||
def clipboard_button(data = {})
|
||||
css_class = data[:class] || 'btn-clipboard'
|
||||
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
|
||||
content_tag :button,
|
||||
icon('clipboard'),
|
||||
class: "btn btn-clipboard",
|
||||
class: "btn #{css_class}",
|
||||
data: data,
|
||||
type: :button,
|
||||
title: "Copy to Clipboard"
|
||||
|
|
|
@ -13,14 +13,12 @@ module GitlabMarkdownHelper
|
|||
def link_to_gfm(body, url, html_options = {})
|
||||
return "" if body.blank?
|
||||
|
||||
escaped_body = if body.start_with?('<img')
|
||||
body
|
||||
else
|
||||
escape_once(body)
|
||||
end
|
||||
|
||||
user = current_user if defined?(current_user)
|
||||
gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
|
||||
context = {
|
||||
project: @project,
|
||||
current_user: (current_user if defined?(current_user)),
|
||||
pipeline: :single_line,
|
||||
}
|
||||
gfm_body = Banzai.render(body, context)
|
||||
|
||||
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
|
||||
if fragment.children.size == 1 && fragment.children[0].name == 'a'
|
||||
|
@ -51,17 +49,15 @@ module GitlabMarkdownHelper
|
|||
context[:project] ||= @project
|
||||
|
||||
html = Banzai.render(text, context)
|
||||
banzai_postprocess(html, context)
|
||||
end
|
||||
|
||||
context.merge!(
|
||||
current_user: (current_user if defined?(current_user)),
|
||||
def markdown_field(object, field)
|
||||
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
|
||||
|
||||
def asciidoc(text)
|
||||
|
@ -196,4 +192,18 @@ module GitlabMarkdownHelper
|
|||
icon(options[:icon])
|
||||
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
|
||||
|
|
|
@ -113,14 +113,13 @@ module IssuesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def award_user_list(awards, current_user)
|
||||
def award_user_list(awards, current_user, limit: 10)
|
||||
names = awards.map do |award|
|
||||
award.user == current_user ? 'You' : award.user.name
|
||||
end
|
||||
|
||||
# Take first 9 OR current user + first 9
|
||||
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
|
||||
|
||||
|
|
|
@ -72,6 +72,19 @@ module MergeRequestsHelper
|
|||
)
|
||||
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)
|
||||
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
|
|||
search_path(options)
|
||||
end
|
||||
|
||||
# Sanitize html generated after parsing markdown from issue description or comment
|
||||
def search_md_sanitize(html)
|
||||
# Sanitize a HTML field for search display. Most tags are stripped out and the
|
||||
# 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))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
class AbuseReport < ActiveRecord::Base
|
||||
include CacheMarkdownField
|
||||
|
||||
cache_markdown_field :message, pipeline: :single_line
|
||||
|
||||
belongs_to :reporter, class_name: 'User'
|
||||
belongs_to :user
|
||||
|
||||
|
@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
|
|||
validates :message, presence: true
|
||||
validates :user_id, uniqueness: { message: 'has already been reported' }
|
||||
|
||||
# For CacheMarkdownField
|
||||
alias_method :author, :reporter
|
||||
|
||||
def remove_user(deleted_by:)
|
||||
user.block
|
||||
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
class Appearance < ActiveRecord::Base
|
||||
include CacheMarkdownField
|
||||
|
||||
cache_markdown_field :description
|
||||
|
||||
validates :title, presence: true
|
||||
validates :description, presence: true
|
||||
validates :logo, file_size: { maximum: 1.megabyte }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
class ApplicationSetting < ActiveRecord::Base
|
||||
include CacheMarkdownField
|
||||
include TokenAuthenticatable
|
||||
|
||||
add_authentication_token_field :runners_registration_token
|
||||
add_authentication_token_field :health_check_access_token
|
||||
|
||||
|
@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
serialize :domain_whitelist, 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
|
||||
|
||||
validates :session_expire_delay,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
class BroadcastMessage < ActiveRecord::Base
|
||||
include CacheMarkdownField
|
||||
include Sortable
|
||||
|
||||
cache_markdown_field :message, pipeline: :broadcast_message
|
||||
|
||||
validates :message, presence: true
|
||||
validates :starts_at, presence: true
|
||||
validates :ends_at, presence: true
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class CommitStatus < ActiveRecord::Base
|
||||
include HasStatus
|
||||
include Importable
|
||||
include AfterCommitQueue
|
||||
|
||||
self.table_name = 'ci_builds'
|
||||
|
||||
|
@ -85,21 +86,24 @@ class CommitStatus < ActiveRecord::Base
|
|||
end
|
||||
|
||||
after_transition do |commit_status, transition|
|
||||
commit_status.pipeline.try do |pipeline|
|
||||
break if transition.loopback?
|
||||
return if transition.loopback?
|
||||
|
||||
if commit_status.complete?
|
||||
PipelineProcessWorker.perform_async(pipeline.id)
|
||||
commit_status.run_after_commit do
|
||||
pipeline.try do |pipeline|
|
||||
if complete?
|
||||
PipelineProcessWorker.perform_async(pipeline.id)
|
||||
else
|
||||
PipelineUpdateWorker.perform_async(pipeline.id)
|
||||
end
|
||||
end
|
||||
|
||||
PipelineUpdateWorker.perform_async(pipeline.id)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
|
131
app/models/concerns/cache_markdown_field.rb
Normal file
131
app/models/concerns/cache_markdown_field.rb
Normal file
|
@ -0,0 +1,131 @@
|
|||
# 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 @@
|
|||
#
|
||||
module Issuable
|
||||
extend ActiveSupport::Concern
|
||||
include CacheMarkdownField
|
||||
include Participable
|
||||
include Mentionable
|
||||
include Subscribable
|
||||
|
@ -13,6 +14,9 @@ module Issuable
|
|||
include Awardable
|
||||
|
||||
included do
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
|
||||
belongs_to :author, class_name: "User"
|
||||
belongs_to :assignee, class_name: "User"
|
||||
belongs_to :updated_by, class_name: "User"
|
||||
|
|
|
@ -68,8 +68,10 @@ class Event < ActiveRecord::Base
|
|||
true
|
||||
elsif issue? || issue_note?
|
||||
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
|
||||
((merge_request? || note?) && target.present?) || milestone?
|
||||
milestone?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -280,6 +282,10 @@ class Event < ActiveRecord::Base
|
|||
note? && target && target.for_issue?
|
||||
end
|
||||
|
||||
def merge_request_note?
|
||||
note? && target && target.for_merge_request?
|
||||
end
|
||||
|
||||
def project_snippet_note?
|
||||
target.for_snippet?
|
||||
end
|
||||
|
@ -335,7 +341,7 @@ class Event < ActiveRecord::Base
|
|||
# update the project. Only one query should actually perform the update,
|
||||
# hence we add the extra WHERE clause for last_activity_at.
|
||||
Project.unscoped.where(id: project_id).
|
||||
where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
|
||||
where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
|
||||
update_all(last_activity_at: created_at)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@ class GlobalLabel
|
|||
|
||||
delegate :color, :description, to: :@first_label
|
||||
|
||||
def for_display
|
||||
@first_label
|
||||
end
|
||||
|
||||
def self.build_collection(labels)
|
||||
labels = labels.group_by(&:title)
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@ class GlobalMilestone
|
|||
attr_accessor :title, :milestones
|
||||
alias_attribute :name, :title
|
||||
|
||||
def for_display
|
||||
@first_milestone
|
||||
end
|
||||
|
||||
def self.build_collection(milestones)
|
||||
milestones = milestones.group_by(&:title)
|
||||
|
||||
|
@ -17,6 +21,7 @@ class GlobalMilestone
|
|||
@title = title
|
||||
@name = title
|
||||
@milestones = milestones
|
||||
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
|
||||
end
|
||||
|
||||
def safe_title
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Label < ActiveRecord::Base
|
||||
include CacheMarkdownField
|
||||
include Referable
|
||||
include Subscribable
|
||||
|
||||
|
@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
|
|||
None = LabelStruct.new('No Label', 'No Label')
|
||||
Any = LabelStruct.new('Any Label', '')
|
||||
|
||||
cache_markdown_field :description, pipeline: :single_line
|
||||
|
||||
DEFAULT_COLOR = '#428BCA'
|
||||
|
||||
default_value_for :color, DEFAULT_COLOR
|
||||
|
|
|
@ -103,7 +103,12 @@ class Member < ActiveRecord::Base
|
|||
}
|
||||
|
||||
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
|
||||
member.save
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
# Temporary fields to store compare vars
|
||||
# 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
|
||||
event :close do
|
||||
|
@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def diff_size
|
||||
merge_request_diff.size
|
||||
diffs(diff_options).size
|
||||
end
|
||||
|
||||
def diff_base_commit
|
||||
|
|
|
@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
# Prevent store of diff if commits amount more then 500
|
||||
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
|
||||
|
||||
state_machine :state, initial: :empty do
|
||||
|
@ -170,6 +173,15 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
|
||||
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)
|
||||
commits.map(&:to_hash)
|
||||
end
|
||||
|
@ -200,7 +212,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def load_diffs(raw, options)
|
||||
if raw.respond_to?(:each)
|
||||
if valid_raw_diff?(raw)
|
||||
if paths = options[:paths]
|
||||
raw = raw.select do |diff|
|
||||
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
|
||||
|
|
|
@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
|
|||
Any = MilestoneStruct.new('Any Milestone', '', -1)
|
||||
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
|
||||
|
||||
include CacheMarkdownField
|
||||
include InternalId
|
||||
include Sortable
|
||||
include Referable
|
||||
include StripAttribute
|
||||
include Milestoneish
|
||||
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
|
||||
belongs_to :project
|
||||
has_many :issues
|
||||
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
class Namespace < ActiveRecord::Base
|
||||
acts_as_paranoid
|
||||
|
||||
include CacheMarkdownField
|
||||
include Sortable
|
||||
include Gitlab::ShellAdapter
|
||||
|
||||
cache_markdown_field :description, pipeline: :description
|
||||
|
||||
has_many :projects, dependent: :destroy
|
||||
belongs_to :owner, class_name: "User"
|
||||
|
||||
|
@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base
|
|||
def clean_path(path)
|
||||
path = path.dup
|
||||
# Get the email username by removing everything after an `@` sign.
|
||||
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/, "")
|
||||
path.gsub!(/@.*\z/, "")
|
||||
# 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.
|
||||
# Work around that by setting their username to "blank", followed by a counter.
|
||||
|
|
|
@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
|
|||
include Awardable
|
||||
include Importable
|
||||
include FasterCacheKeys
|
||||
include CacheMarkdownField
|
||||
|
||||
cache_markdown_field :note, pipeline: :note
|
||||
|
||||
# Attribute containing rendered and redacted Markdown as generated by
|
||||
# Banzai::ObjectRenderer.
|
||||
attr_accessor :note_html
|
||||
attr_accessor :redacted_note_html
|
||||
|
||||
# An Array containing the number of visible references as generated by
|
||||
# Banzai::ObjectRenderer
|
||||
|
|
|
@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
|
|||
include Gitlab::VisibilityLevel
|
||||
include Gitlab::CurrentSettings
|
||||
include AccessRequestable
|
||||
include CacheMarkdownField
|
||||
include Referable
|
||||
include Sortable
|
||||
include AfterCommitQueue
|
||||
|
@ -17,6 +18,8 @@ class Project < ActiveRecord::Base
|
|||
|
||||
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
|
||||
|
||||
default_value_for :archived, false
|
||||
|
@ -372,19 +375,9 @@ class Project < ActiveRecord::Base
|
|||
%r{(?<project>#{name_pattern}/#{name_pattern})}
|
||||
end
|
||||
|
||||
def trending(since = 1.month.ago)
|
||||
# By counting in the JOIN we don't expose the GROUP BY to the outer query.
|
||||
# This means that calls such as "any?" and "count" just return a number of
|
||||
# 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')
|
||||
def trending
|
||||
joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
|
||||
reorder('trending_projects.id ASC')
|
||||
end
|
||||
|
||||
def cached_count
|
||||
|
|
|
@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base
|
|||
belongs_to :group
|
||||
|
||||
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_access, presence: true
|
||||
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
class Release < ActiveRecord::Base
|
||||
include CacheMarkdownField
|
||||
|
||||
cache_markdown_field :description
|
||||
|
||||
belongs_to :project
|
||||
|
||||
validates :description, :project, :tag, presence: true
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue