Commit a9b86128 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream



See merge request !64
parents 4781a626 a48614c4
...@@ -87,4 +87,12 @@ flay: ...@@ -87,4 +87,12 @@ flay:
tags: tags:
- ruby - ruby
- mysql - mysql
bundler:audit:
script:
- "bundle exec bundle-audit update"
- "bundle exec bundle-audit check"
tags:
- ruby
- mysql
allow_failure: true allow_failure: true
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased) v 8.3.0 (unreleased)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
v 8.2.1
- Forcefully update builds that didn't want to update with state machine
- Fix: saving GitLabCiService as Admin Template
v 8.2.0 v 8.2.0
v 8.0.1
v 8.1.0 (unreleased)
v 8.2.0 (unreleased)
v 8.3.0 (unreleased)
v 8.2.0
- Improved performance of finding projects and groups in various places
- Improved performance of rendering user profile pages and Atom feeds
- Fix grouping of contributors by email in graph.
- Improved performance of finding projects and groups in various places
- Improved performance of rendering user profile pages and Atom feeds
- Expose build artifacts path as config option
- Fix grouping of contributors by email in graph.
- Improved performance of finding issues with/without labels
- Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
- Fix Drone CI service template not saving properly (Stan Hu) - Fix Drone CI service template not saving properly (Stan Hu)
- Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu) - Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu)
...@@ -10,6 +29,10 @@ v 8.2.0 ...@@ -10,6 +29,10 @@ v 8.2.0
- Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu)
- Improved performance of finding users by one of their Email addresses - Improved performance of finding users by one of their Email addresses
- Add allow_failure field to commit status API (Stan Hu) - Add allow_failure field to commit status API (Stan Hu)
- Commits without .gitlab-ci.yml are marked as skipped
- Save detailed error when YAML syntax is invalid
- Since GitLab CI is enabled by default, remove enabling it by pushing .gitlab-ci.yml
- Added build artifacts
- Improved performance of replacing references in comments - Improved performance of replacing references in comments
- Show last project commit to default branch on project home page - Show last project commit to default branch on project home page
- Highlight comment based on anchor in URL - Highlight comment based on anchor in URL
...@@ -27,6 +50,7 @@ v 8.2.0 ...@@ -27,6 +50,7 @@ v 8.2.0
- Allow to define cache in `.gitlab-ci.yml` - Allow to define cache in `.gitlab-ci.yml`
- Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
- Remove deprecated CI events from project settings page - Remove deprecated CI events from project settings page
- Improve personal snippet access workflow (Douglas Alexandre)
- [API] Add ability to fetch the commit ID of the last commit that actually touched a file - [API] Add ability to fetch the commit ID of the last commit that actually touched a file
- Fix omniauth documentation setting for omnibus configuration (Jon Cairns) - Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
- Add "New file" link to dropdown on project page - Add "New file" link to dropdown on project page
...@@ -34,6 +58,7 @@ v 8.2.0 ...@@ -34,6 +58,7 @@ v 8.2.0
- Add "added", "modified" and "removed" properties to commit object in webhook - Add "added", "modified" and "removed" properties to commit object in webhook
- Rename "Back to" links to "Go to" because its not always a case it point to place user come from - Rename "Back to" links to "Go to" because its not always a case it point to place user come from
- Allow groups to appear in the search results if the group owner allows it - Allow groups to appear in the search results if the group owner allows it
- Add email notification to former assignee upon unassignment (Adam Lieskovský)
- New design for project graphs page - New design for project graphs page
- Remove deprecated dumped yaml file generated from previous job definitions - Remove deprecated dumped yaml file generated from previous job definitions
- Fix incoming email config defaults - Fix incoming email config defaults
...@@ -47,6 +72,9 @@ v 8.2.0 ...@@ -47,6 +72,9 @@ v 8.2.0
- Fix trailing whitespace issue in merge request/issue title - Fix trailing whitespace issue in merge request/issue title
- Fix bug when milestone/label filter was empty for dashboard issues page - Fix bug when milestone/label filter was empty for dashboard issues page
- Add ability to create milestone in group projects from single form - Add ability to create milestone in group projects from single form
- Add option to create merge request when editing/creating a file (Dirceu Tiegs)
- Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
- Add Award Emoji to issue and merge request pages
v 8.1.4 v 8.1.4
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
...@@ -116,7 +144,6 @@ v 8.1.0 ...@@ -116,7 +144,6 @@ v 8.1.0
- Show CI status on Your projects page and Starred projects page - Show CI status on Your projects page and Starred projects page
- Remove "Continuous Integration" page from dashboard - Remove "Continuous Integration" page from dashboard
- Add notes and SSL verification entries to hook APIs (Ben Boeckel) - Add notes and SSL verification entries to hook APIs (Ben Boeckel)
- Added build artifacts
- Fix grammar in admin area "labels" .nothing-here-block when no labels exist. - Fix grammar in admin area "labels" .nothing-here-block when no labels exist.
- Move CI runners page to project settings area - Move CI runners page to project settings area
- Move CI variables page to project settings area - Move CI variables page to project settings area
......
...@@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic ...@@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic
## Security vulnerability disclosure ## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Closing policy for issues and merge requests ## Closing policy for issues and merge requests
...@@ -23,8 +23,8 @@ Issues and merge requests should be in English and contain appropriate language ...@@ -23,8 +23,8 @@ Issues and merge requests should be in English and contain appropriate language
## Helping others ## Helping others
Please help other GitLab users when you can. Please help other GitLab users when you can.
The channnels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/). The channels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/).
Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the irc channel. Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the IRC channel.
You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day.
## Issue tracker ## Issue tracker
...@@ -35,7 +35,7 @@ The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab ...@@ -35,7 +35,7 @@ The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab
Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple. Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple.
Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there. Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](https://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there.
### Issue tracker guidelines ### Issue tracker guidelines
...@@ -59,7 +59,7 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, and ...@@ -59,7 +59,7 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, and
Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls). Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls).
If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. If you are new to GitLab development (or web development in general), search for the label `easyfix` ([GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [GitHub](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint.
To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file.
...@@ -72,7 +72,7 @@ If you can, please submit a merge request with the fix or improvements including ...@@ -72,7 +72,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG) 1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message 1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message
1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) 1. If you have multiple commits please combine them into one commit by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
1. Push the commit to your fork 1. Push the commit to your fork
1. Submit a merge request (MR) to the master branch 1. Submit a merge request (MR) to the master branch
1. The MR title should describe the change you want to make 1. The MR title should describe the change you want to make
...@@ -99,7 +99,7 @@ If you contribute to GitLab please know that changes involve more than just code ...@@ -99,7 +99,7 @@ If you contribute to GitLab please know that changes involve more than just code
We have the following [definition of done](http://guide.agilealliance.org/guide/definition-of-done.html). We have the following [definition of done](http://guide.agilealliance.org/guide/definition-of-done.html).
Please ensure you support the feature you contribute through all of these steps. Please ensure you support the feature you contribute through all of these steps.
1. Description explaning the relevancy (see following item) 1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed 1. Working and clean code that is commented where needed
1. Unit and integration tests that pass on the CI server 1. Unit and integration tests that pass on the CI server
1. Documented in the /doc directory 1. Documented in the /doc directory
...@@ -163,7 +163,7 @@ If you add a dependency in GitLab (such as an operating system package) please c ...@@ -163,7 +163,7 @@ If you add a dependency in GitLab (such as an operating system package) please c
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. [Database Migrations](doc/development/migration_style_guide.md) 1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Documentation styleguide](doc_styleguide.md) 1. [Documentation styleguide](doc_styleguide.md)
1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing). 1. Interface text should be written subjectively instead of objectively. It should be the GitLab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing).
This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com). This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
...@@ -181,4 +181,4 @@ This code of conduct applies both within project spaces and in public spaces whe ...@@ -181,4 +181,4 @@ This code of conduct applies both within project spaces and in public spaces whe
Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
source "https://rubygems.org" source "https://rubygems.org"
gem 'rails', '4.1.12' gem 'rails', '4.1.14'
# Specify a sprockets version due to security issue # Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY # See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
...@@ -265,6 +265,7 @@ group :development, :test do ...@@ -265,6 +265,7 @@ group :development, :test do
gem 'simplecov', '~> 0.10.0', require: false gem 'simplecov', '~> 0.10.0', require: false
gem 'flog', require: false gem 'flog', require: false
gem 'flay', require: false gem 'flay', require: false
gem 'bundler-audit', require: false
gem 'benchmark-ips', require: false gem 'benchmark-ips', require: false
end end
......
...@@ -4,25 +4,25 @@ GEM ...@@ -4,25 +4,25 @@ GEM
CFPropertyList (2.3.1) CFPropertyList (2.3.1)
RedCloth (4.2.9) RedCloth (4.2.9)
ace-rails-ap (2.0.1) ace-rails-ap (2.0.1)
actionmailer (4.1.12) actionmailer (4.1.14)
actionpack (= 4.1.12) actionpack (= 4.1.14)
actionview (= 4.1.12) actionview (= 4.1.14)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
actionpack (4.1.12) actionpack (4.1.14)
actionview (= 4.1.12) actionview (= 4.1.14)
activesupport (= 4.1.12) activesupport (= 4.1.14)
rack (~> 1.5.2) rack (~> 1.5.2)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
actionview (4.1.12) actionview (4.1.14)
activesupport (= 4.1.12) activesupport (= 4.1.14)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
activemodel (4.1.12) activemodel (4.1.14)
activesupport (= 4.1.12) activesupport (= 4.1.14)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.1.12) activerecord (4.1.14)
activemodel (= 4.1.12) activemodel (= 4.1.14)
activesupport (= 4.1.12) activesupport (= 4.1.14)
arel (~> 5.0.0) arel (~> 5.0.0)
activerecord-deprecated_finders (1.0.4) activerecord-deprecated_finders (1.0.4)
activerecord-session_store (0.1.1) activerecord-session_store (0.1.1)
...@@ -33,7 +33,7 @@ GEM ...@@ -33,7 +33,7 @@ GEM
activemodel (~> 4.0) activemodel (~> 4.0)
activesupport (~> 4.0) activesupport (~> 4.0)
rails-observers (~> 0.1.1) rails-observers (~> 0.1.1)
activesupport (4.1.12) activesupport (4.1.14)
i18n (~> 0.6, >= 0.6.9) i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
...@@ -90,6 +90,9 @@ GEM ...@@ -90,6 +90,9 @@ GEM
bullet (4.14.9) bullet (4.14.9)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0) uniform_notifier (~> 1.9.0)
bundler-audit (0.4.0)
bundler (~> 1.2)
thor (~> 0.18)
byebug (6.0.2) byebug (6.0.2)
cal-heatmap-rails (0.0.1) cal-heatmap-rails (0.0.1)
capybara (2.4.4) capybara (2.4.4)
...@@ -512,21 +515,21 @@ GEM ...@@ -512,21 +515,21 @@ GEM
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.1.12) rails (4.1.14)
actionmailer (= 4.1.12) actionmailer (= 4.1.14)
actionpack (= 4.1.12) actionpack (= 4.1.14)
actionview (= 4.1.12) actionview (= 4.1.14)
activemodel (= 4.1.12) activemodel (= 4.1.14)
activerecord (= 4.1.12) activerecord (= 4.1.14)
activesupport (= 4.1.12) activesupport (= 4.1.14)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.1.12) railties (= 4.1.14)
sprockets-rails (~> 2.0) sprockets-rails (~> 2.0)
rails-observers (0.1.2) rails-observers (0.1.2)
activemodel (~> 4.0) activemodel (~> 4.0)
railties (4.1.12) railties (4.1.14)
actionpack (= 4.1.12) actionpack (= 4.1.14)
activesupport (= 4.1.12) activesupport (= 4.1.14)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.0.0) rainbow (2.0.0)
...@@ -690,7 +693,7 @@ GEM ...@@ -690,7 +693,7 @@ GEM
multi_json (~> 1.0) multi_json (~> 1.0)
rack (~> 1.0) rack (~> 1.0)
tilt (~> 1.1, != 1.3.0) tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.3.2) sprockets-rails (2.3.3)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
...@@ -805,6 +808,7 @@ DEPENDENCIES ...@@ -805,6 +808,7 @@ DEPENDENCIES
brakeman (= 3.0.1) brakeman (= 3.0.1)
browser (~> 1.0.0) browser (~> 1.0.0)
bullet bullet
bundler-audit
byebug byebug
cal-heatmap-rails (~> 0.0.1) cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.4.0) capybara (~> 2.4.0)
...@@ -892,7 +896,7 @@ DEPENDENCIES ...@@ -892,7 +896,7 @@ DEPENDENCIES
rack-attack (~> 4.3.0) rack-attack (~> 4.3.0)
rack-cors (~> 0.4.0) rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.0.5) rack-oauth2 (~> 1.0.5)
rails (= 4.1.12) rails (= 4.1.14)
raphael-rails (~> 2.1.2) raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
......
...@@ -40,7 +40,12 @@ Workflow labels are purposely not very detailed since that would be hard to keep ...@@ -40,7 +40,12 @@ Workflow labels are purposely not very detailed since that would be hard to keep
- *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away) - *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away)
- *Attached MR*: There is a MR attached and the discussion should happen there - *Attached MR*: There is a MR attached and the discussion should happen there
- We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay. - We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay.
- *Awaiting developer action/feedback*: Issue needs to be fixed or clarified by a developer - *Developer*: needs help from a developer
- *UX* needs needs help from a UX designer
- *Frontend* needs help from a Front-end engineer
- *Graphics* needs help from a Graphics designer
Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
## Functional labels ## Functional labels
......
...@@ -53,8 +53,6 @@ There are two editions of GitLab: ...@@ -53,8 +53,6 @@ There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license. - GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). - GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
Included with the GitLab Omnibus Packages is [GitLab CI](https://about.gitlab.com/gitlab-ci/) that can easily build, test and deploy code.
## Website ## Website
On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: On [about.gitlab.com](https://about.gitlab.com/) you can find more information about:
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id) ->
addAward: (emoji) ->
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
addAwardToEmojiBar: (emoji, custom_path = '') ->
if @exist(emoji)
if @isActive(emoji)
@decrementCounter(emoji)
else
counter = @findEmojiIcon(emoji).siblings(".counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
else
@createEmoji(emoji, custom_path)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
isActive: (emoji) ->
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
counter.parent().removeClass("active")
@removeMeFromAuthorList(emoji)
else
award = counter.parent()
award.tooltip("destroy")
award.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors = _.without(authors, "me").join(", ")
award_block.attr("title", authors)
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors.push("me")
award_block.attr("title", authors.join(", "))
@resetTooltip(award_block)
resetTooltip: (award) ->
award.tooltip("destroy")
# "destroy" call is asynchronous, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
createEmoji: (emoji, custom_path) ->
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon' data-emoji='" + emoji + "'>")
nodes.push(@getImage(emoji, custom_path))
nodes.push("</div>")
nodes.push("<div class='counter'>1")
nodes.push("</div></div>")
$(".awards-controls").before(nodes.join("\n"))
$(".award").tooltip()
getImage: (emoji, custom_path) ->
if custom_path
$("<img>").attr({src: custom_path, width: 20, height: 20}).wrap("<div>").parent().html()
else
$("li[data-emoji='" + emoji + "']").html()
postEmoji: (emoji, callback) ->
$.post @post_emoji_url, { note: {
note: ":" + emoji + ":"
noteable_type: @noteable_type
noteable_id: @noteable_id
}},(data) ->
if data.ok
callback.call()
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
\ No newline at end of file
...@@ -23,18 +23,6 @@ class @BlobFileDropzone ...@@ -23,18 +23,6 @@ class @BlobFileDropzone
init: -> init: ->
this.on 'addedfile', (file) -> this.on 'addedfile', (file) ->
$('.dropzone-alerts').html('').hide() $('.dropzone-alerts').html('').hide()
commit_message = form.find('#commit_message')[0]
if /^Upload/.test(commit_message.placeholder)
commit_message.placeholder = 'Upload ' + file.name
return
this.on 'removedfile', (file) ->
commit_message = form.find('#commit_message')[0]
if /^Upload/.test(commit_message.placeholder)
commit_message.placeholder = 'Upload new file'
return return
...@@ -47,8 +35,9 @@ class @BlobFileDropzone ...@@ -47,8 +35,9 @@ class @BlobFileDropzone
return return
this.on 'sending', (file, xhr, formData) -> this.on 'sending', (file, xhr, formData) ->
formData.append('new_branch', form.find('#new_branch').val()) formData.append('new_branch', form.find('.js-new-branch').val())
formData.append('commit_message', form.find('#commit_message').val()) formData.append('create_merge_request', form.find('.js-create-merge-request').val())
formData.append('commit_message', form.find('.js-commit-message').val())
return return
# Override behavior of adding error underneath preview # Override behavior of adding error underneath preview
......
...@@ -9,13 +9,24 @@ $ -> ...@@ -9,13 +9,24 @@ $ ->
clipboard.on 'success', (e) -> clipboard.on 'success', (e) ->
$(e.trigger). $(e.trigger).
tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!'). tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!').
tooltip('show') tooltip('show').
one('mouseleave', -> $(this).tooltip('hide'))
# Clear the selection and blur the trigger so it loses its border # Clear the selection and blur the trigger so it loses its border
e.clearSelection() e.clearSelection()
$(e.trigger).blur() $(e.trigger).blur()
# Manually hide the tooltip after 1 second # Safari doesn't support `execCommand`, so instead we inform the user to
setTimeout(-> # copy manually.
$(e.trigger).tooltip('hide') #
, 1000) # See http://clipboardjs.com/#browser-support
clipboard.on 'error', (e) ->
if /Mac/i.test(navigator.userAgent)
title = "Press &#8984;-C to copy"
else
title = "Press Ctrl-C to copy"
$(e.trigger).
tooltip(trigger: 'manual', placement: 'auto bottom', html: true, title: title).
tooltip('show').
one('mouseleave', -> $(this).tooltip('hide'))
...@@ -28,6 +28,8 @@ class Dispatcher ...@@ -28,6 +28,8 @@ class Dispatcher
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
new DropzoneInput($('.milestone-form')) new DropzoneInput($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
when 'projects:compare:show' when 'projects:compare:show'
new Diff() new Diff()
when 'projects:issues:new','projects:issues:edit' when 'projects:issues:new','projects:issues:edit'
......
class @NewCommitForm
constructor: (form) ->
@newBranch = form.find('.js-new-branch')
@originalBranch = form.find('.js-original-branch')
@createMergeRequest = form.find('.js-create-merge-request')
@createMergeRequestFormGroup = form.find('.js-create-merge-request-form-group')
@renderDestination()
@newBranch.keyup @renderDestination
renderDestination: =>
different = @newBranch.val() != @originalBranch.val()
if different
@createMergeRequestFormGroup.show()
@createMergeRequest.prop('checked', true) unless @wasDifferent
else
@createMergeRequestFormGroup.hide()
@createMergeRequest.prop('checked', false)
@wasDifferent = different
...@@ -29,6 +29,7 @@ class @Notes ...@@ -29,6 +29,7 @@ class @Notes
$(document).on "ajax:success", "form.edit_note", @updateNote $(document).on "ajax:success", "form.edit_note", @updateNote
# Edit note link # Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
$(document).on "click", ".note-edit-cancel", @cancelEdit $(document).on "click", ".note-edit-cancel", @cancelEdit
# Reopen and close actions for Issue/MR combined with note form submit # Reopen and close actions for Issue/MR combined with note form submit
...@@ -66,6 +67,7 @@ class @Notes ...@@ -66,6 +67,7 @@ class @Notes
$(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form" $(document).off "ajax:success", ".js-discussion-note-form"
$(document).off "ajax:success", "form.edit_note" $(document).off "ajax:success", "form.edit_note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel" $(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-delete"
$(document).off "click", ".js-note-attachment-delete" $(document).off "click", ".js-note-attachment-delete"
...@@ -111,13 +113,16 @@ class @Notes ...@@ -111,13 +113,16 @@ class @Notes
renderNote: (note) -> renderNote: (note) ->
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
if @isNewNote(note) if @isNewNote(note) && !note.award
@note_ids.push(note.id) @note_ids.push(note.id)
$('ul.main-notes-list'). $('ul.main-notes-list').
append(note.html). append(note.html).
syntaxHighlight() syntaxHighlight()
@initTaskList() @initTaskList()
if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
### ###
Check if note does not exists on page Check if note does not exists on page
### ###
...@@ -253,7 +258,6 @@ class @Notes ...@@ -253,7 +258,6 @@ class @Notes
### ###
addNote: (xhr, note, status) => addNote: (xhr, note, status) =>
@renderNote(note) @renderNote(note)
@updateVotes()
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -285,14 +289,13 @@ class @Notes ...@@ -285,14 +289,13 @@ class @Notes
Adds a hidden div with the original content of the note to fill the edit note form with Adds a hidden div with the original content of the note to fill the edit note form with
if the user cancels if the user cancels
### ###
showEditForm: (note, formHTML) -> showEditForm: (e) ->
nodeText = note.find(".note-text"); e.preventDefault()
nodeText.hide() note = $(this).closest(".note")
note.find('.note-edit-form').remove()
nodeText.after(formHTML)
note.find(".note-body > .note-text").hide() note.find(".note-body > .note-text").hide()
note.find(".note-header").hide() note.find(".note-header").hide()
form = note.find(".note-edit-form") base_form = note.find(".note-edit-form")
form = base_form.clone().insertAfter(base_form)
form.addClass('current-note-edit-form gfm-form') form.addClass('current-note-edit-form gfm-form')
form.find('.div-dropzone').remove() form.find('.div-dropzone').remove()
...@@ -472,9 +475,6 @@ class @Notes ...@@ -472,9 +475,6 @@ class @Notes
form = $(e.target).closest(".js-discussion-note-form") form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form) @removeDiscussionNoteForm(form)
updateVotes: ->
true
### ###
Called after an attachment file has been selected. Called after an attachment file has been selected.
......
...@@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil = ...@@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil =
for entry in log for entry in log
@add_date(entry.date, total) unless total[entry.date]? @add_date(entry.date, total) unless total[entry.date]?
data = by_author[entry.author_name] #|| by_email[entry.author_email] data = by_author[entry.author_name] || by_email[entry.author_email]
data ?= @add_author(entry, by_author, by_email) data ?= @add_author(entry, by_author, by_email)
@add_date(entry.date, data) unless data[entry.date] @add_date(entry.date, data) unless data[entry.date]
...@@ -96,4 +96,3 @@ window.ContributorsStatGraphUtil = ...@@ -96,4 +96,3 @@ window.ContributorsStatGraphUtil =
true true
else else
false false
\ No newline at end of file
...@@ -60,11 +60,8 @@ class @UsersSelect ...@@ -60,11 +60,8 @@ class @UsersSelect
query.callback(data) query.callback(data)
initSelection: (element, callback) => initSelection: (args...) =>
id = $(element).val() @initSelection(args...)
if id != "" && id != "0"
@user(id, callback)
formatResult: (args...) => formatResult: (args...) =>
@formatResult(args...) @formatResult(args...)
formatSelection: (args...) => formatSelection: (args...) =>
...@@ -73,6 +70,14 @@ class @UsersSelect ...@@ -73,6 +70,14 @@ class @UsersSelect
escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
m m
initSelection: (element, callback) ->
id = $(element).val()
if id == "0"
nullUser = { name: 'Unassigned' }
callback(nullUser)
else if id != ""
@user(id, callback)
formatResult: (user) -> formatResult: (user) ->
if user.avatar_url if user.avatar_url
avatar = user.avatar_url avatar = user.avatar_url
......
...@@ -64,7 +64,7 @@ pre { ...@@ -64,7 +64,7 @@ pre {
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus { .dropdown-menu > li > a:focus {
background: $gl-primary; background: $gl-primary;
color: #FFF color: #FFF;
} }
.str-truncated { .str-truncated {
......
...@@ -190,6 +190,10 @@ ...@@ -190,6 +190,10 @@
.btn { .btn {
min-width: 124px; min-width: 124px;
} }
.btn-clipboard {
min-width: 0px;
}
} }
&.panel-small { &.panel-small {
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.autoscroll-container { .autoscroll-container {
position: fixed; position: fixed;
bottom: 10px; bottom: 20px;
right: 20px; right: 20px;
z-index: 100; z-index: 100;
} }
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
a { a {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 10px;
} }
} }
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
li { li {
padding: 3px 0px; padding: 3px 0px;
line-height: 20px;
} }
} }
.new-file { .new-file {
......
...@@ -101,3 +101,71 @@ ...@@ -101,3 +101,71 @@
background-color: $background-color; background-color: $background-color;
} }
} }
.awards {
@include clearfix;
line-height: 34px;
margin: 2px 0;
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin: 0 5px;
border-color: $border-color;
cursor: pointer;
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
}
.awards-menu {
padding: $gl-padding;
min-width: 214px;
> li {
margin: 5px;
}
}
}
.awards-menu{
li {
float: left;
margin: 3px;
}
}
}
...@@ -15,10 +15,10 @@ module Ci ...@@ -15,10 +15,10 @@ module Ci
@builds = @config_processor.builds @builds = @config_processor.builds
@status = true @status = true
end end
rescue Ci::GitlabCiYamlProcessor::ValidationError => e rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
@error = e.message @error = e.message
@status = false @status = false
rescue Exception rescue
@error = "Undefined error" @error = "Undefined error"
@status = false @status = false
end end
......
module CreatesMergeRequestForCommit
extend ActiveSupport::Concern
def new_merge_request_path
if @project.forked?
target_project = @project.forked_from_project || @project
target_branch = target_project.repository.root_ref
else
target_project = @project
target_branch = @ref
end
new_namespace_project_merge_request_path(
@project.namespace,
@project,
merge_request: {
source_project_id: @project.id,
target_project_id: target_project.id,
source_branch: @new_branch,
target_branch: target_branch
}
)
end
def create_merge_request?
params[:create_merge_request] && @new_branch != @ref
end
end
module GlobalMilestones
extend ActiveSupport::Concern
def milestones
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end
def milestone
milestones = Milestone.of_projects(@projects).where(title: params[:title])
if milestones.present?
@milestone = GlobalMilestone.new(params[:title], milestones)
else
render_404
end
end
end
module IssuesAction
extend ActiveSupport::Concern
def issues
@issues = get_issues_collection
@issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
@issues = @issues.preload(:author, :project)
respond_to do |format|
format.html
format.atom { render layout: false }
end
end
end
module MergeRequestsAction
extend ActiveSupport::Concern
def merge_requests
@merge_requests = get_merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
end
end
class Dashboard::MilestonesController < Dashboard::ApplicationController class Dashboard::MilestonesController < Dashboard::ApplicationController
before_action :load_projects include GlobalMilestones
before_action :projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show]
def index def index
project_milestones = case params[:state]
when 'all'; state
when 'closed'; state('closed')
else state('active')
end
@dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
@dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE)
end end
def show def show
project_milestones = Milestone.where(project_id: @projects).order("due_date ASC")
@dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
end end
private private
def load_projects def projects
@projects = current_user.authorized_projects.sorted_by_activity.non_archived @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
def title
params[:title]
end
def state(state = nil)
conditions = { project_id: @projects }
conditions.reverse_merge!(state: state) if state
Milestone.where(conditions).order("title ASC")
end end
end end
class DashboardController < Dashboard::ApplicationController class DashboardController < Dashboard::ApplicationController
include IssuesAction
include MergeRequestsAction
before_action :event_filter, only: :activity before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
respond_to :html respond_to :html
def merge_requests
@merge_requests = get_merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
end
def issues
@issues = get_issues_collection
@issues = @issues.page(params[:page]).per(PER_PAGE)
@issues = @issues.preload(:author, :project)
respond_to do |format|
format.html
format.atom { render layout: false }
end
end
def activity def activity
@last_push = current_user.recent_push @last_push = current_user.recent_push
...@@ -47,4 +34,8 @@ class DashboardController < Dashboard::ApplicationController ...@@ -47,4 +34,8 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations @events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end end
class Groups::ApplicationController < ApplicationController class Groups::ApplicationController < ApplicationController
layout 'group' layout 'group'
before_action :group
private private
......
class Groups::AvatarsController < ApplicationController class Groups::AvatarsController < Groups::ApplicationController
def destroy def destroy
@group = Group.find_by(path: params[:group_id])
@group.remove_avatar! @group.remove_avatar!
@group.save @group.save
redirect_to edit_group_path(@group) redirect_to edit_group_path(@group)
......
class Groups::GroupMembersController < Groups::ApplicationController class Groups::GroupMembersController < Groups::ApplicationController
skip_before_action :authenticate_user!, only: [:index] skip_before_action :authenticate_user!, only: [:index]
before_action :group
# Authorize # Authorize
before_action :authorize_read_group! before_action :authorize_read_group!
before_action :authorize_admin_group!, except: [:index, :leave] before_action :authorize_admin_group_member!, except: [:index, :leave]
before_action :authorize_admin_group_member!, only: [:create, :resend_invite]
def index def index
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
...@@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
@members = @members.order('access_level DESC').page(params[:page]).per(50) @members = @members.order('access_level DESC').page(params[:page]).per(50)
@group_member = GroupMember.new
@group_member = @group.group_members.new
end end
def create def create
...@@ -36,21 +35,22 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -36,21 +35,22 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def update def update
@member = @group.group_members.find(params[:id]) @group_member = @group.group_members.find(params[:id])
return render_403 unless can?(current_user, :update_group_member, @member) return render_403 unless can?(current_user, :update_group_member, @group_member)
old_access_level = @member.human_access old_access_level = @group_member.human_access
if @member.update_attributes(member_params) if @group_member.update_attributes(member_params)
log_audit_event(@member, action: :update, old_access_level: old_access_level) log_audit_event(@group_member, action: :update, old_access_level: old_access_level)
end end
end end
def destroy def destroy
@group_member = @group.group_members.find(params[:id]) @group_member = @group.group_members.find(params[:id])
if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. return render_403 unless can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy @group_member.destroy
log_audit_event(@group_member, action: :destroy) log_audit_event(@group_member, action: :destroy)
...@@ -58,9 +58,6 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -58,9 +58,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
format.js { render nothing: true } format.js { render nothing: true }
end end
else
return render_403
end
end end
def resend_invite def resend_invite
...@@ -78,7 +75,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -78,7 +75,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def leave def leave
@group_member = @group.group_members.where(user_id: current_user.id).first @group_member = @group.group_members.find_by(user_id: current_user)
if can?(current_user, :destroy_group_member, @group_member) if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy @group_member.destroy
......
class Groups::MilestonesController < Groups::ApplicationController class Groups::MilestonesController < Groups::ApplicationController
before_action :authorize_group_milestone!, only: :update include GlobalMilestones
before_action :projects
before_action :milestones, only: [:index]
before_action :milestone, only: [:show, :update]
before_action :authorize_group_milestone!, only: [:create, :update]
def index def index
project_milestones = case params[:state]
when 'all'; state
when 'closed'; state('closed')
else state('active')
end
@group_milestones = Milestones::GroupService.new(project_milestones).execute
@group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE)
end end
def show def new
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC") @milestone = Milestone.new
@group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
end end
def update def create
project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC") project_ids = params[:milestone][:project_ids]
@group_milestones = Milestones::GroupService.new(project_milestones).milestone(title) title = milestone_params[:title]
@group.projects.where(id: project_ids).each do |project|
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
@group_milestones.milestones.each do |milestone| redirect_to milestone_path(title)
Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone)
end end
respond_to do |format| def show
format.js
format.html do
redirect_to group_milestones_path(group)
end end
def update
@milestone.milestones.each do |milestone|
Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end end
redirect_back_or_default(default: milestone_path(@milestone.title))
end end
private private
def group def authorize_group_milestone!
@group ||= Group.find_by(path: params[:group_id]) return render_404 unless can?(current_user, :admin_milestones, group)
end end
def title def milestone_params
params[:title] params.require(:milestone).permit(:title, :description, :due_date, :state_event)
end end
def state(state = nil) def milestone_path(title)
conditions = { project_id: group.projects } group_milestone_path(@group, title.parameterize, title: title)
conditions.reverse_merge!(state: state) if state
Milestone.where(conditions).order("title ASC")
end end
def authorize_group_milestone! def projects
return render_404 unless can?(current_user, :admin_group, group) @projects ||= @group.projects
end end
end end
class GroupsController < Groups::ApplicationController class GroupsController < Groups::ApplicationController
include IssuesAction
include MergeRequestsAction
skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests]
respond_to :html respond_to :html
before_action :group, except: [:new, :create] before_action :group, except: [:new, :create]
...@@ -55,23 +58,6 @@ class GroupsController < Groups::ApplicationController ...@@ -55,23 +58,6 @@ class GroupsController < Groups::ApplicationController
end end
end end
def merge_requests
@merge_requests = get_merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
end
def issues
@issues = get_issues_collection
@issues = @issues.page(params[:page]).per(PER_PAGE)
@issues = @issues.preload(:author, :project)
respond_to do |format|
format.html
format.atom { render layout: false }
end
end
def edit def edit
end end
......
# Controller for viewing a file's blame # Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController class Projects::BlobController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include CreatesMergeRequestForCommit
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path # Raised when given an invalid file path
...@@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController
end end
def create def create
result = Files::CreateService.new(@project, current_user, @commit_params).execute create_commit(Files::CreateService, success_path: after_create_path,
failure_view: :new,
if result[:status] == :success failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
flash[:notice] = "The changes have been successfully committed"
respond_to do |format|
format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }
format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render :new }
format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } }
end
end
end end
def show def show
...@@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController
end end
def update def update
result = Files::UpdateService.new(@project, current_user, @commit_params).execute create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
if result[:status] == :success failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
flash[:notice] = "Your changes have been successfully committed"
respond_to do |format|
format.html { redirect_to after_edit_path }
format.json { render json: { message: "success", filePath: after_edit_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render :edit }
format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
end
end
end end
def preview def preview
...@@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController
if result[:status] == :success if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed" flash[:notice] = "Your changes have been successfully committed"
redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) redirect_to after_destroy_path
else else
flash[:alert] = result[:message] flash[:alert] = result[:message]
render :show render :show
...@@ -131,15 +108,51 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -131,15 +108,51 @@ class Projects::BlobController < Projects::ApplicationController
render_404 render_404
end end
def create_commit(service, success_path:, failure_view:, failure_path:)
result = service.new(@project, current_user, @commit_params).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render failure_view }
format.json { render json: { message: "failed", filePath: failure_path } }
end
end
end
def after_create_path
@after_create_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path))
end
end
def after_edit_path def after_edit_path
@after_edit_path ||= @after_edit_path ||=
if from_merge_request if create_merge_request?
new_merge_request_path
elsif from_merge_request && @new_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}" "#file-path-#{hexdigest(@path)}"
elsif @target_branch.present?
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
else else
namespace_project_blob_path(@project.namespace, @project, @id) namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path))
end
end
def after_destroy_path
@after_destroy_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_tree_path(@project.namespace, @project, @new_branch)
end end
end end
...@@ -154,7 +167,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -154,7 +167,7 @@ class Projects::BlobController < Projects::ApplicationController
def editor_variables def editor_variables
@current_branch = @ref @current_branch = @ref
@target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref @new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref
@file_path = @file_path =
if action_name.to_s == 'create' if action_name.to_s == 'create'
...@@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = { @commit_params = {
file_path: @file_path, file_path: @file_path,
current_branch: @current_branch, current_branch: @current_branch,
target_branch: @target_branch, target_branch: @new_branch,
commit_message: params[:commit_message], commit_message: params[:commit_message],
file_content: params[:content], file_content: params[:content],
file_content_encoding: params[:encoding] file_content_encoding: params[:encoding]
......
...@@ -20,8 +20,8 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -20,8 +20,8 @@ class Projects::CompareController < Projects::ApplicationController
if compare_result if compare_result
@commits = Commit.decorate(compare_result.commits, @project) @commits = Commit.decorate(compare_result.commits, @project)
@diffs = compare_result.diffs @diffs = compare_result.diffs
@commit = @commits.last @commit = @project.commit(head_ref)
@first_commit = @commits.first @first_commit = @project.commit(base_ref)
@line_notes = [] @line_notes = []
end end
end end
......
...@@ -66,7 +66,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -66,7 +66,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show def show
@participants = @issue.participants(current_user) @participants = @issue.participants(current_user)
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.with_associations.fresh @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue @noteable = @issue
respond_with(@issue) respond_with(@issue)
......
...@@ -294,7 +294,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -294,7 +294,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
@discussions = Note.discussions_from_notes(@notes) @discussions = Note.discussions_from_notes(@notes)
@noteable = @merge_request @noteable = @merge_request
......
...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :edit, :delete_attachment] before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
def index def index
current_fetched_at = Time.now.to_i current_fetched_at = Time.now.to_i
...@@ -29,11 +29,6 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -29,11 +29,6 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def edit
@note = note
render layout: false
end
def update def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note) @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
...@@ -63,6 +58,30 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -63,6 +58,30 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def award_toggle
noteable = if note_params[:noteable_type] == "issue"
project.issues.find(note_params[:noteable_id])
else
project.merge_requests.find(note_params[:noteable_id])
end
data = {
author: current_user,
is_award: true,
note: note_params[:note].gsub(":", '')
}
note = noteable.notes.find_by(data)
if note
note.destroy
else
Notes::CreateService.new(project, current_user, note_params).execute
end
render json: { ok: true }
end
private private
def note def note
...@@ -116,6 +135,9 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -116,6 +135,9 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id, id: note.id,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_to_html(note), html: note_to_html(note),
award: note.is_award,
emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
note: note.note,
discussion_html: note_to_discussion_html(note), discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note) discussion_with_diff_html: note_to_discussion_with_diff_html(note)
} }
......
class Projects::ProjectMembersController < Projects::ApplicationController class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project!, except: :leave before_action :authorize_admin_project_member!, except: :leave
def index def index
@project_members = @project.project_members @project_members = @project.project_members
...@@ -30,10 +30,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -30,10 +30,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_group_links = @project.project_group_links @project_group_links = @project.project_group_links
end end
def new
@project_member = @project.project_members.new
end
def create def create
@project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
members = @project.project_members.where(user_id: params[:user_ids].split(',')) members = @project.project_members.where(user_id: params[:user_ids].split(','))
...@@ -47,6 +43,9 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -47,6 +43,9 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def update def update
@project_member = @project.project_members.find(params[:id]) @project_member = @project.project_members.find(params[:id])
return render_403 unless can?(current_user, :update_project_member, @project_member)
old_access_level = @project_member.human_access old_access_level = @project_member.human_access
if @project_member.update_attributes(member_params) if @project_member.update_attributes(member_params)
...@@ -56,7 +55,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -56,7 +55,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def destroy def destroy
@project_member = @project.project_members.find(params[:id]) @project_member = @project.project_members.find(params[:id])
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy @project_member.destroy
log_audit_event(@project_member, action: :destroy) log_audit_event(@project_member, action: :destroy)
respond_to do |format| respond_to do |format|
...@@ -82,19 +85,25 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -82,19 +85,25 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def leave def leave
if @project.namespace == current_user.namespace
message = 'You can not leave your own project. Transfer or delete the project.'
return redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
end
@project_member = @project.project_members.find_by(user_id: current_user) @project_member = @project.project_members.find_by(user_id: current_user)
if can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy @project_member.destroy
log_audit_event(@project_member, action: :destroy) log_audit_event(@project_member, action: :destroy)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_projects_path } format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { render nothing: true } format.js { render nothing: true }
end end
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
else
render_403
end
end
end end
def apply_import def apply_import
......
# Controller for viewing a repository's file structure # Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController class Projects::TreeController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include CreatesMergeRequestForCommit
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
before_action :require_non_empty_project, except: [:new, :create] before_action :require_non_empty_project, except: [:new, :create]
...@@ -43,7 +44,7 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -43,7 +44,7 @@ class Projects::TreeController < Projects::ApplicationController
if result && result[:status] == :success if result && result[:status] == :success
flash[:notice] = "The directory has been successfully created" flash[:notice] = "The directory has been successfully created"
respond_to do |format| respond_to do |format|
format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) } format.html { redirect_to after_create_dir_path }
end end
else else
flash[:alert] = message flash[:alert] = message
...@@ -53,6 +54,8 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -53,6 +54,8 @@ class Projects::TreeController < Projects::ApplicationController
end end
end end
private
def assign_dir_vars def assign_dir_vars
@new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
@dir_name = File.join(@path, params[:dir_name]) @dir_name = File.join(@path, params[:dir_name])
...@@ -63,4 +66,12 @@ class Projects::TreeController < Projects::ApplicationController ...@@ -63,4 +66,12 @@ class Projects::TreeController < Projects::ApplicationController
commit_message: params[:commit_message], commit_message: params[:commit_message],
} }
end end
def after_create_dir_path
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name))
end
end
end end
class SnippetsController < ApplicationController class SnippetsController < ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
before_action :authorize_read_snippet!, only: [:show]
# Allow modify snippet # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_update_snippet!, only: [:edit, :update]
...@@ -79,8 +82,12 @@ class SnippetsController < ApplicationController ...@@ -79,8 +82,12 @@ class SnippetsController < ApplicationController
[Snippet::PUBLIC, Snippet::INTERNAL]). [Snippet::PUBLIC, Snippet::INTERNAL]).
find(params[:id]) find(params[:id])
else else
PersonalSnippet.are_public.find(params[:id]) PersonalSnippet.find(params[:id])
end
end end
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
end end
def authorize_update_snippet! def authorize_update_snippet!
......
...@@ -3,14 +3,11 @@ class UsersController < ApplicationController ...@@ -3,14 +3,11 @@ class UsersController < ApplicationController
before_action :set_user before_action :set_user
def show def show
@contributed_projects = contributed_projects.joined(@user). @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
reject(&:forked?)
@projects = @user.personal_projects. @projects = PersonalProjectsFinder.new(@user).execute(current_user)
where(id: authorized_projects_ids).includes(:namespace)
# Collect only groups common for both users @groups = JoinedGroupsFinder.new(@user).execute(current_user)
@groups = @user.groups & GroupsFinder.new.execute(current_user)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -53,16 +50,8 @@ class UsersController < ApplicationController ...@@ -53,16 +50,8 @@ class UsersController < ApplicationController
@user = User.find_by_username!(params[:username]) @user = User.find_by_username!(params[:username])
end end
def authorized_projects_ids
# Projects user can view
@authorized_projects_ids ||=
ProjectsFinder.new.execute(current_user).pluck(:id)
end
def contributed_projects def contributed_projects
@contributed_projects = Project. ContributedProjectsFinder.new(@user).execute(current_user)
where(id: authorized_projects_ids & @user.contributed_projects_ids).
includes(:namespace)
end end
def contributions_calendar def contributions_calendar
...@@ -73,9 +62,13 @@ class UsersController < ApplicationController ...@@ -73,9 +62,13 @@ class UsersController < ApplicationController
def load_events def load_events
# Get user activity feed for projects common for both users # Get user activity feed for projects common for both users
@events = @user.recent_events. @events = @user.recent_events.
where(project_id: authorized_projects_ids). merge(projects_for_current_user).
with_associations references(:project).
with_associations.
limit_recent(20, params[:offset])
end
@events = @events.limit(20).offset(params[:offset] || 0) def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end end
end end
class ContributedProjectsFinder
def initialize(user)
@user = user
end
# Finds the projects "@user" contributed to, limited to either public projects
# or projects visible to the given user.
#
# current_user - When given the list of the projects is limited to those only
# visible by this user.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
relation.includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.contributed_projects.visible_to_user(current_user)
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_projects.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.contributed_projects.public_only
end
end
class GroupsFinder class GroupsFinder
def execute(current_user, options = {}) # Finds the groups available to the given user.
all_groups(current_user) #
# current_user - The user to find the groups for.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end end
private private
def all_groups(current_user) # This method returns the groups "current_user" can see.
group_ids = if current_user def groups_visible_to_user(current_user)
if current_user.authorized_groups.any? base = groups_for_projects(public_and_internal_projects)
# User has access to groups
# union = Gitlab::SQL::Union.
# Return only: new([base.select(:id), current_user.authorized_groups.select(:id)])
# groups with public projects
# groups with internal projects Group.where("namespaces.id IN (#{union.to_sql})")
# groups with joined projects
#
Project.public_and_internal_only.pluck(:namespace_id) +
current_user.authorized_groups.pluck(:id)
else
# User has no group membership
#
# Return only:
# groups with public projects
# groups with internal projects
#
Project.public_and_internal_only.pluck(:namespace_id)
end end
else
# Not authenticated def public_groups
# groups_for_projects(public_projects)
# Return only: end
# groups with public projects
Project.public_only.pluck(:namespace_id) def groups_for_projects(projects)
Group.public_and_given_groups(projects.select(:namespace_id))
end
def public_projects
Project.unscoped.public_only
end end
Group.where("public IS TRUE OR id IN(?)", group_ids) def public_and_internal_projects
Project.unscoped.public_and_internal_only
end end
end end
...@@ -77,11 +77,11 @@ class IssuableFinder ...@@ -77,11 +77,11 @@ class IssuableFinder
return @projects if defined?(@projects) return @projects if defined?(@projects)
if project? if project?
project @projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related? elsif current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects @projects = current_user.authorized_projects
else else
ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user)
end end
end end
...@@ -190,8 +190,10 @@ class IssuableFinder ...@@ -190,8 +190,10 @@ class IssuableFinder
def by_project(items) def by_project(items)
items = items =
if projects if project?
items.of_projects(projects).references(:project) items.of_projects(projects).references_project
elsif projects
items.merge(projects.reorder(nil)).join_project
else else
items.none items.none
end end
...@@ -206,7 +208,9 @@ class IssuableFinder ...@@ -206,7 +208,9 @@ class IssuableFinder
end end
def sort(items) def sort(items)
items.sort(params[:sort]) # Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
end end
def by_assignee(items) def by_assignee(items)
......
# Class for finding the groups a user is a member of.
class JoinedGroupsFinder
def initialize(user = nil)
@user = user
end
# Finds the groups of the source user, optionally limited to those visible to
# the current user.
#
# current_user - If given the groups of "@user" will only include the groups
# "current_user" can also see.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = groups_visible_to_user(current_user)
else
relation = public_groups
end
relation.order_id_desc
end
private
# Returns the groups the user in "current_user" can see.
#
# This list includes all public/internal projects as well as the projects of
# "@user" that "current_user" also has access to.
def groups_visible_to_user(current_user)
base = @user.authorized_groups.visible_to_user(current_user)
extra = public_and_internal_groups
union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
Group.where("namespaces.id IN (#{union.to_sql})")
end
def public_groups
groups_for_projects(@user.authorized_projects.public_only)
end
def public_and_internal_groups
groups_for_projects(@user.authorized_projects.public_and_internal_only)
end
def groups_for_projects(projects)
@user.groups.public_and_given_groups(projects.select(:namespace_id))
end
end
class MilestonesFinder
def execute(projects, params)
milestones = Milestone.of_projects(projects)
milestones = milestones.order("due_date ASC")
case params[:state]
when 'closed' then milestones.closed
when 'all' then milestones
else milestones.active
end
end
end
...@@ -12,9 +12,9 @@ class NotesFinder ...@@ -12,9 +12,9 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).not_inline project.notes.for_commit_id(target_id).not_inline
when "issue" when "issue"
project.issues.find(target_id).notes.inc_author project.issues.find(target_id).notes.nonawards.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes project.snippets.find(target_id).notes
else else
......
class PersonalProjectsFinder
def initialize(user)
@user = user
end
# Finds the projects belonging to the user in "@user", limited to either
# public projects or projects visible to the given user.
#
# current_user - When given the list of projects is limited to those only
# visible by this user.
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
if current_user
relation = projects_visible_to_user(current_user)
else
relation = public_projects
end
relation.includes(:namespace).order_id_desc
end
private
def projects_visible_to_user(current_user)
authorized = @user.personal_projects.visible_to_user(current_user)
union = Gitlab::SQL::Union.
new([authorized.select(:id), public_and_internal_projects.select(:id)])
Project.where("projects.id IN (#{union.to_sql})")
end
def public_projects
@user.personal_projects.public_only
end
def public_and_internal_projects
@user.personal_projects.public_and_internal_only
end
end
class ProjectsFinder class ProjectsFinder
def execute(current_user, options = {}) # Returns all projects, optionally including group projects a user has access
# to.
#
# ## Examples
#
# Retrieving all public projects:
#
# ProjectsFinder.new.execute
#
# Retrieving all public/internal projects and those the given user has access
# to:
#
# ProjectsFinder.new.execute(some_user)
#
# Retrieving all public/internal projects as well as the group's projects the
# user has access to:
#
# ProjectsFinder.new.execute(some_user, group: some_group)
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil, options = {})
group = options[:group] group = options[:group]
if group if group
group_projects(current_user, group) segments = group_projects(current_user, group)
else else
all_projects(current_user) segments = all_projects(current_user)
end
if segments.length > 1
union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
Project.where("projects.id IN (#{union.to_sql})")
else
segments.first
end end
end end
...@@ -13,89 +41,37 @@ class ProjectsFinder ...@@ -13,89 +41,37 @@ class ProjectsFinder
def group_projects(current_user, group) def group_projects(current_user, group)
if current_user if current_user
if group.users.include?(current_user) [
# User is group member group_projects_for_user(current_user, group),
# group.projects.public_and_internal_only,
# Return ALL group projects group.shared_projects.visible_to_user(current_user)
group.projects ]
else
projects_members = ProjectMember.in_projects(group.projects).
with_user(current_user)
if projects_members.any?
# User is a project member
#
# Return only:
# public projects
# internal projects
# joined projects
#
group.projects.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
projects_members.pluck(:source_id),
Project.public_and_internal_levels
)
else else
# User has no access to group or group projects [group.projects.public_only]
# or has access through shared project
#
# Return only:
# public projects
# internal projects
# shared projects
projects_ids = []
ProjectGroupLink.where(project_id: group.projects).each do |shared_project|
if shared_project.group.users.include?(current_user) || shared_project.project.users.include?(current_user)
projects_ids << shared_project.project.id
end end
end end
group.projects.where( def all_projects(current_user)
"projects.id IN (?) OR projects.visibility_level IN (?)", if current_user
projects_ids, [current_user.authorized_projects, public_and_internal_projects]
Project.public_and_internal_levels
)
end
end
else else
# Not authenticated [Project.public_only]
#
# Return only:
# public projects
group.projects.public_only
end end
end end
def all_projects(current_user) def group_projects_for_user(current_user, group)
if current_user if group.users.include?(current_user)
if current_user.authorized_projects.any? group.projects
# User has access to private projects
#
# Return only:
# public projects
# internal projects
# joined projects
#
Project.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
current_user.authorized_projects.pluck(:id),
Project.public_and_internal_levels
)
else else
# User has no access to private projects group.projects.visible_to_user(current_user)
#
# Return only:
# public projects
# internal projects
#
Project.public_and_internal_only
end end
else
# Not authenticated
#
# Return only:
# public projects
Project.public_only
end end
def public_projects
Project.unscoped.public_only
end
def public_and_internal_projects
Project.unscoped.public_and_internal_only
end end
end end
module DiffHelper module DiffHelper
def diff_view
params[:view] == 'parallel' ? 'parallel' : 'inline'
end
def allowed_diff_size def allowed_diff_size
if diff_hard_limit_enabled? if diff_hard_limit_enabled?
Commit::DIFF_HARD_LIMIT_FILES Commit::DIFF_HARD_LIMIT_FILES
...@@ -132,25 +136,11 @@ module DiffHelper ...@@ -132,25 +136,11 @@ module DiffHelper
end end
def inline_diff_btn def inline_diff_btn
params_copy = params.dup diff_btn('Inline', 'inline', diff_view == 'inline')
params_copy[:view] = 'inline'
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do
'Inline'
end
end end
def parallel_diff_btn def parallel_diff_btn
params_copy = params.dup diff_btn('Side-by-side', 'parallel', diff_view == 'parallel')
params_copy[:view] = 'parallel'
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do
'Side-by-side'
end
end end
def submodule_link(blob, ref, repository = @repository) def submodule_link(blob, ref, repository = @repository)
...@@ -171,7 +161,7 @@ module DiffHelper ...@@ -171,7 +161,7 @@ module DiffHelper
def commit_for_diff(diff) def commit_for_diff(diff)
if diff.deleted_file if diff.deleted_file
first_commit = @first_commit || @commit first_commit = @first_commit || @commit
first_commit.parent first_commit.parent || @first_commit
else else
@commit @commit
end end
...@@ -187,4 +177,18 @@ module DiffHelper ...@@ -187,4 +177,18 @@ module DiffHelper
def editable_diff?(diff) def editable_diff?(diff)
!diff.deleted_file && @merge_request && @merge_request.source_project !diff.deleted_file && @merge_request && @merge_request.source_project
end end
private
def diff_btn(title, name, selected)
params_copy = params.dup
params_copy[:view] = name
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
title
end
end
end end
...@@ -46,39 +46,13 @@ module GitlabMarkdownHelper ...@@ -46,39 +46,13 @@ module GitlabMarkdownHelper
end end
def markdown(text, context = {}) def markdown(text, context = {})
return "" unless text.present? process_markdown(text, context)
context.reverse_merge!(
path: @path,
pipeline: :default,
project: @project,
project_wiki: @project_wiki,
ref: @ref
)
user = current_user if defined?(current_user)
html = Gitlab::Markdown.render(text, context)
Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user)
end end
# TODO (rspeicher): Remove all usages of this helper and just call `markdown` # TODO (rspeicher): Remove all usages of this helper and just call `markdown`
# with a custom pipeline depending on the content being rendered # with a custom pipeline depending on the content being rendered
def gfm(text, options = {}) def gfm(text, options = {})
return "" unless text.present? process_markdown(text, options, :gfm)
options.reverse_merge!(
path: @path,
pipeline: :default,
project: @project,
project_wiki: @project_wiki,
ref: @ref
)
user = current_user if defined?(current_user)
html = Gitlab::Markdown.gfm(text, options)
Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
end end
def asciidoc(text) def asciidoc(text)
...@@ -204,4 +178,26 @@ module GitlabMarkdownHelper ...@@ -204,4 +178,26 @@ module GitlabMarkdownHelper
'' ''
end end
end end
def process_markdown(text, options, method = :markdown)
return "" unless text.present?
options.reverse_merge!(
path: @path,
pipeline: :default,
project: @project,
project_wiki: @project_wiki,
ref: @ref
)
user = current_user if defined?(current_user)
html = if method == :gfm
Gitlab::Markdown.gfm(text, options)
else
Gitlab::Markdown.render(text, options)
end
Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
end
end end
...@@ -12,7 +12,7 @@ module GroupsHelper ...@@ -12,7 +12,7 @@ module GroupsHelper
end end
def should_user_see_group_roles?(user, group) def should_user_see_group_roles?(user, group)
if user if user && group
user.is_admin? || group.members.exists?(user_id: user.id) user.is_admin? || group.members.exists?(user_id: user.id)
else else
false false
......
...@@ -87,6 +87,33 @@ module IssuesHelper ...@@ -87,6 +87,33 @@ module IssuesHelper
merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
end end
def url_to_emoji(name)
emoji_path = ::AwardEmoji.path_to_emoji_image(name)
url_to_image(emoji_path)
rescue StandardError
""
end
def emoji_author_list(notes, current_user)
list = notes.map do |note|
note.author == current_user ? "me" : note.author.username
end
list.join(", ")
end
def emoji_list
::AwardEmoji::EMOJI_LIST
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
"active"
else
""
end
end
# Required for Gitlab::Markdown::IssueReferenceFilter # Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue module_function :url_for_issue
end end
...@@ -100,7 +100,7 @@ module LabelsHelper ...@@ -100,7 +100,7 @@ module LabelsHelper
Label.where(project_id: @projects) Label.where(project_id: @projects)
end end
grouped_labels = Labels::GroupService.new(labels).execute grouped_labels = GlobalLabel.build_collection(labels)
grouped_labels.unshift(Label::None) grouped_labels.unshift(Label::None)
grouped_labels.unshift(Label::Any) grouped_labels.unshift(Label::Any)
......
...@@ -8,14 +8,6 @@ module MergeRequestsHelper ...@@ -8,14 +8,6 @@ module MergeRequestsHelper
) )
end end
def new_mr_path_for_fork_from_push_event(event)
new_namespace_project_merge_request_path(
event.project.namespace,
event.project,
new_mr_from_push_event(event, event.project.forked_from_project)
)
end
def new_mr_from_push_event(event, target_project) def new_mr_from_push_event(event, target_project)
{ {
merge_request: { merge_request: {
......
...@@ -28,7 +28,7 @@ module MilestonesHelper ...@@ -28,7 +28,7 @@ module MilestonesHelper
Milestone.where(project_id: @projects) Milestone.where(project_id: @projects)
end.active end.active
grouped_milestones = Milestones::GroupService.new(milestones).execute grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
......
...@@ -17,15 +17,6 @@ module NamespacesHelper ...@@ -17,15 +17,6 @@ module NamespacesHelper
grouped_options_for_select(options, selected) grouped_options_for_select(options, selected)
end end
def namespace_select_tag(id, opts = {})
css_class = "ajax-namespace-select "
css_class << "multiselect " if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
hidden_field_tag(id, value, class: css_class)
end
def namespace_icon(namespace, size = 40) def namespace_icon(namespace, size = 40)
if namespace.kind_of?(Group) if namespace.kind_of?(Group)
group_icon(namespace) group_icon(namespace)
......
...@@ -46,8 +46,20 @@ module SelectsHelper ...@@ -46,8 +46,20 @@ module SelectsHelper
end end
def groups_select_tag(id, opts = {}) def groups_select_tag(id, opts = {})
css_class = "ajax-groups-select " opts[:class] ||= ''
css_class << "multiselect " if opts[:multiple] opts[:class] << ' ajax-groups-select'
select2_tag(id, opts)
end
def namespace_select_tag(id, opts = {})
opts[:class] ||= ''
opts[:class] << ' ajax-namespace-select'
select2_tag(id, opts)
end
def select2_tag(id, opts = {})
css_class = ''
css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '') css_class << (opts[:class] || '')
value = opts[:selected] || '' value = opts[:selected] || ''
......
module Emails module Emails
module Issues module Issues
def new_issue_email(recipient_id, issue_id) def new_issue_email(recipient_id, issue_id)
@issue = Issue.find(issue_id) issue_mail_with_notification(issue_id, recipient_id) do
@project = @issue.project mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue) end
mail_new_thread(@issue,
from: sender(@issue.author_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})"))
SentNotification.record(@issue, recipient_id, reply_key)
end end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id) def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
@issue = Issue.find(issue_id) issue_mail_with_notification(issue_id, recipient_id) do
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
@project = @issue.project mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue) end
mail_answer_thread(@issue,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})"))
SentNotification.record(@issue, recipient_id, reply_key)
end end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id) def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
@issue = Issue.find issue_id issue_mail_with_notification(issue_id, recipient_id) do
@project = @issue.project
@updated_by = User.find updated_by_user_id @updated_by = User.find updated_by_user_id
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
mail_answer_thread(@issue, end
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})"))
SentNotification.record(@issue, recipient_id, reply_key)
end end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
@issue = Issue.find issue_id issue_mail_with_notification(issue_id, recipient_id) do
@issue_status = status @issue_status = status
@project = @issue.project
@updated_by = User.find updated_by_user_id @updated_by = User.find updated_by_user_id
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
mail_answer_thread(@issue, end
from: sender(updated_by_user_id), end
private
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")) subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
def issue_mail_with_notification(issue_id, recipient_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
yield
SentNotification.record(@issue, recipient_id, reply_key) SentNotification.record(@issue, recipient_id, reply_key)
end end
......
module Emails module Emails
module Notes module Notes
def note_commit_email(recipient_id, note_id) def note_commit_email(recipient_id, note_id)
@note = Note.find(note_id) note_mail_with_notification(note_id, recipient_id) do
@commit = @note.noteable @commit = @note.noteable
@project = @note.project @target_url = namespace_project_commit_url(*note_target_url_options)
@target_url = namespace_project_commit_url(@project.namespace, @project,
@commit, anchor:
"note_#{@note.id}")
mail_answer_thread(@commit, mail_answer_thread(@commit,
from: sender(@note.author_id), from: sender(@note.author_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@commit.title} (#{@commit.short_id})")) subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
SentNotification.record_note(@note, recipient_id, reply_key)
end end
def note_issue_email(recipient_id, note_id) def note_issue_email(recipient_id, note_id)
@note = Note.find(note_id) note_mail_with_notification(note_id, recipient_id) do
@issue = @note.noteable @issue = @note.noteable
@project = @note.project @target_url = namespace_project_issue_url(*note_target_url_options)
@target_url = namespace_project_issue_url(@project.namespace, @project, mail_answer_thread(@issue, note_thread_options(recipient_id))
@issue, anchor: end
"note_#{@note.id}")
mail_answer_thread(@issue,
from: sender(@note.author_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})"))
SentNotification.record_note(@note, recipient_id, reply_key)
end end
def note_merge_request_email(recipient_id, note_id) def note_merge_request_email(recipient_id, note_id)
@note = Note.find(note_id) note_mail_with_notification(note_id, recipient_id) do
@merge_request = @note.noteable @merge_request = @note.noteable
@project = @note.project @target_url = namespace_project_merge_request_url(*note_target_url_options)
@target_url = namespace_project_merge_request_url(@project.namespace, mail_answer_thread(@merge_request, note_thread_options(recipient_id))
@project, end
@merge_request, anchor: end
"note_#{@note.id}")
mail_answer_thread(@merge_request, private
def note_target_url_options
[@project.namespace, @project, @note.noteable, anchor: "note_#{@note.id}"]
end
def note_thread_options(recipient_id)
{
from: sender(@note.author_id), from: sender(@note.author_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})")
}
end
def note_mail_with_notification(note_id, recipient_id)
@note = Note.find(note_id)
@project = @note.project
yield
SentNotification.record_note(@note, recipient_id, reply_key) SentNotification.record(@note, recipient_id, reply_key)
end end
end end
end end
class Ability class Ability
class << self class << self
def allowed(user, subject) def allowed(user, subject)
return not_auth_abilities(user, subject) if user.nil? return anonymous_abilities(user, subject) if user.nil?
return [] unless user.kind_of?(User) return [] unless user.is_a?(User)
return [] if user.blocked? return [] if user.blocked?
abilities = abilities =
...@@ -16,6 +16,7 @@ class Ability ...@@ -16,6 +16,7 @@ class Ability
when "Group" then group_abilities(user, subject) when "Group" then group_abilities(user, subject)
when "Namespace" then namespace_abilities(user, subject) when "Namespace" then namespace_abilities(user, subject)
when "GroupMember" then group_member_abilities(user, subject) when "GroupMember" then group_member_abilities(user, subject)
when "ProjectMember" then project_member_abilities(user, subject)
else [] else []
end end
...@@ -35,15 +36,25 @@ class Ability ...@@ -35,15 +36,25 @@ class Ability
] ]
end end
# List of possible abilities # List of possible abilities for anonymous user
# for non-authenticated user def anonymous_abilities(user, subject)
def not_auth_abilities(user, subject) case true
project = if subject.kind_of?(Project) when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project)
anonymous_project_abilities(subject)
when subject.is_a?(Group) || subject.respond_to?(:group)
anonymous_group_abilities(subject)
else
[]
end
end
def anonymous_project_abilities(subject)
project = if subject.is_a?(Project)
subject subject
elsif subject.respond_to?(:project)
subject.project
else else
nil subject.project
end end
if project && project.public? if project && project.public?
...@@ -63,12 +74,15 @@ class Ability ...@@ -63,12 +74,15 @@ class Ability
rules - project_disabled_features_rules(project) rules - project_disabled_features_rules(project)
else else
group = if subject.kind_of?(Group) []
end
end
def anonymous_group_abilities(subject)
group = if subject.is_a?(Group)
subject subject
elsif subject.respond_to?(:group)
subject.group
else else
nil subject.group
end end
if group && group.public_profile? if group && group.public_profile?
...@@ -77,6 +91,13 @@ class Ability ...@@ -77,6 +91,13 @@ class Ability
[] []
end end
end end
def anonymous_personal_snippet_abilities(snippet)
if snippet.public?
[:read_personal_snippet]
else
[]
end
end end
def global_abilities(user) def global_abilities(user)
...@@ -247,21 +268,22 @@ class Ability ...@@ -247,21 +268,22 @@ class Ability
# Only group masters and group owners can create new projects in group # Only group masters and group owners can create new projects in group
if group.has_master?(user) || group.has_owner?(user) || user.admin? if group.has_master?(user) || group.has_owner?(user) || user.admin?
rules.push(*[ rules += [
:create_projects, :create_projects,
]) :admin_milestones
]
end end
# Only group owner and administrators can admin group # Only group owner and administrators can admin group
if group.has_owner?(user) || user.admin? if group.has_owner?(user) || user.admin?
rules.push(*[ rules += [
:admin_group, :admin_group,
:admin_namespace, :admin_namespace,
:admin_group_member :admin_group_member
]) ]
unless group.ldap_synced? if group.ldap_synced?
rules << :admin_group_member rules.delete(:admin_group_member)
end end
end end
...@@ -273,16 +295,15 @@ class Ability ...@@ -273,16 +295,15 @@ class Ability
# Only namespace owner and administrators can admin it # Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin? if namespace.owner == user || user.admin?
rules.push(*[ rules += [
:create_projects, :create_projects,
:admin_namespace :admin_namespace
]) ]
end end
rules.flatten rules.flatten
end end
[:issue, :merge_request].each do |name| [:issue, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject| define_method "#{name}_abilities" do |user, subject|
rules = [] rules = []
...@@ -299,7 +320,7 @@ class Ability ...@@ -299,7 +320,7 @@ class Ability
end end
end end
[:note, :project_snippet, :personal_snippet].each do |name| [:note, :project_snippet].each do |name|
define_method "#{name}_abilities" do |user, subject| define_method "#{name}_abilities" do |user, subject|
rules = [] rules = []
...@@ -319,20 +340,62 @@ class Ability ...@@ -319,20 +340,62 @@ class Ability
end end
end end
def personal_snippet_abilities(user, snippet)
rules = []
if snippet.author == user
rules += [
:read_personal_snippet,
:update_personal_snippet,
:admin_personal_snippet
]
end
if snippet.public? || snippet.internal?
rules << :read_personal_snippet
end
rules
end
def group_member_abilities(user, subject) def group_member_abilities(user, subject)
rules = [] rules = []
target_user = subject.user target_user = subject.user
group = subject.group group = subject.group
unless group.last_owner?(target_user)
can_manage = group_abilities(user, group).include?(:admin_group_member) can_manage = group_abilities(user, group).include?(:admin_group_member)
if can_manage && (user != target_user) if can_manage && user != target_user
rules << :update_group_member rules << :update_group_member
rules << :destroy_group_member rules << :destroy_group_member
end end
if !group.last_owner?(user) && (can_manage || (user == target_user)) if user == target_user
rules << :destroy_group_member rules << :destroy_group_member
end end
end
rules
end
def project_member_abilities(user, subject)
rules = []
target_user = subject.user
project = subject.project
unless target_user == project.owner
can_manage = project_abilities(user, project).include?(:admin_project_member)
if can_manage && user != target_user
rules << :update_project_member
rules << :destroy_project_member
end
if user == target_user
rules << :destroy_project_member
end
end
rules rules
end end
......
...@@ -100,7 +100,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -100,7 +100,7 @@ class ApplicationSetting < ActiveRecord::Base
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.gitlab_ci['max_artifacts_size'], max_artifacts_size: Settings.artifacts['max_size'],
) )
end end
......
...@@ -97,6 +97,8 @@ module Ci ...@@ -97,6 +97,8 @@ module Ci
state_machine :status, initial: :pending do state_machine :status, initial: :pending do
after_transition any => [:success, :failed, :canceled] do |build, transition| after_transition any => [:success, :failed, :canceled] do |build, transition|
return unless build.gl_project
project = build.project project = build.project
if project.web_hooks? if project.web_hooks?
......
...@@ -188,13 +188,13 @@ module Ci ...@@ -188,13 +188,13 @@ module Ci
end end
def config_processor def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace) @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError => e rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message) save_yaml_error(e.message)
nil nil
rescue Exception => e rescue
logger.error e.message + "\n" + e.backtrace.join("\n") save_yaml_error("Undefined error")
save_yaml_error("Undefined yaml error")
nil nil
end end
......
...@@ -35,6 +35,9 @@ module Issuable ...@@ -35,6 +35,9 @@ module Issuable
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
delegate :name, delegate :name,
:email, :email,
to: :author, to: :author,
...@@ -89,39 +92,14 @@ module Issuable ...@@ -89,39 +92,14 @@ module Issuable
opened? || reopened? opened? || reopened?
end end
# # Deprecated. Still exists to preserve API compatibility.
# Votes
#
# Return the number of -1 comments (downvotes)
def downvotes def downvotes
filter_superceded_votes(notes.select(&:downvote?), notes).size
end
def downvotes_in_percent
if votes_count.zero?
0 0
else
100.0 - upvotes_in_percent
end
end end
# Return the number of +1 comments (upvotes) # Deprecated. Still exists to preserve API compatibility.
def upvotes def upvotes
filter_superceded_votes(notes.select(&:upvote?), notes).size
end
def upvotes_in_percent
if votes_count.zero?
0 0
else
100.0 / votes_count * upvotes
end
end
# Return the total number of votes
def votes_count
upvotes + downvotes
end end
def subscribed?(user) def subscribed?(user)
...@@ -184,17 +162,8 @@ module Issuable ...@@ -184,17 +162,8 @@ module Issuable
notes.includes(:author, :project) notes.includes(:author, :project)
end end
private def updated_tasks
Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
def filter_superceded_votes(votes, notes) new_content: description)
filteredvotes = [] + votes
votes.each do |vote|
if vote.superceded?(notes)
filteredvotes.delete(vote)
end
end
filteredvotes
end end
end end
...@@ -8,8 +8,9 @@ module Sortable ...@@ -8,8 +8,9 @@ module Sortable
included do included do
# By default all models should be ordered # By default all models should be ordered
# by created_at field starting from newest # by created_at field starting from newest
default_scope { order(id: :desc) } default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) }
......
...@@ -7,14 +7,39 @@ require 'task_list/filter' ...@@ -7,14 +7,39 @@ require 'task_list/filter'
# #
# Used by MergeRequest and Issue # Used by MergeRequest and Issue
module Taskable module Taskable
COMPLETED = 'completed'.freeze
INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = /
^
(?:\s*[-+*]|(?:\d+\.))? # optional list prefix
\s* # optional whitespace prefix
(\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text.
/x
def self.get_tasks(content)
content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
# ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble.
TaskList::Item.new("- #{checkbox}", label.strip)
end
end
def self.get_updated_tasks(old_content:, new_content:)
old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content)
new_tasks.select.with_index do |new_task, i|
old_task = old_tasks[i]
next unless old_task
new_task.source == old_task.source && new_task.complete? != old_task.complete?
end
end
# Called by `TaskList::Summary` # Called by `TaskList::Summary`
def task_list_items def task_list_items
return [] if description.blank? return [] if description.blank?
@task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item| @task_list_items ||= Taskable.get_tasks(description)
# ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
TaskList::Item.new("- #{item}")
end
end end
def tasks def tasks
......
...@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base ...@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base
Event::PUSHED, ["MergeRequest", "Issue"], Event::PUSHED, ["MergeRequest", "Issue"],
[Event::CREATED, Event::CLOSED, Event::MERGED]) [Event::CREATED, Event::CLOSED, Event::MERGED])
end end
def latest_update_time
row = select(:updated_at, :project_id).reorder(id: :desc).take
row ? row.updated_at : nil
end
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
end end
def proper? def proper?
......
class GroupLabel class GlobalLabel
attr_accessor :title, :labels attr_accessor :title, :labels
alias_attribute :name, :title alias_attribute :name, :title
def self.build_collection(labels)
labels = labels.group_by(&:title)
labels.map do |title, label|
new(title, label)
end
end
def initialize(title, labels) def initialize(title, labels)
@title = title @title = title
@labels = labels @labels = labels
......
class GroupMilestone class GlobalMilestone
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
milestones.map do |title, milestones|
new(title, milestones)
end
end
def initialize(title, milestones) def initialize(title, milestones)
@title = title @title = title
@milestones = milestones @milestones = milestones
...@@ -60,15 +68,15 @@ class GroupMilestone ...@@ -60,15 +68,15 @@ class GroupMilestone
end end
def issues def issues
@group_issues ||= milestones.map(&:issues).flatten.group_by(&:state) @issues ||= milestones.map(&:issues).flatten.group_by(&:state)
end end
def merge_requests def merge_requests
@group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
end end
def participants def participants
@group_participants ||= milestones.map(&:participants).flatten.compact.uniq @participants ||= milestones.map(&:participants).flatten.compact.uniq
end end
def opened_issues def opened_issues
...@@ -86,4 +94,8 @@ class GroupMilestone ...@@ -86,4 +94,8 @@ class GroupMilestone
def closed_merge_requests def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten merge_requests.values_at("closed", "merged", "locked").compact.flatten
end end
def complete?
total_items_count == closed_items_count
end
end end
...@@ -22,6 +22,7 @@ class Group < Namespace ...@@ -22,6 +22,7 @@ class Group < Namespace
include Referable include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members has_many :users, through: :group_members
has_many :project_group_links, dependent: :destroy has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
...@@ -52,6 +53,14 @@ class Group < Namespace ...@@ -52,6 +53,14 @@ class Group < Namespace
def reference_pattern def reference_pattern
User.reference_pattern User.reference_pattern
end end
def public_and_given_groups(ids)
where('public IS TRUE OR namespaces.id IN (?)', ids)
end
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
end end
def to_reference(_from_project = nil) def to_reference(_from_project = nil)
...@@ -114,10 +123,6 @@ class Group < Namespace ...@@ -114,10 +123,6 @@ class Group < Namespace
has_owner?(user) && owners.size == 1 has_owner?(user) && owners.size == 1
end end
def members
group_members
end
def avatar_type def avatar_type
unless self.avatar.image? unless self.avatar.image?
self.errors.add :avatar, "only images allowed" self.errors.add :avatar, "only images allowed"
......
...@@ -35,9 +35,18 @@ class Member < ActiveRecord::Base ...@@ -35,9 +35,18 @@ class Member < ActiveRecord::Base
message: "already exists in source", message: "already exists in source",
allow_nil: true } allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :invite_email, presence: { if: :invite? }, validates :invite_email,
email: { strict_mode: true, allow_nil: true }, presence: {
uniqueness: { scope: [:source_type, :source_id], allow_nil: true } if: :invite?
},
email: {
strict_mode: true,
allow_nil: true
},
uniqueness: {
scope: [:source_type, :source_id],
allow_nil: true
}
scope :invite, -> { where(user_id: nil) } scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") } scope :non_invite, -> { where("user_id IS NOT NULL") }
...@@ -83,6 +92,7 @@ class Member < ActiveRecord::Base ...@@ -83,6 +92,7 @@ class Member < ActiveRecord::Base
member.invite_email = user member.invite_email = user
end end
if can_update_member?(current_user, member)
member.created_by ||= current_user member.created_by ||= current_user
member.access_level = access_level member.access_level = access_level
...@@ -92,6 +102,16 @@ class Member < ActiveRecord::Base ...@@ -92,6 +102,16 @@ class Member < ActiveRecord::Base
end end
end end
private
def can_update_member?(current_user, member)
# There is no current user for bulk actions, in which case anything is allowed
!current_user ||
current_user.can?(:update_group_member, member) ||
current_user.can?(:update_project_member, member)
end
end
def invite? def invite?
self.invite_token.present? self.invite_token.present?
end end
......
...@@ -135,6 +135,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -135,6 +135,8 @@ class MergeRequest < ActiveRecord::Base
scope :merged, -> { with_state(:merged) } scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
participant :approvers_left participant :approvers_left
...@@ -545,7 +547,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -545,7 +547,7 @@ class MergeRequest < ActiveRecord::Base
end end
def ci_commit def ci_commit
if last_commit if last_commit and source_project
source_project.ci_commit(last_commit.id) source_project.ci_commit(last_commit.id)
end end
end end
......
...@@ -40,16 +40,20 @@ class Note < ActiveRecord::Base ...@@ -40,16 +40,20 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
validates :author, presence: true
mount_uploader :attachment, AttachmentUploader mount_uploader :attachment, AttachmentUploader
# Scopes # Scopes
scope :awards, ->{ where(is_award: true) }
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") } scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: [nil, '']) }
...@@ -97,6 +101,12 @@ class Note < ActiveRecord::Base ...@@ -97,6 +101,12 @@ class Note < ActiveRecord::Base
def search(query) def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%") where("LOWER(note) like :query", query: "%#{query.downcase}%")
end end
def grouped_awards
awards.select(:note).distinct.map do |note|
[ note.note, where(note: note.note) ]
end
end
end end
def cross_reference? def cross_reference?
...@@ -288,44 +298,6 @@ class Note < ActiveRecord::Base ...@@ -288,44 +298,6 @@ class Note < ActiveRecord::Base
nil nil
end end
DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
# Check if the note is a downvote
def downvote?
votable? && note.start_with?(*DOWNVOTES)
end
UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
# Check if the note is an upvote
def upvote?
votable? && note.start_with?(*UPVOTES)
end
def superceded?(notes)
return false unless vote?
notes.each do |note|
next if note == self
if note.vote? &&
self[:author_id] == note[:author_id] &&
self[:created_at] <= note[:created_at]
return true
end
end
false
end
def vote?
upvote? || downvote?
end
def votable?
for_issue? || (for_merge_request? && !for_diff_line?)
end
# Mentionable override. # Mentionable override.
def gfm_reference(from_project = nil) def gfm_reference(from_project = nil)
noteable.gfm_reference(from_project) noteable.gfm_reference(from_project)
...@@ -363,6 +335,16 @@ class Note < ActiveRecord::Base ...@@ -363,6 +335,16 @@ class Note < ActiveRecord::Base
read_attribute(:system) read_attribute(:system)
end end
# Deprecated. Still exists to preserve API compatibility.
def downvote?
false
end
# Deprecated. Still exists to preserve API compatibility.
def upvote?
false
end
def editable? def editable?
!system? !system?
end end
......
...@@ -72,12 +72,11 @@ class Project < ActiveRecord::Base ...@@ -72,12 +72,11 @@ class Project < ActiveRecord::Base
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace belongs_to :namespace
belongs_to :mirror_user, foreign_key: 'mirror_user_id', class_name: 'User'
has_one :git_hook, dependent: :destroy has_one :git_hook, dependent: :destroy
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
belongs_to :mirror_user, foreign_key: 'mirror_user_id', class_name: 'User'
# Project services # Project services
has_many :services has_many :services
has_one :gitlab_ci_service, dependent: :destroy has_one :gitlab_ci_service, dependent: :destroy
...@@ -132,9 +131,9 @@ class Project < ActiveRecord::Base ...@@ -132,9 +131,9 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group has_many :invited_groups, through: :project_group_links, source: :group
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id
...@@ -313,6 +312,10 @@ class Project < ActiveRecord::Base ...@@ -313,6 +312,10 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC') joins(join_body).reorder('join_note_counts.amount DESC')
end end
def visible_to_user(user)
where(id: user.authorized_projects.select(:id).reorder(nil))
end
end end
def team def team
......
...@@ -32,6 +32,8 @@ class DroneCiService < CiService ...@@ -32,6 +32,8 @@ class DroneCiService < CiService
def compose_service_hook def compose_service_hook
hook = service_hook || build_service_hook hook = service_hook || build_service_hook
# If using a service template, project may not be available
hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
hook.enable_ssl_verification = enable_ssl_verification hook.enable_ssl_verification = enable_ssl_verification
hook.save hook.save
end end
......
...@@ -30,6 +30,7 @@ class GitlabCiService < CiService ...@@ -30,6 +30,7 @@ class GitlabCiService < CiService
end end
def ensure_gitlab_ci_project def ensure_gitlab_ci_project
return unless project
project.ensure_gitlab_ci_project project.ensure_gitlab_ci_project
end end
......
...@@ -45,30 +45,27 @@ class SlackService ...@@ -45,30 +45,27 @@ class SlackService
def create_commit_note(commit) def create_commit_note(commit)
commit_sha = commit[:id] commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha) commit_sha = Commit.truncate_sha(commit_sha)
commit_link = "[commit #{commit_sha}](#{@note_url})" commented_on_message(
title = format_title(commit[:message]) "[commit #{commit_sha}](#{@note_url})",
@message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*" format_title(commit[:message]))
end end
def create_issue_note(issue) def create_issue_note(issue)
issue_iid = issue[:iid] commented_on_message(
note_link = "[issue ##{issue_iid}](#{@note_url})" "[issue ##{issue[:iid]}](#{@note_url})",
title = format_title(issue[:title]) format_title(issue[:title]))
@message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*"
end end
def create_merge_note(merge_request) def create_merge_note(merge_request)
merge_request_id = merge_request[:iid] commented_on_message(
merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" "[merge request ##{merge_request[:iid]}](#{@note_url})",
title = format_title(merge_request[:title]) format_title(merge_request[:title]))
@message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*"
end end
def create_snippet_note(snippet) def create_snippet_note(snippet)
snippet_id = snippet[:id] commented_on_message(
snippet_link = "[snippet ##{snippet_id}](#{@note_url})" "[snippet ##{snippet[:id]}](#{@note_url})",
title = format_title(snippet[:title]) format_title(snippet[:title]))
@message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*"
end end
def description_message def description_message
...@@ -78,5 +75,9 @@ class SlackService ...@@ -78,5 +75,9 @@ class SlackService
def project_link def project_link
"[#{@project_name}](#{@project_url})" "[#{@project_name}](#{@project_url})"
end end
def commented_on_message(target_link, title)
@message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
end
end end
end end
...@@ -86,6 +86,8 @@ class ProjectWiki ...@@ -86,6 +86,8 @@ class ProjectWiki
commit = commit_details(:created, message, title) commit = commit_details(:created, message, title)
wiki.write_page(title, format, content, commit) wiki.write_page(title, format, content, commit)
update_project_activity
rescue Gollum::DuplicatePageError => e rescue Gollum::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}" @error_message = "Duplicate page: #{e.message}"
return false return false
...@@ -95,10 +97,14 @@ class ProjectWiki ...@@ -95,10 +97,14 @@ class ProjectWiki
commit = commit_details(:updated, message, page.title) commit = commit_details(:updated, message, page.title)
wiki.update_page(page, page.name, format, content, commit) wiki.update_page(page, page.name, format, content, commit)
update_project_activity
end end
def delete_page(page, message = nil) def delete_page(page, message = nil)
wiki.delete_page(page, commit_details(:deleted, message, page.title)) wiki.delete_page(page, commit_details(:deleted, message, page.title))
update_project_activity
end end
def page_title_and_dir(title) def page_title_and_dir(title)
...@@ -146,4 +152,8 @@ class ProjectWiki ...@@ -146,4 +152,8 @@ class ProjectWiki
def path_to_repo def path_to_repo
@path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git")
end end
def update_project_activity
@project.touch(:last_activity_at)
end
end end
...@@ -417,43 +417,23 @@ class User < ActiveRecord::Base ...@@ -417,43 +417,23 @@ class User < ActiveRecord::Base
end end
end end
# Groups user has access to # Returns the groups a user has access to
def authorized_groups def authorized_groups
@authorized_groups ||= begin union = Gitlab::SQL::Union.
group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) new([groups.select(:id), authorized_projects.select(:namespace_id)])
Group.where(id: group_ids)
end
end
def authorized_projects_id Group.where("namespaces.id IN (#{union.to_sql})")
@authorized_projects_id ||= begin
project_ids = personal_projects.pluck(:id)
project_ids.push(*groups_projects.pluck(:id))
project_ids.push(*projects.pluck(:id).uniq)
project_ids.push(*groups.joins(:shared_projects).pluck(:project_id))
end
end end
def master_or_owner_projects_id # Returns the groups a user is authorized to access.
@master_or_owner_projects_id ||= begin
scope = { access_level: [ Gitlab::Access::MASTER, Gitlab::Access::OWNER ] }
project_ids = personal_projects.pluck(:id)
project_ids.push(*groups_projects.where(members: scope).pluck(:id))
project_ids.push(*projects.where(members: scope).pluck(:id).uniq)
end
end
# Projects user has access to
def authorized_projects def authorized_projects
@authorized_projects ||= Project.where(id: authorized_projects_id) Project.where("projects.id IN (#{projects_union.to_sql})")
end end
def owned_projects def owned_projects
@owned_projects ||= @owned_projects ||=
begin Project.where('namespace_id IN (?) OR namespace_id = ?',
namespace_ids = owned_groups.pluck(:id).push(namespace.id) owned_groups.select(:id), namespace.id).joins(:namespace)
Project.in_namespace(namespace_ids).joins(:namespace)
end
end end
# Team membership in authorized projects # Team membership in authorized projects
...@@ -772,12 +752,25 @@ class User < ActiveRecord::Base ...@@ -772,12 +752,25 @@ class User < ActiveRecord::Base
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
end end
def contributed_projects_ids # Returns the projects a user contributed to in the last year.
Event.contributions.where(author_id: self). #
# This method relies on a subquery as this performs significantly better
# compared to a JOIN when coupled with, for example,
# `Project.visible_to_user`. That is, consider the following code:
#
# some_user.contributed_projects.visible_to_user(other_user)
#
# If this method were to use a JOIN the resulting query would take roughly 200
# ms on a database with a similar size to GitLab.com's database. On the other
# hand, using a subquery means we can get the exact same data in about 40 ms.
def contributed_projects
events = Event.select(:project_id).
contributions.where(author_id: self).
where("created_at > ?", Time.now - 1.year). where("created_at > ?", Time.now - 1.year).
reorder(project_id: :desc). uniq.
select(:project_id). reorder(nil)
uniq.map(&:project_id)
Project.where(id: events)
end end
def restricted_signup_domains def restricted_signup_domains
...@@ -810,8 +803,28 @@ class User < ActiveRecord::Base ...@@ -810,8 +803,28 @@ class User < ActiveRecord::Base
def ci_authorized_runners def ci_authorized_runners
@ci_authorized_runners ||= begin @ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.joins(:project). runner_ids = Ci::RunnerProject.joins(:project).
where(ci_projects: { gitlab_id: master_or_owner_projects_id }).select(:runner_id) where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids) Ci::Runner.specific.where(id: runner_ids)
end end
end end
private
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
projects.select(:id),
groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
other = projects.where(members: scope)
Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
other.select(:id)])
end
end end
require_relative 'base_service'
class CreateReleaseService < BaseService
def execute(tag_name, release_description)
repository = project.repository
existing_tag = repository.find_tag(tag_name)
# Only create a release if the tag exists
if existing_tag
release = project.releases.find_by(tag: tag_name)
if release
error('Release already exists', 409)
else
release = project.releases.new({ tag: tag_name, description: release_description })
release.save
success(release)
end
else
error('Tag does not exist', 404)
end
end
def success(release)
out = super()
out[:release] = release
out
end
end
...@@ -19,16 +19,16 @@ class CreateTagService < BaseService ...@@ -19,16 +19,16 @@ class CreateTagService < BaseService
new_tag = repository.find_tag(tag_name) new_tag = repository.find_tag(tag_name)
if new_tag if new_tag
if release_description
release = project.releases.find_or_initialize_by(tag: tag_name)
release.update_attributes(description: release_description)
end
push_data = create_push_data(project, current_user, new_tag) push_data = create_push_data(project, current_user, new_tag)
EventCreateService.new.push(project, current_user, push_data) EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks)
if release_description
CreateReleaseService.new(@project, @current_user).
execute(tag_name, release_description)
end
success(new_tag) success(new_tag)
else else
error('Invalid reference name') error('Invalid reference name')
......
...@@ -58,12 +58,6 @@ class GitPushService ...@@ -58,12 +58,6 @@ class GitPushService
@push_data = build_push_data(oldrev, newrev, ref) @push_data = build_push_data(oldrev, newrev, ref)
# If CI was disabled but .gitlab-ci.yml file was pushed
# we enable CI automatically
if !project.builds_enabled? && gitlab_ci_yaml?(newrev)
project.enable_ci
end
EventCreateService.new.push(project, user, @push_data) EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks) project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks)
...@@ -134,10 +128,4 @@ class GitPushService ...@@ -134,10 +128,4 @@ class GitPushService
def commit_user(commit) def commit_user(commit)
commit.author || user commit.author || user
end end
def gitlab_ci_yaml?(sha)
@project.repository.blob_at(sha, '.gitlab-ci.yml')
rescue Rugged::ReferenceError
nil
end
end end
...@@ -27,7 +27,16 @@ class IssuableBaseService < BaseService ...@@ -27,7 +27,16 @@ class IssuableBaseService < BaseService
old_branch, new_branch) old_branch, new_branch)
end end
def create_task_status_note(issuable)
issuable.updated_tasks.each do |task|
SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
end
end
def filter_params(issuable_ability_name = :issue) def filter_params(issuable_ability_name = :issue)
params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
ability = :"admin_#{issuable_ability_name}" ability = :"admin_#{issuable_ability_name}"
unless can?(current_user, ability, project) unless can?(current_user, ability, project)
...@@ -36,4 +45,44 @@ class IssuableBaseService < BaseService ...@@ -36,4 +45,44 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id) params.delete(:assignee_id)
end end
end end
def update(issuable)
change_state(issuable)
filter_params
old_labels = issuable.labels.to_a
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
handle_changes(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
issuable
end
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
reopen_service.new(project, current_user, {}).execute(issuable)
when 'close'
close_service.new(project, current_user, {}).execute(issuable)
end
end
def handle_common_system_notes(issuable, options = {})
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
end
if issuable.previous_changes.include?('description') && issuable.tasks?
create_task_status_note(issuable)
end
old_labels = options[:old_labels]
if old_labels && (issuable.labels != old_labels)
create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
end
end
end end
module Issues module Issues
class UpdateService < Issues::BaseService class UpdateService < Issues::BaseService
def execute(issue) def execute(issue)
case params.delete(:state_event) update(issue)
when 'reopen'
Issues::ReopenService.new(project, current_user, {}).execute(issue)
when 'close'
Issues::CloseService.new(project, current_user, {}).execute(issue)
end
params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
filter_params
old_labels = issue.labels.to_a
if params.present? && issue.update_attributes(params.merge(updated_by: current_user))
issue.reset_events_cache
if issue.labels != old_labels
create_labels_note(
issue, issue.labels - old_labels, old_labels - issue.labels)
end
handle_changes(issue)
issue.create_new_cross_references!(current_user)
execute_hooks(issue, 'update')
end
issue
end end
def handle_changes(issue) def handle_changes(issue)
...@@ -39,10 +13,14 @@ module Issues ...@@ -39,10 +13,14 @@ module Issues
create_assignee_note(issue) create_assignee_note(issue)
notification_service.reassigned_issue(issue, current_user) notification_service.reassigned_issue(issue, current_user)
end end
end
if issue.previous_changes.include?('title') def reopen_service
create_title_change_note(issue, issue.previous_changes['title'].first) Issues::ReopenService
end end
def close_service
Issues::CloseService
end end
end end
end end
module Labels
class GroupService < ::BaseService
def initialize(project_labels)
@project_labels = project_labels.group_by(&:title)
end
def execute
build(@project_labels)
end
def label(title)
if title
group_label = @project_labels[title].group_by(&:title)
build(group_label).first
else
nil
end
end
private
def build(label)
label.map { |title, labels| GroupLabel.new(title, labels) }
end
end
end
...@@ -11,36 +11,7 @@ module MergeRequests ...@@ -11,36 +11,7 @@ module MergeRequests
params.except!(:target_project_id) params.except!(:target_project_id)
params.except!(:source_branch) params.except!(:source_branch)
case params.delete(:state_event) update(merge_request)
when 'reopen'
MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request)
when 'close'
MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request)
end
params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
filter_params
old_labels = merge_request.labels.to_a
if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user))
merge_request.reset_events_cache
if merge_request.labels != old_labels
create_labels_note(
merge_request,
merge_request.labels - old_labels,
old_labels - merge_request.labels
)
end
handle_changes(merge_request)
merge_request.create_new_cross_references!(current_user)
execute_hooks(merge_request, 'update')
end
merge_request
end end
def handle_changes(merge_request) def handle_changes(merge_request)
...@@ -59,14 +30,18 @@ module MergeRequests ...@@ -59,14 +30,18 @@ module MergeRequests
notification_service.reassigned_merge_request(merge_request, current_user) notification_service.reassigned_merge_request(merge_request, current_user)
end end
if merge_request.previous_changes.include?('title')
create_title_change_note(merge_request, merge_request.previous_changes['title'].first)
end
if merge_request.previous_changes.include?('target_branch') || if merge_request.previous_changes.include?('target_branch') ||
merge_request.previous_changes.include?('source_branch') merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
end end
end end
def reopen_service
MergeRequests::ReopenService
end
def close_service
MergeRequests::CloseService
end
end end
end end
module Milestones
class GroupService < Milestones::BaseService
def initialize(project_milestones)
@project_milestones = project_milestones.group_by(&:title)
end
def execute
build(@project_milestones)
end
def milestone(title)
if title
group_milestone = @project_milestones[title].group_by(&:title)
build(group_milestone).first
else
nil
end
end
private
def build(milestone)
milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) }
end
end
end
...@@ -5,11 +5,16 @@ module Notes ...@@ -5,11 +5,16 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if contains_emoji_only?(params[:note])
note.is_award = true
note.note = emoji_name(params[:note])
end
if note.save if note.save
notification_service.new_note(note) notification_service.new_note(note)
# Skip system notes, like status changes and cross-references. # Skip system notes, like status changes and cross-references and awards
unless note.system unless note.system || note.is_award
event_service.leave_note(note, note.author) event_service.leave_note(note, note.author)
note.create_cross_references! note.create_cross_references!
execute_hooks(note) execute_hooks(note)
...@@ -28,5 +33,13 @@ module Notes ...@@ -28,5 +33,13 @@ module Notes
note.project.execute_hooks(note_data, :note_hooks) note.project.execute_hooks(note_data, :note_hooks)
note.project.execute_services(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks)
end end
def contains_emoji_only?(note)
note =~ /\A:[-_+[:alnum:]]*:\s?\z/
end
def emoji_name(note)
note.match(/\A:([-_+[:alnum:]]*):\s?/)[1]
end
end end
end end
...@@ -102,6 +102,7 @@ class NotificationService ...@@ -102,6 +102,7 @@ class NotificationService
# ignore gitlab service messages # ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed') return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true return true if note.cross_reference? && note.system == true
return true if note.is_award
target = note.noteable target = note.noteable
...@@ -276,35 +277,25 @@ class NotificationService ...@@ -276,35 +277,25 @@ class NotificationService
# Remove users with disabled notifications from array # Remove users with disabled notifications from array
# Also remove duplications and nil recipients # Also remove duplications and nil recipients
def reject_muted_users(users, project = nil) def reject_muted_users(users, project = nil)
users = users.to_a.compact.uniq reject_users(users, :disabled?, project)
users = users.reject(&:blocked?)
users.reject do |user|
next user.notification.disabled? unless project
member = project.project_members.find_by(user_id: user.id)
if !member && project.group
member = project.group.group_members.find_by(user_id: user.id)
end
# reject users who globally disabled notification and has no membership
next user.notification.disabled? unless member
# reject users who disabled notification in project
next true if member.notification.disabled?
# reject users who have N_GLOBAL in project and disabled in global settings
member.notification.global? && user.notification.disabled?
end
end end
# Remove users with notification level 'Mentioned' # Remove users with notification level 'Mentioned'
def reject_mention_users(users, project = nil) def reject_mention_users(users, project = nil)
reject_users(users, :mention?, project)
end
# Reject users which method_name from notification object returns true.
#
# Example:
# reject_users(users, :watch?, project)
#
def reject_users(users, method_name, project = nil)
users = users.to_a.compact.uniq users = users.to_a.compact.uniq
users = users.reject(&:blocked?)
users.reject do |user| users.reject do |user|
next user.notification.mention? unless project next user.notification.send(method_name) unless project
member = project.project_members.find_by(user_id: user.id) member = project.project_members.find_by(user_id: user.id)
...@@ -313,13 +304,13 @@ class NotificationService ...@@ -313,13 +304,13 @@ class NotificationService
end end
# reject users who globally set mention notification and has no membership # reject users who globally set mention notification and has no membership
next user.notification.mention? unless member next user.notification.send(method_name) unless member
# reject users who set mention notification in project # reject users who set mention notification in project
next true if member.notification.mention? next true if member.notification.send(method_name)
# reject users who have N_MENTION in project and disabled in global settings # reject users who have N_MENTION in project and disabled in global settings
member.notification.global? && user.notification.mention? member.notification.global? && user.notification.send(method_name)
end end
end end
...@@ -361,11 +352,13 @@ class NotificationService ...@@ -361,11 +352,13 @@ class NotificationService
end end
def reassign_resource_email(target, project, current_user, method) def reassign_resource_email(target, project, current_user, method)
assignee_id_was = previous_record(target, "assignee_id") previous_assignee_id = previous_record(target, "assignee_id")
recipients = build_recipients(target, project, current_user) previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
recipients = build_recipients(target, project, current_user, [previous_assignee])
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id) mailer.send(method, recipient.id, target.id, previous_assignee_id, current_user.id)
end end
end end
...@@ -377,9 +370,11 @@ class NotificationService ...@@ -377,9 +370,11 @@ class NotificationService
end end
end end
def build_recipients(target, project, current_user) def build_recipients(target, project, current_user, extra_recipients = nil)
recipients = target.participants(current_user) recipients = target.participants(current_user)
recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients
recipients = add_project_watchers(recipients, project) recipients = add_project_watchers(recipients, project)
recipients = reject_mention_users(recipients, project) recipients = reject_mention_users(recipients, project)
recipients = reject_muted_users(recipients, project) recipients = reject_muted_users(recipients, project)
......
...@@ -361,4 +361,22 @@ class SystemNoteService ...@@ -361,4 +361,22 @@ class SystemNoteService
"* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
end end
# Called when the status of a Task has changed
#
# noteable - Noteable object.
# project - Project owning noteable
# author - User performing the change
# new_task - TaskList::Item object.
#
# Example Note text:
#
# "Soandso marked the task Whatever as completed."
#
# Returns the created Note object
def self.change_task_status(noteable, project, author, new_task)
status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
body = "Marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
end end
require_relative 'base_service'
class UpdateReleaseService < BaseService
def execute(tag_name, release_description)
repository = project.repository
existing_tag = repository.find_tag(tag_name)
if existing_tag
release = project.releases.find_by(tag: tag_name)
if release
release.update_attributes(description: release_description)
success(release)
else
error('Release does not exist', 404)
end
else
error('Tag does not exist', 404)
end
end
def success(release)
out = super()
out[:release] = release
out
end
end
...@@ -5,15 +5,15 @@ class ArtifactUploader < CarrierWave::Uploader::Base ...@@ -5,15 +5,15 @@ class ArtifactUploader < CarrierWave::Uploader::Base
attr_accessor :build, :field attr_accessor :build, :field
def self.artifacts_path def self.artifacts_path
File.expand_path('shared/artifacts/', Rails.root) Gitlab.config.artifacts.path
end end
def self.artifacts_upload_path def self.artifacts_upload_path
File.expand_path('shared/artifacts/tmp/uploads/', Rails.root) File.join(self.artifacts_path, 'tmp/uploads/')
end end
def self.artifacts_cache_path def self.artifacts_cache_path
File.expand_path('shared/artifacts/tmp/cache/', Rails.root) File.join(self.artifacts_path, 'tmp/cache/')
end end
def initialize(build, field) def initialize(build, field)
......
...@@ -16,11 +16,11 @@ ...@@ -16,11 +16,11 @@
- unless user.linkedin.blank? - unless user.linkedin.blank?
%li %li
%span.light LinkedIn: %span.light LinkedIn:
%strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}" %strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}"
- unless user.twitter.blank? - unless user.twitter.blank?
%li %li
%span.light Twitter: %span.light Twitter:
%strong= link_to user.twitter, "http://www.twitter.com/#{user.twitter}" %strong= link_to user.twitter, "https://twitter.com/#{user.twitter}"
- unless user.website_url.blank? - unless user.website_url.blank?
%li %li
%span.light Website: %span.light Website:
......
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
.milestones .milestones
%ul.content-list %ul.content-list
- if @dashboard_milestones.blank? - if @milestones.blank?
%li %li
.nothing-here-block No milestones to show .nothing-here-block No milestones to show
- else - else
- @dashboard_milestones.each do |milestone| - @milestones.each do |milestone|
= render 'milestone', milestone: milestone = render 'milestone', milestone: milestone
= paginate @dashboard_milestones, theme: "gitlab" = paginate @milestones, theme: "gitlab"
- page_title @dashboard_milestone.title, "Milestones" - page_title @milestone.title, "Milestones"
%h4.page-title %h4.page-title
.issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- if @dashboard_milestone.closed? - if @milestone.closed?
Closed Closed
- else - else
Open Open
Milestone #{@dashboard_milestone.title} Milestone #{@milestone.title}
%hr %hr
- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active? - if @milestone.complete? && @milestone.active?
.alert.alert-success .alert.alert-success
%span All issues for this milestone are closed. You may close the milestone now. %span All issues for this milestone are closed. You may close the milestone now.
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%th Open issues %th Open issues
%th State %th State
%th Due date %th Due date
- @dashboard_milestone.milestones.each do |milestone| - @milestone.milestones.each do |milestone|
%tr %tr
%td %td
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
...@@ -39,46 +39,46 @@ ...@@ -39,46 +39,46 @@
.context .context
%p.lead %p.lead
Progress: Progress:
#{@dashboard_milestone.closed_items_count} closed #{@milestone.closed_items_count} closed
&ndash; &ndash;
#{@dashboard_milestone.open_items_count} open #{@milestone.open_items_count} open
= milestone_progress_bar(@dashboard_milestone) = milestone_progress_bar(@milestone)
%ul.nav.nav-tabs %ul.nav.nav-tabs
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do = link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues Issues
%span.badge= @dashboard_milestone.issue_count %span.badge= @milestone.issue_count
%li %li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests Merge Requests
%span.badge= @dashboard_milestone.merge_requests_count %span.badge= @milestone.merge_requests_count
%li %li
= link_to '#tab-participants', 'data-toggle' => 'tab' do = link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants Participants
%span.badge= @dashboard_milestone.participants.count %span.badge= @milestone.participants.count
.pull-right .pull-right
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped" = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
.tab-content .tab-content
.tab-pane.active#tab-issues .tab-pane.active#tab-issues
.row .row
.col-md-6 .col-md-6
= render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues = render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6 .col-md-6
= render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues = render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests .tab-pane#tab-merge-requests
.row .row
.col-md-6 .col-md-6
= render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6 .col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants .tab-pane#tab-participants
%ul.bordered-list %ul.bordered-list
- @dashboard_milestone.participants.each do |user| - @milestone.participants.each do |user|
%li %li
= link_to user, title: user.name, class: "darken" do = link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32" = image_tag avatar_icon(user, 32), class: "avatar s32"
......
...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear ...@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url xml.id dashboard_projects_url
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event| @events.each do |event|
event_to_atom(xml, event) event_to_atom(xml, event)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment