Commit 7d9ef112 authored by Marin Jankovski's avatar Marin Jankovski

Merge remote-tracking branch 'ce/master' into merge_ce

Conflicts:
	README.md
	app/assets/javascripts/project.js.coffee
	app/models/concerns/mentionable.rb
	app/models/project.rb
	app/models/project_services/jira_service.rb
	app/models/service.rb
	app/models/user.rb
	app/services/git_push_service.rb
	config/initializers/1_settings.rb
	config/routes.rb
	db/schema.rb
	doc/integration/README.md
	doc/integration/external-issue-tracker.md
	doc/integration/github.md
	spec/controllers/import/github_controller_spec.rb
	spec/helpers/merge_requests_helper.rb
	spec/lib/gitlab/upgrader_spec.rb
	spec/models/note_spec.rb
	spec/requests/api/groups_spec.rb
	spec/services/git_push_service_spec.rb
parents 7f5e67a6 6fa752a8
CHANGELOG merge=union
\ No newline at end of file
Note: The upcoming release contains empty lines to reduce the number of merge conflicts, scroll down to see past releases. v 7.8.0 (unreleased)
v 7.8.0
- Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner)
- Make project search case insensitive (Hannes Rosenögger) - Make project search case insensitive (Hannes Rosenögger)
- Include issue/mr participants in list of recipients for reassign/close/reopen emails - Include issue/mr participants in list of recipients for reassign/close/reopen emails
...@@ -10,63 +8,53 @@ v 7.8.0 ...@@ -10,63 +8,53 @@ v 7.8.0
- Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger) - Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger)
- Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen) - Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
- View note image attachments in new tab when clicked instead of downloading them - View note image attachments in new tab when clicked instead of downloading them
- - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
- Allow more variations for commit messages closing issues (Julien Bianchi and Hannes Rosenögger) - Allow more variations for commit messages closing issues (Julien Bianchi and Hannes Rosenögger)
- - Fix overflow at sidebar when have several itens
- - Add notes for label changes in issue and merge requests
- Show tags in commit view (Hannes Rosenögger) - Show tags in commit view (Hannes Rosenögger)
- Only count a user's vote once on a merge request or issue (Michael Clarke) - Only count a user's vote once on a merge request or issue (Michael Clarke)
-
- Increate font size when browse source files and diffs - Increate font size when browse source files and diffs
- Create new file in empty repository using GitLab UI - Create new file in empty repository using GitLab UI
-
- Ability to clone project using oauth2 token - Ability to clone project using oauth2 token
-
- Upgrade Sidekiq gem to version 3.3.0 - Upgrade Sidekiq gem to version 3.3.0
- Stop git zombie creation during force push check - Stop git zombie creation during force push check
- Show success/error messages for test setting button in services - Show success/error messages for test setting button in services
- Added Rubocop for code style checks - Added Rubocop for code style checks
- Fix commits pagination - Fix commits pagination
-
- Async load a branch information at the commit page - Async load a branch information at the commit page
- Disable blacklist validation for project names - Disable blacklist validation for project names
- Allow configuring protection of the default branch upon first push (Marco Wessel) - Allow configuring protection of the default branch upon first push (Marco Wessel)
-
- Add gitlab.com importer - Add gitlab.com importer
- Add an ability to login with gitlab.com - Add an ability to login with gitlab.com
-
- Add a commit calendar to the user profile (Hannes Rosenögger) - Add a commit calendar to the user profile (Hannes Rosenögger)
-
- Submit comment on command-enter - Submit comment on command-enter
- - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
- Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close"
- Fix long broadcast message cut-off on left sidebar (Visay Keo) - Fix long broadcast message cut-off on left sidebar (Visay Keo)
- Add Project Avatars (Steven Thonus and Hannes Rosenögger) - Add Project Avatars (Steven Thonus and Hannes Rosenögger)
-
-
- Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
- - Edit group members via API
-
- Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks) - Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks)
-
-
- Add action property to merge request hook (Julien Bianchi) - Add action property to merge request hook (Julien Bianchi)
-
- Remove duplicates from group milestone participants list. - Remove duplicates from group milestone participants list.
-
-
- Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger) - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger)
-
-
- API: Access groups with their path (Julien Bianchi) - API: Access groups with their path (Julien Bianchi)
- Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard) - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard)
- - Allow notification email to be set separately from primary email.
-
- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
- - Don't have Markdown preview fail for long comments/wiki pages.
-
- When test web hook - show error message instead of 500 error page if connection to hook url was reset - When test web hook - show error message instead of 500 error page if connection to hook url was reset
- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
- Added persistent collapse button for left side nav bar (Jason Blanchard) - Added persistent collapse button for left side nav bar (Jason Blanchard)
- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
- Don't allow page to be scaled on mobile.
- Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up.
- Show assignees in merge request index page (Kelvin Mutuma)
- Link head panel titles to relevant root page.
- Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S).
- Show users button to share their newly created public or internal projects on twitter
- Add quick help links to the GitLab pricing and feature comparison pages.
v 7.7.2 v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
...@@ -102,9 +90,9 @@ v 7.7.0 ...@@ -102,9 +90,9 @@ v 7.7.0
- When accept merge request - do merge using sidaekiq job - When accept merge request - do merge using sidaekiq job
- Enable web signups by default - Enable web signups by default
- Fixes for diff comments: drag-n-drop images, selecting images - Fixes for diff comments: drag-n-drop images, selecting images
- Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update
- Remove password strength indicator - Remove password strength indicator
v 7.6.0 v 7.6.0
......
...@@ -63,7 +63,7 @@ If you can, please submit a merge request with the fix or improvements including ...@@ -63,7 +63,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Fork the project on GitLab Cloud 1. Fork the project on GitLab Cloud
1. Create a feature branch 1. Create a feature branch
1. Write [tests](README.md#run-the-tests) and code 1. Write [tests](README.md#run-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG) insert your line at a [random point](doc/workflow/gitlab_flow.md#do-not-order-commits-with-rebase) in the current version 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](http://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
......
...@@ -154,6 +154,9 @@ gem "gemnasium-gitlab-service", "~> 0.2" ...@@ -154,6 +154,9 @@ gem "gemnasium-gitlab-service", "~> 0.2"
# Slack integration # Slack integration
gem "slack-notifier", "~> 1.0.0" gem "slack-notifier", "~> 1.0.0"
# Asana integration
gem 'asana', '~> 0.0.6'
# d3 # d3
gem "d3_rails", "~> 3.1.4" gem "d3_rails", "~> 3.1.4"
...@@ -221,7 +224,7 @@ group :development, :test do ...@@ -221,7 +224,7 @@ group :development, :test do
gem 'rubocop', '0.28.0', require: false gem 'rubocop', '0.28.0', require: false
# gem 'rails-dev-tweaks' # gem 'rails-dev-tweaks'
gem 'spinach-rails' gem 'spinach-rails'
gem "rspec-rails" gem "rspec-rails", '2.99'
gem "capybara", '~> 2.2.1' gem "capybara", '~> 2.2.1'
gem "pry-rails" gem "pry-rails"
gem "awesome_print" gem "awesome_print"
......
...@@ -23,6 +23,10 @@ GEM ...@@ -23,6 +23,10 @@ GEM
activemodel (= 4.1.1) activemodel (= 4.1.1)
activesupport (= 4.1.1) activesupport (= 4.1.1)
arel (~> 5.0.0) arel (~> 5.0.0)
activeresource (4.0.0)
activemodel (~> 4.0)
activesupport (~> 4.0)
rails-observers (~> 0.1.1)
activesupport (4.1.1) activesupport (4.1.1)
i18n (~> 0.6, >= 0.6.9) i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
...@@ -36,6 +40,8 @@ GEM ...@@ -36,6 +40,8 @@ GEM
activerecord (>= 2.3.0) activerecord (>= 2.3.0)
rake (>= 0.8.7) rake (>= 0.8.7)
arel (5.0.1.20140414130214) arel (5.0.1.20140414130214)
asana (0.0.6)
activeresource (>= 3.2.3)
asciidoctor (0.1.4) asciidoctor (0.1.4)
ast (2.0.0) ast (2.0.0)
astrolabe (1.3.0) astrolabe (1.3.0)
...@@ -410,6 +416,8 @@ GEM ...@@ -410,6 +416,8 @@ GEM
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.1.1) railties (= 4.1.1)
sprockets-rails (~> 2.0) sprockets-rails (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
rails_autolink (1.1.6) rails_autolink (1.1.6)
rails (> 3.1) rails (> 3.1)
railties (4.1.1) railties (4.1.1)
...@@ -452,21 +460,25 @@ GEM ...@@ -452,21 +460,25 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
rinku (1.7.3) rinku (1.7.3)
rouge (1.7.4) rouge (1.7.4)
rspec (2.14.1) rspec (2.99.0)
rspec-core (~> 2.14.0) rspec-core (~> 2.99.0)
rspec-expectations (~> 2.14.0) rspec-expectations (~> 2.99.0)
rspec-mocks (~> 2.14.0) rspec-mocks (~> 2.99.0)
rspec-core (2.14.7) rspec-collection_matchers (1.1.2)
rspec-expectations (2.14.4) rspec-expectations (>= 2.99.0.beta1)
rspec-core (2.99.2)
rspec-expectations (2.99.2)
diff-lcs (>= 1.1.3, < 2.0) diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.14.4) rspec-mocks (2.99.3)
rspec-rails (2.14.0) rspec-rails (2.99.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activemodel (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 2.14.0) rspec-collection_matchers
rspec-expectations (~> 2.14.0) rspec-core (~> 2.99.0)
rspec-mocks (~> 2.14.0) rspec-expectations (~> 2.99.0)
rspec-mocks (~> 2.99.0)
rubocop (0.28.0) rubocop (0.28.0)
astrolabe (~> 1.3) astrolabe (~> 1.3)
parser (>= 2.2.0.pre.7, < 3.0) parser (>= 2.2.0.pre.7, < 3.0)
...@@ -632,6 +644,7 @@ DEPENDENCIES ...@@ -632,6 +644,7 @@ DEPENDENCIES
acts-as-taggable-on acts-as-taggable-on
addressable addressable
annotate (~> 2.6.0.beta2) annotate (~> 2.6.0.beta2)
asana (~> 0.0.6)
asciidoctor (= 0.1.4) asciidoctor (= 0.1.4)
awesome_print awesome_print
better_errors better_errors
...@@ -721,7 +734,7 @@ DEPENDENCIES ...@@ -721,7 +734,7 @@ DEPENDENCIES
redcarpet (~> 3.1.2) redcarpet (~> 3.1.2)
redis-rails redis-rails
request_store request_store
rspec-rails rspec-rails (= 2.99)
rubocop (= 0.28.0) rubocop (= 0.28.0)
rugments rugments
sanitize (~> 2.0) sanitize (~> 2.0)
......
...@@ -22,7 +22,7 @@ https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md#updating- ...@@ -22,7 +22,7 @@ https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md#updating-
If you need help with your GitLab installation and for any technical questions please contact us at subscribers@gitlab.com. If you need help with your GitLab installation and for any technical questions please contact us at subscribers@gitlab.com.
For all other questions, contact us at sales@gitlab.com For all other questions, contact us at sales@gitlab.com
## Open source software to collaborate on code ## Open source software to collaborate on code
...@@ -33,14 +33,14 @@ For all other questions, contact us at sales@gitlab.com ...@@ -33,14 +33,14 @@ For all other questions, contact us at sales@gitlab.com
- Each project can also have an issue tracker and a wiki - Each project can also have an issue tracker and a wiki
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- Completely free and open source (MIT Expat license) - Completely free and open source (MIT Expat license)
- Powered by Ruby on Rails - Powered by [Ruby on Rails](https://github.com/rails/rails)
## Editions ## Editions
There are two editions of GitLab. There are two editions of GitLab.
GitLab [Community Edition](https://about.gitlab.com/features/) (CE) is available without any costs under an MIT license. *GitLab [Community Edition](https://about.gitlab.com/features/) (CE)* is available without any costs under an MIT license.
GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are most useful for organizations with more than 100 users. *GitLab Enterprise Edition (EE)* includes [extra features](https://about.gitlab.com/features/#compare) that are most useful for organizations with more than 100 users.
To get access to the EE and support please [become a subscriber](https://about.gitlab.com/pricing/). To get access to the EE and support please [become a subscriber](https://about.gitlab.com/pricing/).
## Canonical source ## Canonical source
...@@ -72,42 +72,45 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a ...@@ -72,42 +72,45 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a
## Requirements ## Requirements
- Ubuntu/Debian/CentOS/RHEL** GitLab requires the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.0 or 2.1 - Ruby (MRI) 2.0 or 2.1
- git 1.7.10+ - Git 1.7.10+
- redis 2.0+ - Redis 2.0+
- MySQL or PostgreSQL - MySQL or PostgreSQL
** More details are in the [requirements doc](doc/install/requirements.md). Please see the [requirements documentation](doc/install/requirements.md) for system requirements and more information about the supported operating systems.
## Installation ## Installation
Please see [the installation page on the GitLab website](https://about.gitlab.com/installation/) for the various options. The recommended way to install GitLab is using the provided [Omnibus packages](https://about.gitlab.com/downloads/). Compared to an installation from source, this is faster and less error prone. Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager.
Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm).
You can access new installation with the login `root` and password `5iveL!fe`, after login you are required to set a unique password. There are various other options to install GitLab, please refer to the [installation page on the GitLab website](https://about.gitlab.com/installation/) for more information.
You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password.
## Third-party applications ## Third-party applications
There are a lot of applications and API wrappers for GitLab. There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages.
Find them [on our website](https://about.gitlab.com/applications/).
## New versions ## GitLab release cycle
Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases come out when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the release [documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457). Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457).
## Upgrading ## Upgrading
For updating the the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For manual installations there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update). For updating the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For installations from source there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update) detailing all necessary commands to migrate to the next version.
## Install a development environment ## Install a development environment
We recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
If you do not use the GitLab Development Development kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. If you do not use the GitLab Development Kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone.
One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file:
cp config/unicorn.rb.example.development config/unicorn.rb cp config/unicorn.rb.example.development config/unicorn.rb
Instructions on how to start Gitlab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development).
## Documentation ## Documentation
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
#= require jquery.blockUI #= require jquery.blockUI
#= require jquery.turbolinks #= require jquery.turbolinks
#= require turbolinks #= require turbolinks
#= require autosave
#= require bootstrap #= require bootstrap
#= require select2 #= require select2
#= require raphael #= require raphael
......
class @Autosave
constructor: (field, key) ->
@field = field
key = key.join("/") if key.join?
@key = "autosave/#{key}"
@field.data "autosave", this
@restore()
@field.on "input", => @save()
restore: ->
return unless window.localStorage?
text = window.localStorage.getItem @key
@field.val text if text?.length > 0
@field.trigger "input"
save: ->
return unless window.localStorage?
text = @field.val()
if text?.length > 0
window.localStorage.setItem @key, text
else
@reset()
reset: ->
return unless window.localStorage?
window.localStorage.removeItem @key
\ No newline at end of file
...@@ -50,7 +50,7 @@ class @DropzoneInput ...@@ -50,7 +50,7 @@ class @DropzoneInput
preview.text "Nothing to preview." preview.text "Nothing to preview."
else else
preview.text "Loading..." preview.text "Loading..."
$.get($(this).data("url"), $.post($(this).data("url"),
md_text: mdText md_text: mdText
).success (previewData) -> ).success (previewData) ->
preview.html previewData preview.html previewData
......
...@@ -4,7 +4,7 @@ class @ImporterStatus ...@@ -4,7 +4,7 @@ class @ImporterStatus
this.setAutoUpdate() this.setAutoUpdate()
initStatusPage: -> initStatusPage: ->
$(".btn-add-to-import").click (event) => $(".js-add-to-import").click (event) =>
new_namespace = null new_namespace = null
tr = $(event.currentTarget).closest("tr") tr = $(event.currentTarget).closest("tr")
id = tr.attr("id").replace("repo_", "") id = tr.attr("id").replace("repo_", "")
...@@ -12,6 +12,10 @@ class @ImporterStatus ...@@ -12,6 +12,10 @@ class @ImporterStatus
new_namespace = tr.find(".import-target input").prop("value") new_namespace = tr.find(".import-target input").prop("value")
tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name")) tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name"))
$.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script' $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script'
$(".js-import-all").click (event) =>
$(".js-add-to-import").each ->
$(this).click()
setAutoUpdate: -> setAutoUpdate: ->
setInterval (=> setInterval (=>
......
...@@ -58,7 +58,8 @@ class @Notes ...@@ -58,7 +58,8 @@ class @Notes
$(document).on "visibilitychange", @visibilityChange $(document).on "visibilitychange", @visibilityChange
@notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea'
$(document).on('keypress', @notes_forms, (e)-> # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown.
$(document).on('keydown', @notes_forms, (e) ->
if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13)
$(@).parents('form').submit() $(@).parents('form').submit()
) )
...@@ -170,6 +171,8 @@ class @Notes ...@@ -170,6 +171,8 @@ class @Notes
form.find(".js-md-write-button").click() form.find(".js-md-write-button").click()
form.find(".js-note-text").val("").trigger "input" form.find(".js-note-text").val("").trigger "input"
form.find(".js-note-text").data("autosave").reset()
### ###
Called when clicking the "Choose File" button. Called when clicking the "Choose File" button.
...@@ -220,12 +223,22 @@ class @Notes ...@@ -220,12 +223,22 @@ class @Notes
# setup preview buttons # setup preview buttons
form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left" form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button") previewButton = form.find(".js-md-preview-button")
form.find(".js-note-text").on "input", ->
textarea = form.find(".js-note-text")
textarea.on "input", ->
if $(this).val().trim() isnt "" if $(this).val().trim() isnt ""
previewButton.removeClass("turn-off").addClass "turn-on" previewButton.removeClass("turn-off").addClass "turn-on"
else else
previewButton.removeClass("turn-on").addClass "turn-off" previewButton.removeClass("turn-on").addClass "turn-off"
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
form.find("#note_line_code").val()
form.find("#note_noteable_type").val()
form.find("#note_noteable_id").val()
]
# remove notify commit author checkbox for non-commit notes # remove notify commit author checkbox for non-commit notes
form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit" form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit"
...@@ -233,7 +246,6 @@ class @Notes ...@@ -233,7 +246,6 @@ class @Notes
new DropzoneInput(form) new DropzoneInput(form)
form.show() form.show()
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -407,6 +419,8 @@ class @Notes ...@@ -407,6 +419,8 @@ class @Notes
removeDiscussionNoteForm: (form)-> removeDiscussionNoteForm: (form)->
row = form.closest("tr") row = form.closest("tr")
form.find(".js-note-text").data("autosave").reset()
# show the reply button (will only work for replies) # show the reply button (will only work for replies)
form.prev(".js-discussion-reply-button").show() form.prev(".js-discussion-reply-button").show()
if row.is(".js-temp-notes-holder") if row.is(".js-temp-notes-holder")
......
...@@ -16,5 +16,11 @@ class @Project ...@@ -16,5 +16,11 @@ class @Project
$('.hide-no-ssh-message').on 'click', (e) -> $('.hide-no-ssh-message').on 'click', (e) ->
path = '/' path = '/'
$.cookie('hide_no_ssh_message', 'false', { path: path }) $.cookie('hide_no_ssh_message', 'false', { path: path })
$(@).parents('.no-ssh-key-message').hide() $(@).parents('.no-ssh-key-message').remove()
e.preventDefault() e.preventDefault()
\ No newline at end of file
$('.hide-no-password-message').on 'click', (e) ->
path = '/'
$.cookie('hide_no_password_message', 'false', { path: path })
$(@).parents('.no-password-message').remove()
e.preventDefault()
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
&.s24 { margin-right: 4px; } &.s24 { margin-right: 4px; }
} }
&.avatar-tile { &.group-avatar, &.project-avatar, &.avatar-tile {
@include border-radius(0px); @include border-radius(0px);
} }
......
...@@ -342,6 +342,10 @@ table { ...@@ -342,6 +342,10 @@ table {
margin-bottom: 9px; margin-bottom: 9px;
} }
.wiki .code {
overflow-x: auto;
}
.footer-links a { .footer-links a {
margin-right: 15px; margin-right: 15px;
} }
......
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
box-shadow: none; box-shadow: none;
background: $box_bg; background: $box_bg;
padding: 1em; padding: 1em;
overflow-x: auto;
code { code {
font-family: $monospace_font; font-family: $monospace_font;
......
...@@ -75,9 +75,6 @@ ...@@ -75,9 +75,6 @@
} }
} }
} }
.project-avatar {
float: left;
}
.project-description { .project-description {
overflow: hidden; overflow: hidden;
......
...@@ -64,6 +64,10 @@ ...@@ -64,6 +64,10 @@
.md { .md {
font-size: 13px; font-size: 13px;
iframe.twitter-share-button {
vertical-align: bottom;
}
} }
pre { pre {
......
...@@ -40,12 +40,15 @@ ...@@ -40,12 +40,15 @@
.login-heading h3 { .login-heading h3 {
font-weight: 300; font-weight: 300;
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0 0 10px 0;
display: none;
} }
.login-footer { .login-footer {
margin-top: 10px; margin-top: 10px;
p:last-child {
margin-bottom: 0;
}
} }
a.forgot { a.forgot {
...@@ -88,6 +91,7 @@ ...@@ -88,6 +91,7 @@
.devise-errors { .devise-errors {
h2 { h2 {
margin-top: 0;
font-size: 14px; font-size: 14px;
color: #a00; color: #a00;
} }
......
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
.nav-sidebar { .nav-sidebar {
margin-top: 20px; margin-top: 20px;
position: absolute; position: fixed;
top: 45px; top: 45px;
width: 52px; width: 52px;
......
...@@ -32,7 +32,6 @@ ...@@ -32,7 +32,6 @@
.avatar { .avatar {
width: 70px; width: 70px;
height: 70px; height: 70px;
@include border-radius(0px);
} }
.identicon { .identicon {
......
...@@ -26,6 +26,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -26,6 +26,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:signup_enabled, :signup_enabled,
:signin_enabled, :signin_enabled,
:gravatar_enabled, :gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text, :sign_in_text,
:home_page_url :home_page_url
) )
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
def index def index
@projects = Project.order("created_at DESC").limit(10) @projects = Project.limit(10)
@users = User.order("created_at DESC").limit(10) @users = User.limit(10)
@groups = Group.order("created_at DESC").limit(10) @groups = Group.limit(10)
end end
end end
...@@ -2,7 +2,8 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -2,7 +2,8 @@ class Admin::GroupsController < Admin::ApplicationController
before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update]
def index def index
@groups = Group.order('name ASC') @groups = Group.all
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]).per(20) @groups = @groups.page(params[:page]).per(20)
end end
......
class Admin::ServicesController < Admin::ApplicationController
before_filter :service, only: [:edit, :update]
def index
@services = services_templates
end
def edit
unless service.present?
redirect_to admin_application_settings_services_path,
alert: "Service is unknown or it doesn't exist"
end
end
def update
if service.update_attributes(application_services_params[:service])
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
else
render :edit
end
end
private
def services_templates
templates = []
Service.available_services_names.each do |service_name|
service_template = service_name.concat("_service").camelize.constantize
templates << service_template.where(template: true).first_or_create
end
templates
end
def service
@service ||= Service.where(id: params[:id], template: true).first
end
def application_services_params
params.permit(:id,
service: [
:title, :token, :type, :active, :api_key, :subdomain,
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch
])
end
end
...@@ -2,16 +2,16 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -2,16 +2,16 @@ class Admin::UsersController < Admin::ApplicationController
before_filter :user, only: [:show, :edit, :update, :destroy] before_filter :user, only: [:show, :edit, :update, :destroy]
def index def index
@users = User.filter(params[:filter]) @users = User.order_name_asc.filter(params[:filter])
@users = @users.search(params[:name]) if params[:name].present? @users = @users.search(params[:name]) if params[:name].present?
@users = @users.sort(@sort = params[:sort]) @users = @users.sort(@sort = params[:sort])
@users = @users.alphabetically.page(params[:page]) @users = @users.page(params[:page])
end end
def show def show
@personal_projects = user.personal_projects @personal_projects = user.personal_projects
@joined_projects = user.projects.joined(@user) @joined_projects = user.projects.joined(@user)
@keys = user.keys.order('id DESC') @keys = user.keys
end end
def new def new
...@@ -102,6 +102,9 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -102,6 +102,9 @@ class Admin::UsersController < Admin::ApplicationController
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
email.destroy email.destroy
user.set_notification_email
user.save if user.notification_email_changed?
respond_to do |format| respond_to do |format|
format.html { redirect_to :back, notice: "Successfully removed email." } format.html { redirect_to :back, notice: "Successfully removed email." }
format.js { render nothing: true } format.js { render nothing: true }
...@@ -118,7 +121,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -118,7 +121,7 @@ class Admin::UsersController < Admin::ApplicationController
params.require(:user).permit( params.require(:user).permit(
:email, :remember_me, :bio, :name, :username, :email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
:projects_limit, :can_create_group, :admin, :key_id :projects_limit, :can_create_group, :admin, :key_id
) )
end end
......
...@@ -260,7 +260,7 @@ class ApplicationController < ActionController::Base ...@@ -260,7 +260,7 @@ class ApplicationController < ActionController::Base
end end
def set_filters_params def set_filters_params
params[:sort] ||= 'newest' params[:sort] ||= 'created_desc'
params[:scope] = 'all' if params[:scope].blank? params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank? params[:state] = 'opened' if params[:state].blank?
...@@ -286,7 +286,7 @@ class ApplicationController < ActionController::Base ...@@ -286,7 +286,7 @@ class ApplicationController < ActionController::Base
author_id = @filter_params[:author_id] author_id = @filter_params[:author_id]
milestone_id = @filter_params[:milestone_id] milestone_id = @filter_params[:milestone_id]
@sort = @filter_params[:sort].try(:humanize) @sort = @filter_params[:sort]
@assignees = User.where(id: collection.pluck(:assignee_id)) @assignees = User.where(id: collection.pluck(:assignee_id))
@authors = User.where(id: collection.pluck(:author_id)) @authors = User.where(id: collection.pluck(:author_id))
@milestones = Milestone.where(id: collection.pluck(:milestone_id)) @milestones = Milestone.where(id: collection.pluck(:milestone_id))
......
...@@ -9,7 +9,7 @@ class DashboardController < ApplicationController ...@@ -9,7 +9,7 @@ class DashboardController < ApplicationController
# If user needs more - point to Dashboard#projects page # If user needs more - point to Dashboard#projects page
@projects_limit = 30 @projects_limit = 30
@groups = current_user.authorized_groups.sort_by(&:human_name) @groups = current_user.authorized_groups.order_name_asc
@has_authorized_projects = @projects.count > 0 @has_authorized_projects = @projects.count > 0
@projects_count = @projects.count @projects_count = @projects.count
@projects = @projects.limit(@projects_limit) @projects = @projects.limit(@projects_limit)
......
...@@ -45,7 +45,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -45,7 +45,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user if current_user
# Add new authentication method # Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
redirect_to profile_path redirect_to profile_account_path, notice: 'Authentication method updated'
else else
@user = Gitlab::OAuth::User.new(oauth) @user = Gitlab::OAuth::User.new(oauth)
@user.save @user.save
......
...@@ -18,6 +18,9 @@ class Profiles::EmailsController < ApplicationController ...@@ -18,6 +18,9 @@ class Profiles::EmailsController < ApplicationController
@email = current_user.emails.find(params[:id]) @email = current_user.emails.find(params[:id])
@email.destroy @email.destroy
current_user.set_notification_email
current_user.save if current_user.notification_email_changed?
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url } format.html { redirect_to profile_emails_url }
format.js { render nothing: true } format.js { render nothing: true }
......
...@@ -3,7 +3,7 @@ class Profiles::KeysController < ApplicationController ...@@ -3,7 +3,7 @@ class Profiles::KeysController < ApplicationController
skip_before_filter :authenticate_user!, only: [:get_keys] skip_before_filter :authenticate_user!, only: [:get_keys]
def index def index
@keys = current_user.keys.order('id DESC') @keys = current_user.keys
end end
def show def show
......
...@@ -2,6 +2,7 @@ class Profiles::NotificationsController < ApplicationController ...@@ -2,6 +2,7 @@ class Profiles::NotificationsController < ApplicationController
layout 'profile' layout 'profile'
def show def show
@user = current_user
@notification = current_user.notification @notification = current_user.notification
@project_members = current_user.project_members @project_members = current_user.project_members
@group_members = current_user.group_members @group_members = current_user.group_members
...@@ -11,8 +12,7 @@ class Profiles::NotificationsController < ApplicationController ...@@ -11,8 +12,7 @@ class Profiles::NotificationsController < ApplicationController
type = params[:notification_type] type = params[:notification_type]
@saved = if type == 'global' @saved = if type == 'global'
current_user.notification_level = params[:notification_level] current_user.update_attributes(user_params)
current_user.save
elsif type == 'group' elsif type == 'group'
users_group = current_user.group_members.find(params[:notification_id]) users_group = current_user.group_members.find(params[:notification_id])
users_group.notification_level = params[:notification_level] users_group.notification_level = params[:notification_level]
...@@ -22,5 +22,23 @@ class Profiles::NotificationsController < ApplicationController ...@@ -22,5 +22,23 @@ class Profiles::NotificationsController < ApplicationController
project_member.notification_level = params[:notification_level] project_member.notification_level = params[:notification_level]
project_member.save project_member.save
end end
respond_to do |format|
format.html do
if @saved
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
end
redirect_to :back
end
format.js
end
end
def user_params
params.require(:user).permit(:notification_email, :notification_level)
end end
end end
...@@ -11,7 +11,7 @@ class Profiles::PasswordsController < ApplicationController ...@@ -11,7 +11,7 @@ class Profiles::PasswordsController < ApplicationController
end end
def create def create
unless @user.valid_password?(user_params[:current_password]) unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
redirect_to new_profile_password_path, alert: 'You must provide a valid current password' redirect_to new_profile_password_path, alert: 'You must provide a valid current password'
return return
end end
...@@ -21,7 +21,8 @@ class Profiles::PasswordsController < ApplicationController ...@@ -21,7 +21,8 @@ class Profiles::PasswordsController < ApplicationController
result = @user.update_attributes( result = @user.update_attributes(
password: new_password, password: new_password,
password_confirmation: new_password_confirmation password_confirmation: new_password_confirmation,
password_automatically_set: false
) )
if result if result
...@@ -39,8 +40,9 @@ class Profiles::PasswordsController < ApplicationController ...@@ -39,8 +40,9 @@ class Profiles::PasswordsController < ApplicationController
password_attributes = user_params.select do |key, value| password_attributes = user_params.select do |key, value|
%w(password password_confirmation).include?(key.to_s) %w(password password_confirmation).include?(key.to_s)
end end
password_attributes[:password_automatically_set] = false
unless @user.valid_password?(user_params[:current_password]) unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
redirect_to edit_profile_password_path, alert: 'You must provide a valid current password' redirect_to edit_profile_password_path, alert: 'You must provide a valid current password'
return return
end end
......
...@@ -67,7 +67,7 @@ class ProfilesController < ApplicationController ...@@ -67,7 +67,7 @@ class ProfilesController < ApplicationController
params.require(:user).permit( params.require(:user).permit(
:email, :password, :password_confirmation, :bio, :name, :username, :email, :password, :password_confirmation, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id,
:avatar, :hide_no_ssh_key, :avatar, :hide_no_ssh_key, :hide_no_password
) )
end end
end end
...@@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = @repo.commits(@ref, @path, @limit, @offset) @commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = Note.where(commit_id: @commits.map(&:id)). @note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -7,7 +7,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to :js, :html respond_to :js, :html
def index def index
@labels = @project.labels.order_by_name.page(params[:page]).per(20) @labels = @project.labels.page(params[:page]).per(20)
end end
def new def new
......
...@@ -23,7 +23,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def show def show
@note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)). @note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -29,7 +29,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -29,7 +29,7 @@ class Projects::ServicesController < Projects::ApplicationController
if @service.execute(data) if @service.execute(data)
message = { notice: 'We sent a request to the provided URL' } message = { notice: 'We sent a request to the provided URL' }
else else
message = { alert: 'We tried to send a request to the provided URL but error occured' } message = { alert: 'We tried to send a request to the provided URL but an error occured' }
end end
redirect_to :back, message redirect_to :back, message
...@@ -47,7 +47,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -47,7 +47,7 @@ class Projects::ServicesController < Projects::ApplicationController
:room, :recipients, :project_url, :webhook, :room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :build_type, :build_key, :server, :teamcity_url, :build_type,
:description, :issues_url, :new_issue_url :description, :issues_url, :new_issue_url, :restrict_to_branch
) )
end end
end end
...@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
tag = @repository.find_tag(params[:id]) tag = @repository.find_tag(params[:id])
if tag && @repository.rm_tag(tag.name) if tag && @repository.rm_tag(tag.name)
Event.create_ref_event(@project, current_user, tag, 'rm', 'refs/tags') EventCreateService.new.push_ref(@project, current_user, tag, 'rm', 'refs/tags')
end end
respond_to do |format| respond_to do |format|
......
...@@ -102,7 +102,7 @@ class ProjectsController < ApplicationController ...@@ -102,7 +102,7 @@ class ProjectsController < ApplicationController
note_type = params['type'] note_type = params['type']
note_id = params['type_id'] note_id = params['type_id']
autocomplete = ::Projects::AutocompleteService.new(@project) autocomplete = ::Projects::AutocompleteService.new(@project)
participants = ::Projects::ParticipantsService.new(@project).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
emojis: autocomplete_emojis, emojis: autocomplete_emojis,
......
...@@ -106,6 +106,7 @@ class SnippetsController < ApplicationController ...@@ -106,6 +106,7 @@ class SnippetsController < ApplicationController
def set_title def set_title
@title = 'Snippets' @title = 'Snippets'
@title_url = snippets_path
end end
def snippet_params def snippet_params
......
...@@ -19,6 +19,7 @@ class UsersController < ApplicationController ...@@ -19,6 +19,7 @@ class UsersController < ApplicationController
where(project_id: authorized_projects_ids).limit(30) where(project_id: authorized_projects_ids).limit(30)
@title = @user.name @title = @user.name
@title_url = user_path(@user)
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -10,18 +10,18 @@ class NotesFinder ...@@ -10,18 +10,18 @@ class NotesFinder
notes = notes =
case target_type case target_type
when "commit" when "commit"
project.notes.for_commit_id(target_id).not_inline.fresh project.notes.for_commit_id(target_id).not_inline
when "issue" when "issue"
project.issues.find(target_id).notes.inc_author.fresh project.issues.find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes.fresh project.snippets.find(target_id).notes
else else
raise 'invalid target_type' raise 'invalid target_type'
end end
# Use overlapping intervals to avoid worrying about race conditions # Use overlapping intervals to avoid worrying about race conditions
notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP) notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end end
end end
...@@ -3,6 +3,10 @@ module ApplicationSettingsHelper ...@@ -3,6 +3,10 @@ module ApplicationSettingsHelper
current_application_settings.gravatar_enabled? current_application_settings.gravatar_enabled?
end end
def twitter_sharing_enabled?
current_application_settings.twitter_sharing_enabled?
end
def signup_enabled? def signup_enabled?
current_application_settings.signup_enabled? current_application_settings.signup_enabled?
end end
......
...@@ -10,11 +10,15 @@ module EventsHelper ...@@ -10,11 +10,15 @@ module EventsHelper
end end
def event_action_name(event) def event_action_name(event)
target = if event.target_type target = if event.target_type
event.target_type.titleize.downcase if event.note?
else event.note_target_type
'project' else
end event.target_type.titleize.downcase
end
else
'project'
end
[event.action_name, target].join(" ") [event.action_name, target].join(" ")
end end
...@@ -42,21 +46,30 @@ module EventsHelper ...@@ -42,21 +46,30 @@ module EventsHelper
end end
def event_feed_title(event) def event_feed_title(event)
if event.issue? words = []
"#{event.author_name} #{event.action_name} issue ##{event.target_iid}: #{event.issue_title} at #{event.project_name}" words << event.author_name
elsif event.merge_request? words << event_action_name(event)
"#{event.author_name} #{event.action_name} MR ##{event.target_iid}: #{event.merge_request_title} at #{event.project_name}"
elsif event.push? if event.push?
"#{event.author_name} #{event.push_action_name} #{event.ref_type} #{event.ref_name} at #{event.project_name}" words << event.ref_type
elsif event.membership_changed? words << event.ref_name
"#{event.author_name} #{event.action_name} #{event.project_name}" words << "at"
elsif event.note? && event.note_commit? elsif event.commented?
"#{event.author_name} commented on #{event.note_target_type} #{event.note_short_commit_id} at #{event.project_name}" if event.note_commit?
elsif event.note? words << event.note_short_commit_id
"#{event.author_name} commented on #{event.note_target_type} ##{truncate event.note_target_iid} at #{event.project_name}" else
else words << "##{truncate event.note_target_iid}"
"" end
words << "at"
elsif event.target
words << "##{event.target_iid}:"
words << event.target.title if event.target.respond_to?(:title)
words << "at"
end end
words << event.project_name
words.join(" ")
end end
def event_feed_url(event) def event_feed_url(event)
...@@ -96,8 +109,6 @@ module EventsHelper ...@@ -96,8 +109,6 @@ module EventsHelper
render "events/event_push", event: event render "events/event_push", event: event
elsif event.merge_request? elsif event.merge_request?
render "events/event_merge_request", merge_request: event.merge_request render "events/event_merge_request", merge_request: event.merge_request
elsif event.push?
render "events/event_push", event: event
elsif event.note? elsif event.note?
render "events/event_note", note: event.note render "events/event_note", note: event.note
end end
......
...@@ -7,21 +7,34 @@ module LabelsHelper ...@@ -7,21 +7,34 @@ module LabelsHelper
label_color = label.color || Label::DEFAULT_COLOR label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color) text_color = text_color_for_bg(label_color)
content_tag :span, class: 'label color-label', style: "background:#{label_color};color:#{text_color}" do content_tag :span, class: 'label color-label', style: "background-color:#{label_color};color:#{text_color}" do
label.name label.name
end end
end end
def suggested_colors def suggested_colors
[ [
'#D9534F', '#0033CC',
'#F0AD4E',
'#428BCA', '#428BCA',
'#44AD8E',
'#A8D695',
'#5CB85C', '#5CB85C',
'#69D100',
'#004E00',
'#34495E', '#34495E',
'#7F8C8D', '#7F8C8D',
'#A295D6',
'#5843AD',
'#8E44AD', '#8E44AD',
'#FFECDB' '#FFECDB',
'#AD4363',
'#D10069',
'#CC0033',
'#FF0000',
'#D9534F',
'#D1D100',
'#F0AD4E',
'#AD8D43'
] ]
end end
......
module SortingHelper module SortingHelper
def sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_milestone_soon => sort_title_milestone_soon,
sort_value_milestone_later => sort_title_milestone_later,
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
}
end
def sort_title_oldest_updated def sort_title_oldest_updated
'Oldest updated' 'Oldest updated'
end end
...@@ -14,4 +29,68 @@ module SortingHelper ...@@ -14,4 +29,68 @@ module SortingHelper
def sort_title_recently_created def sort_title_recently_created
'Recently created' 'Recently created'
end end
def sort_title_milestone_soon
'Milestone due soon'
end
def sort_title_milestone_later
'Milestone due later'
end
def sort_title_name
'Name'
end
def sort_title_largest_repo
'Largest repository'
end
def sort_title_recently_signin
'Recent sign in'
end
def sort_title_oldest_signin
'Oldest sign in'
end
def sort_value_oldest_updated
'updated_asc'
end
def sort_value_recently_updated
'updated_desc'
end
def sort_value_oldest_created
'created_asc'
end
def sort_value_recently_created
'created_desc'
end
def sort_value_milestone_soon
'milestone_due_asc'
end
def sort_value_milestone_later
'milestone_due_desc'
end
def sort_value_name
'name_asc'
end
def sort_value_largest_repo
'repository_size_desc'
end
def sort_value_recently_signin
'recent_sign_in'
end
def sort_value_oldest_signin
'oldest_sign_in'
end
end end
...@@ -4,20 +4,20 @@ module Emails ...@@ -4,20 +4,20 @@ module Emails
@user = User.find(user_id) @user = User.find(user_id)
@target_url = user_url(@user) @target_url = user_url(@user)
@token = token @token = token
mail(to: @user.email, subject: subject("Account was created for you")) mail(to: @user.notification_email, subject: subject("Account was created for you"))
end end
def new_email_email(email_id) def new_email_email(email_id)
@email = Email.find(email_id) @email = Email.find(email_id)
@user = @email.user @user = @email.user
mail(to: @user.email, subject: subject("Email was added to your account")) mail(to: @user.notification_email, subject: subject("Email was added to your account"))
end end
def new_ssh_key_email(key_id) def new_ssh_key_email(key_id)
@key = Key.find(key_id) @key = Key.find(key_id)
@user = @key.user @user = @key.user
@target_url = user_url(@user) @target_url = user_url(@user)
mail(to: @user.email, subject: subject("SSH key was added to your account")) mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
end end
end end
end end
...@@ -12,7 +12,7 @@ module Emails ...@@ -12,7 +12,7 @@ module Emails
@user = User.find user_id @user = User.find user_id
@project = Project.find project_id @project = Project.find project_id
@target_url = project_url(@project) @target_url = project_url(@project)
mail(to: @user.email, mail(to: @user.notification_email,
subject: subject("Project was moved")) subject: subject("Project was moved"))
end end
......
...@@ -61,7 +61,7 @@ class Notify < ActionMailer::Base ...@@ -61,7 +61,7 @@ class Notify < ActionMailer::Base
# Returns a String containing the User's email address. # Returns a String containing the User's email address.
def recipient(recipient_id) def recipient(recipient_id)
if recipient = User.find(recipient_id) if recipient = User.find(recipient_id)
recipient.email recipient.notification_email
end end
end end
...@@ -112,6 +112,7 @@ class Notify < ActionMailer::Base ...@@ -112,6 +112,7 @@ class Notify < ActionMailer::Base
# See: mail_answer_thread # See: mail_answer_thread
def mail_new_thread(model, headers = {}, &block) def mail_new_thread(model, headers = {}, &block)
headers['Message-ID'] = message_id(model) headers['Message-ID'] = message_id(model)
headers['X-GitLab-Project'] = "#{@project.name} | " if @project
mail(headers, &block) mail(headers, &block)
end end
...@@ -126,6 +127,7 @@ class Notify < ActionMailer::Base ...@@ -126,6 +127,7 @@ class Notify < ActionMailer::Base
def mail_answer_thread(model, headers = {}, &block) def mail_answer_thread(model, headers = {}, &block)
headers['In-Reply-To'] = message_id(model) headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model) headers['References'] = message_id(model)
headers['X-GitLab-Project'] = "#{@project.name} | " if @project
if (headers[:subject]) if (headers[:subject])
headers[:subject].prepend('Re: ') headers[:subject].prepend('Re: ')
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
# signup_enabled :boolean # signup_enabled :boolean
# signin_enabled :boolean # signin_enabled :boolean
# gravatar_enabled :boolean # gravatar_enabled :boolean
# twitter_sharing_enabled :boolean
# sign_in_text :text # sign_in_text :text
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
...@@ -30,6 +31,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -30,6 +31,7 @@ class ApplicationSetting < ActiveRecord::Base
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'], signin_enabled: Settings.gitlab['signin_enabled'],
twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'], sign_in_text: Settings.extra['sign_in_text'],
) )
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
# #
class BroadcastMessage < ActiveRecord::Base class BroadcastMessage < ActiveRecord::Base
include Sortable
validates :message, presence: true validates :message, presence: true
validates :starts_at, presence: true validates :starts_at, presence: true
validates :ends_at, presence: true validates :ends_at, presence: true
......
...@@ -29,6 +29,8 @@ module Issuable ...@@ -29,6 +29,8 @@ module Issuable
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) } scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
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') }
delegate :name, delegate :name,
:email, :email,
...@@ -55,13 +57,10 @@ module Issuable ...@@ -55,13 +57,10 @@ module Issuable
def sort(method) def sort(method)
case method.to_s case method.to_s
when 'newest' then reorder("#{table_name}.created_at DESC") when 'milestone_due_asc' then order_milestone_due_asc
when 'oldest' then reorder("#{table_name}.created_at ASC") when 'milestone_due_desc' then order_milestone_due_desc
when 'recently_updated' then reorder("#{table_name}.updated_at DESC") else
when 'last_updated' then reorder("#{table_name}.updated_at ASC") order_by(method)
when 'milestone_due_soon' then joins(:milestone).reorder("milestones.due_date ASC")
when 'milestone_due_later' then joins(:milestone).reorder("milestones.due_date DESC")
else reorder("#{table_name}.created_at DESC")
end end
end end
end end
......
...@@ -51,9 +51,12 @@ module Mentionable ...@@ -51,9 +51,12 @@ module Mentionable
identifier = match.delete "@" identifier = match.delete "@"
if identifier == "all" if identifier == "all"
users.push(*project.team.members.flatten) users.push(*project.team.members.flatten)
else elsif namespace = Namespace.find_by(path: identifier)
id = User.find_by(username: identifier).try(:id) if namespace.type == "Group"
users << User.find(id) unless id.blank? users.push(*namespace.users)
else
users << namespace.owner
end
end end
end end
users.uniq users.uniq
...@@ -64,6 +67,7 @@ module Mentionable ...@@ -64,6 +67,7 @@ module Mentionable
return [] if text.blank? return [] if text.blank?
ext = Gitlab::ReferenceExtractor.new ext = Gitlab::ReferenceExtractor.new
ext.analyze(text, p) ext.analyze(text, p)
(ext.issues_for(p) + (ext.issues_for(p) +
ext.merge_requests_for(p) + ext.merge_requests_for(p) +
ext.commits_for(p) ext.commits_for(p)
......
# == Sortable concern
#
# Set default scope for ordering objects
#
module Sortable
extend ActiveSupport::Concern
included do
# By default all models should be ordered
# by created_at field starting from newest
default_scope { order(created_at: :desc, id: :desc) }
scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) }
end
module ClassMethods
def order_by(method)
case method.to_s
when 'name_asc' then order_name_asc
when 'name_desc' then order_name_desc
when 'updated_asc' then order_updated_asc
when 'updated_desc' then order_updated_desc
when 'created_asc' then order_created_asc
when 'created_desc' then order_created_desc
else
all
end
end
end
end
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
# #
class Email < ActiveRecord::Base class Email < ActiveRecord::Base
include Sortable
belongs_to :user belongs_to :user
validates :user_id, presence: true validates :user_id, presence: true
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
# #
class Event < ActiveRecord::Base class Event < ActiveRecord::Base
include Sortable
default_scope { where.not(author_id: nil) } default_scope { where.not(author_id: nil) }
CREATED = 1 CREATED = 1
...@@ -48,29 +49,6 @@ class Event < ActiveRecord::Base ...@@ -48,29 +49,6 @@ class Event < ActiveRecord::Base
scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
class << self class << self
def create_ref_event(project, user, ref, action = 'add', prefix = 'refs/heads')
commit = project.repository.commit(ref.target)
if action.to_s == 'add'
before = '00000000'
after = commit.id
else
before = commit.id
after = '00000000'
end
Event.create(
project: project,
action: Event::PUSHED,
data: {
ref: "#{prefix}/#{ref.name}",
before: before,
after: after
},
author_id: user.id
)
end
def reset_event_cache_for(target) def reset_event_cache_for(target)
Event.where(target_id: target.id, target_type: target.class.to_s). Event.where(target_id: target.id, target_type: target.class.to_s).
order('id DESC').limit(100). order('id DESC').limit(100).
...@@ -83,6 +61,8 @@ class Event < ActiveRecord::Base ...@@ -83,6 +61,8 @@ class Event < ActiveRecord::Base
true true
elsif membership_changed? elsif membership_changed?
true true
elsif created_project?
true
else else
(issue? || merge_request? || note? || milestone?) && target (issue? || merge_request? || note? || milestone?) && target
end end
...@@ -97,25 +77,51 @@ class Event < ActiveRecord::Base ...@@ -97,25 +77,51 @@ class Event < ActiveRecord::Base
end end
def target_title def target_title
if target && target.respond_to?(:title) target.title if target && target.respond_to?(:title)
target.title end
end
def created?
action == CREATED
end end
def push? def push?
action == self.class::PUSHED && valid_push? action == PUSHED && valid_push?
end end
def merged? def merged?
action == self.class::MERGED action == MERGED
end end
def closed? def closed?
action == self.class::CLOSED action == CLOSED
end end
def reopened? def reopened?
action == self.class::REOPENED action == REOPENED
end
def joined?
action == JOINED
end
def left?
action == LEFT
end
def commented?
action == COMMENTED
end
def membership_changed?
joined? || left?
end
def created_project?
created? && !target
end
def created_target?
created? && target
end end
def milestone? def milestone?
...@@ -134,32 +140,32 @@ class Event < ActiveRecord::Base ...@@ -134,32 +140,32 @@ class Event < ActiveRecord::Base
target_type == "MergeRequest" target_type == "MergeRequest"
end end
def joined? def milestone
action == JOINED target if milestone?
end
def left?
action == LEFT
end
def membership_changed?
joined? || left?
end end
def issue def issue
target if target_type == "Issue" target if issue?
end end
def merge_request def merge_request
target if target_type == "MergeRequest" target if merge_request?
end end
def note def note
target if target_type == "Note" target if note?
end end
def action_name def action_name
if closed? if push?
if new_ref?
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
elsif closed?
"closed" "closed"
elsif merged? elsif merged?
"accepted" "accepted"
...@@ -167,6 +173,10 @@ class Event < ActiveRecord::Base ...@@ -167,6 +173,10 @@ class Event < ActiveRecord::Base
'joined' 'joined'
elsif left? elsif left?
'left' 'left'
elsif commented?
"commented on"
elsif created_project?
"created"
else else
"opened" "opened"
end end
...@@ -235,16 +245,6 @@ class Event < ActiveRecord::Base ...@@ -235,16 +245,6 @@ class Event < ActiveRecord::Base
tag? ? "tag" : "branch" tag? ? "tag" : "branch"
end end
def push_action_name
if new_ref?
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
end
def push_with_commits? def push_with_commits?
md_ref? && commits.any? && commit_from && commit_to md_ref? && commits.any? && commit_from && commit_to
end end
......
...@@ -31,6 +31,16 @@ class Group < Namespace ...@@ -31,6 +31,16 @@ class Group < Namespace
after_create :post_create_hook after_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
class << self
def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
end
def sort(method)
order_by(method)
end
end
def human_name def human_name
name name
end end
...@@ -108,20 +118,4 @@ class Group < Namespace ...@@ -108,20 +118,4 @@ class Group < Namespace
def system_hook_service def system_hook_service
SystemHooksService.new SystemHooksService.new
end end
class << self
def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
end
def sort(method)
case method.to_s
when "newest" then reorder("namespaces.created_at DESC")
when "oldest" then reorder("namespaces.created_at ASC")
when "recently_updated" then reorder("namespaces.updated_at DESC")
when "last_updated" then reorder("namespaces.updated_at ASC")
else reorder("namespaces.path, namespaces.name ASC")
end
end
end
end end
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# #
class WebHook < ActiveRecord::Base class WebHook < ActiveRecord::Base
include Sortable
include HTTParty include HTTParty
default_value_for :push_events, true default_value_for :push_events, true
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
# #
class Identity < ActiveRecord::Base class Identity < ActiveRecord::Base
include Sortable
belongs_to :user belongs_to :user
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
......
...@@ -24,6 +24,7 @@ class Issue < ActiveRecord::Base ...@@ -24,6 +24,7 @@ class Issue < ActiveRecord::Base
include Issuable include Issuable
include InternalId include InternalId
include Taskable include Taskable
include Sortable
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
require 'digest/md5' require 'digest/md5'
class Key < ActiveRecord::Base class Key < ActiveRecord::Base
include Sortable
include Gitlab::Popen include Gitlab::Popen
belongs_to :user belongs_to :user
......
...@@ -28,7 +28,7 @@ class Label < ActiveRecord::Base ...@@ -28,7 +28,7 @@ class Label < ActiveRecord::Base
format: { with: /\A[^&\?,&]+\z/ }, format: { with: /\A[^&\?,&]+\z/ },
uniqueness: { scope: :project_id } uniqueness: { scope: :project_id }
scope :order_by_name, -> { reorder("labels.title ASC") } default_scope { order(title: :asc) }
alias_attribute :name, :title alias_attribute :name, :title
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# #
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
include Sortable
include Notifiable include Notifiable
include Gitlab::Access include Gitlab::Access
......
...@@ -114,13 +114,11 @@ class ProjectMember < Member ...@@ -114,13 +114,11 @@ class ProjectMember < Member
end end
def post_create_hook def post_create_hook
Event.create( unless owner?
project_id: self.project.id, event_service.join_project(self.project, self.user)
action: Event::JOINED, notification_service.new_team_member(self)
author_id: self.user.id end
)
notification_service.new_team_member(self) unless owner?
system_hook_service.execute_hooks_for(self, :create) system_hook_service.execute_hooks_for(self, :create)
end end
...@@ -129,15 +127,14 @@ class ProjectMember < Member ...@@ -129,15 +127,14 @@ class ProjectMember < Member
end end
def post_destroy_hook def post_destroy_hook
Event.create( event_service.leave_project(self.project, self.user)
project_id: self.project.id,
action: Event::LEFT,
author_id: self.user.id
)
system_hook_service.execute_hooks_for(self, :destroy) system_hook_service.execute_hooks_for(self, :destroy)
end end
def event_service
EventCreateService.new
end
def notification_service def notification_service
NotificationService.new NotificationService.new
end end
......
...@@ -28,6 +28,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -28,6 +28,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable include Issuable
include Taskable include Taskable
include InternalId include InternalId
include Sortable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
require Rails.root.join("app/models/commit") require Rails.root.join("app/models/commit")
class MergeRequestDiff < ActiveRecord::Base class MergeRequestDiff < ActiveRecord::Base
include Sortable
# Prevent store of diff # Prevent store of diff
# if commits amount more then 200 # if commits amount more then 200
COMMITS_SAFE_SIZE = 200 COMMITS_SAFE_SIZE = 200
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
class Milestone < ActiveRecord::Base class Milestone < ActiveRecord::Base
include InternalId include InternalId
include Sortable
belongs_to :project belongs_to :project
has_many :issues has_many :issues
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# #
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
...@@ -43,6 +44,10 @@ class Namespace < ActiveRecord::Base ...@@ -43,6 +44,10 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') } scope :root, -> { where('type IS NULL') }
def self.by_path(path)
where('lower(path) = :value', value: path.downcase).first
end
def self.search(query) def self.search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%") where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
end end
......
...@@ -49,7 +49,7 @@ class Note < ActiveRecord::Base ...@@ -49,7 +49,7 @@ class Note < ActiveRecord::Base
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :common, ->{ where(noteable_type: ["", nil]) } scope :common, ->{ where(noteable_type: ["", nil]) }
scope :fresh, ->{ order("created_at ASC, id ASC") } scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) } scope :inc_author, ->{ includes(:author) }
...@@ -126,6 +126,36 @@ class Note < ActiveRecord::Base ...@@ -126,6 +126,36 @@ class Note < ActiveRecord::Base
}) })
end end
def create_labels_change_note(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ')
removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ')
message = ''
if added_labels.present?
message << "added #{added_labels}"
end
if added_labels.present? && removed_labels.present?
message << ' and '
end
if removed_labels.present?
message << "removed #{removed_labels}"
end
message << ' ' << 'label'.pluralize(labels_count)
body = "_#{message.capitalize}_"
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
def create_new_commits_note(noteable, project, author, commits) def create_new_commits_note(noteable, project, author, commits)
commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit') commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit')
body = "Added #{commits_text}:\n\n" body = "Added #{commits_text}:\n\n"
......
...@@ -33,6 +33,7 @@ require 'carrierwave/orm/activerecord' ...@@ -33,6 +33,7 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class Project < ActiveRecord::Base class Project < ActiveRecord::Base
include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
...@@ -53,7 +54,7 @@ class Project < ActiveRecord::Base ...@@ -53,7 +54,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch attr_accessor :new_default_branch
# Relations # Relations
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
...@@ -69,6 +70,7 @@ class Project < ActiveRecord::Base ...@@ -69,6 +70,7 @@ class Project < ActiveRecord::Base
has_one :hipchat_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy
has_one :flowdock_service, dependent: :destroy has_one :flowdock_service, dependent: :destroy
has_one :assembla_service, dependent: :destroy has_one :assembla_service, dependent: :destroy
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy
has_one :slack_service, dependent: :destroy has_one :slack_service, dependent: :destroy
has_one :jira_service, dependent: :destroy has_one :jira_service, dependent: :destroy
...@@ -90,7 +92,7 @@ class Project < ActiveRecord::Base ...@@ -90,7 +92,7 @@ class Project < ActiveRecord::Base
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed # Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, -> { order 'issues.state DESC, issues.created_at DESC' }, dependent: :destroy has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy has_many :labels, dependent: :destroy
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
has_many :events, dependent: :destroy has_many :events, dependent: :destroy
...@@ -146,14 +148,16 @@ class Project < ActiveRecord::Base ...@@ -146,14 +148,16 @@ class Project < ActiveRecord::Base
mount_uploader :avatar, AttachmentUploader mount_uploader :avatar, AttachmentUploader
# Scopes # Scopes
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) }
scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped }
scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) }
scope :in_team, ->(team) { where('projects.id IN (:ids)', ids: team.projects.map(&:id)) } scope :in_team, ->(team) { where('projects.id IN (:ids)', ids: team.projects.map(&:id)) }
scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) }
scope :in_group_namespace, -> { joins(:group) } scope :in_group_namespace, -> { joins(:group) }
scope :sorted_by_activity, -> { reorder('projects.last_activity_at DESC') }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
...@@ -235,13 +239,10 @@ class Project < ActiveRecord::Base ...@@ -235,13 +239,10 @@ class Project < ActiveRecord::Base
end end
def sort(method) def sort(method)
case method.to_s if method == 'repository_size_desc'
when 'newest' then reorder('projects.created_at DESC') reorder(repository_size: :desc, id: :desc)
when 'oldest' then reorder('projects.created_at ASC') else
when 'recently_updated' then reorder('projects.updated_at DESC') order_by(method)
when 'last_updated' then reorder('projects.updated_at ASC')
when 'largest_repository' then reorder('projects.repository_size DESC')
else reorder('namespaces.path, projects.name ASC')
end end
end end
end end
...@@ -327,7 +328,7 @@ class Project < ActiveRecord::Base ...@@ -327,7 +328,7 @@ class Project < ActiveRecord::Base
end end
def default_issue_tracker def default_issue_tracker
gitlab_issue_tracker_service ||= create_gitlab_issue_tracker_service gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
end end
def issues_tracker def issues_tracker
...@@ -359,19 +360,28 @@ class Project < ActiveRecord::Base ...@@ -359,19 +360,28 @@ class Project < ActiveRecord::Base
end end
def build_missing_services def build_missing_services
available_services_names.each do |service_name| services_templates = Service.where(template: true)
service = services.find { |service| service.to_param == service_name }
Service.available_services_names.each do |service_name|
service = find_service(services, service_name)
# If service is available but missing in db # If service is available but missing in db
# we should create an instance. Ex `create_gitlab_ci_service` if service.nil?
service = self.send :"create_#{service_name}_service" if service.nil? # We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service`
service = self.send :"create_#{service_name}_service"
else
Service.create_from_template(self.id, template)
end
end
end end
end end
def available_services_names def find_service(list, name)
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla list.find { |service| service.to_param == name }
emails_on_push gemnasium slack pushover buildbox bamboo teamcity jenkins jira redmine custom_issue_tracker
)
end end
def gitlab_ci? def gitlab_ci?
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
#
require 'asana'
class AsanaService < Service
prop_accessor :api_key, :restrict_to_branch
validates :api_key, presence: true, if: :activated?
def title
'Asana'
end
def description
'Asana - Teamwork without email'
end
def help
'This service adds commit messages as comments to Asana tasks.
Once enabled, commit messages are checked for Asana task URLs
(for example, `https://app.asana.com/0/123456/987654`) or task IDs
starting with # (for example, `#987654`). Every task ID found will
get the commit comment added to it.
You can also close a task with a message containing: `fix #123456`.
You can find your Api Keys here:
http://developer.asana.com/documentation/#api_keys'
end
def to_param
'asana'
end
def fields
[
{
type: 'text',
name: 'api_key',
placeholder: 'User API token. User must have access to task,
all comments will be attributed to this user.'
},
{
type: 'text',
name: 'restrict_to_branch',
placeholder: 'Comma-separated list of branches which will be
automatically inspected. Leave blank to include all branches.'
}
]
end
def execute(push)
Asana.configure do |client|
client.api_key = api_key
end
user = push[:user_name]
branch = push[:ref].gsub('refs/heads/', '')
branch_restriction = restrict_to_branch.to_s
# check the branch restriction is poplulated and branch is not included
if branch_restriction.length > 0 && branch_restriction.index(branch) == nil
return
end
project_name = project.name_with_namespace
push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
push[:commits].each do |commit|
check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
task_list = []
close_list = []
message.split("\n").each do |line|
# look for a task ID or a full Asana url
task_list.concat(line.scan(/#(\d+)/))
task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/))
# look for a word starting with 'fix' followed by a task ID
close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i))
end
# post commit to every taskid found
task_list.each do |taskid|
task = Asana::Task.find(taskid[0])
if task
task.create_story(text: push_msg + ' ' + message)
end
end
# close all tasks that had 'fix(ed/es/ing) #:id' in them
close_list.each do |taskid|
task = Asana::Task.find(taskid.last)
if task
task.modify(completed: true)
end
end
end
end
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class AssemblaService < Service class AssemblaService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class BambooService < CiService class BambooService < CiService
......
...@@ -5,13 +5,13 @@ ...@@ -5,13 +5,13 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
require "addressable/uri" require "addressable/uri"
class BuildboxService < CiService class BuildboxService < CiService
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class CampfireService < Service class CampfireService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
# Base class for CI services # Base class for CI services
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class CustomIssueTrackerService < IssueTrackerService class CustomIssueTrackerService < IssueTrackerService
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class EmailsOnPushService < Service class EmailsOnPushService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
require "flowdock-git-hook" require "flowdock-git-hook"
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
require "gemnasium/gitlab_service" require "gemnasium/gitlab_service"
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class GitlabCiService < CiService class GitlabCiService < CiService
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class GitlabIssueTrackerService < IssueTrackerService class GitlabIssueTrackerService < IssueTrackerService
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class HipchatService < Service class HipchatService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class IssueTrackerService < Service class IssueTrackerService < Service
...@@ -68,6 +69,29 @@ class IssueTrackerService < Service ...@@ -68,6 +69,29 @@ class IssueTrackerService < Service
end end
end end
def execute(data)
message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again."
result = false
begin
url = URI.parse(self.project_url)
if url.host && url.port
http = Net::HTTP.start(url.host, url.port, { open_timeout: 5, read_timeout: 5 })
response = http.head("/")
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
result = true
end
end
rescue Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error
message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
end
Rails.logger.info(message)
result
end
private private
def enabled_in_gitlab_config def enabled_in_gitlab_config
...@@ -81,12 +105,14 @@ class IssueTrackerService < Service ...@@ -81,12 +105,14 @@ class IssueTrackerService < Service
end end
def set_project_url def set_project_url
id = self.project.issues_tracker_id if self.project
id = self.project.issues_tracker_id
if id if id
issues_tracker['project_url'].gsub(":issues_tracker_id", id) issues_tracker['project_url'].gsub(":issues_tracker_id", id)
else end
issues_tracker['project_url']
end end
issues_tracker['project_url']
end end
end end
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
...@@ -21,6 +22,19 @@ class JiraService < IssueTrackerService ...@@ -21,6 +22,19 @@ class JiraService < IssueTrackerService
before_validation :set_api_version, :set_jira_issue_transition_id before_validation :set_api_version, :set_jira_issue_transition_id
def help
issue_tracker_link = help_page_path("integration", "external-issue-tracker")
line1 = "Setting `project_url`, `issues_url` and `new_issue_url` will "\
"allow a user to easily navigate to the Jira issue tracker. "\
"See the [integration doc](#{issue_tracker_link}) for details."
line2 = 'Support for referencing commits and automatic closing of Jira issues directly ' \
'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)'
[line1, line2].join("\n\n")
end
def title def title
if self.properties && self.properties['title'].present? if self.properties && self.properties['title'].present?
self.properties['title'] self.properties['title']
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class PivotaltrackerService < Service class PivotaltrackerService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class PushoverService < Service class PushoverService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class RedmineService < IssueTrackerService class RedmineService < IssueTrackerService
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class SlackService < Service class SlackService < Service
......
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE)
# #
class TeamcityService < CiService class TeamcityService < CiService
......
...@@ -5,16 +5,17 @@ ...@@ -5,16 +5,17 @@
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# # template :boolean default(FALSE)
# To add new service you should build a class inherited from Service # To add new service you should build a class inherited from Service
# and implement a set of methods # and implement a set of methods
class Service < ActiveRecord::Base class Service < ActiveRecord::Base
include Sortable
serialize :properties, JSON serialize :properties, JSON
default_value_for :active, false default_value_for :active, false
...@@ -24,7 +25,7 @@ class Service < ActiveRecord::Base ...@@ -24,7 +25,7 @@ class Service < ActiveRecord::Base
belongs_to :project belongs_to :project
has_one :service_hook has_one :service_hook
validates :project_id, presence: true validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
...@@ -32,6 +33,10 @@ class Service < ActiveRecord::Base ...@@ -32,6 +33,10 @@ class Service < ActiveRecord::Base
active active
end end
def template?
template
end
def category def category
:common :common
end end
...@@ -92,4 +97,16 @@ class Service < ActiveRecord::Base ...@@ -92,4 +97,16 @@ class Service < ActiveRecord::Base
def issue_tracker? def issue_tracker?
self.category == :issue_tracker self.category == :issue_tracker
end end
def self.available_services_names
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana
emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine custom_issue_tracker)
end
def self.create_from_template(project_id, template)
service = template.dup
service.template = false
service.project_id = project_id
service if service.save
end
end end
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# #
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Sortable
include Linguist::BlobHelper include Linguist::BlobHelper
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
......
...@@ -40,15 +40,18 @@ ...@@ -40,15 +40,18 @@
# confirmation_sent_at :datetime # confirmation_sent_at :datetime
# unconfirmed_email :string(255) # unconfirmed_email :string(255)
# hide_no_ssh_key :boolean default(FALSE) # hide_no_ssh_key :boolean default(FALSE)
# hide_no_password :boolean default(FALSE)
# website_url :string(255) default(""), not null # website_url :string(255) default(""), not null
# last_credential_check_at :datetime # last_credential_check_at :datetime
# github_access_token :string(255) # github_access_token :string(255)
# notification_email :string(255)
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class User < ActiveRecord::Base class User < ActiveRecord::Base
include Sortable
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
include TokenAuthenticatable include TokenAuthenticatable
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
...@@ -58,6 +61,7 @@ class User < ActiveRecord::Base ...@@ -58,6 +61,7 @@ class User < ActiveRecord::Base
default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
default_value_for :projects_limit, current_application_settings.default_projects_limit default_value_for :projects_limit, current_application_settings.default_projects_limit
default_value_for :theme_id, gitlab_config.default_theme default_value_for :theme_id, gitlab_config.default_theme
...@@ -114,6 +118,7 @@ class User < ActiveRecord::Base ...@@ -114,6 +118,7 @@ class User < ActiveRecord::Base
# #
validates :name, presence: true validates :name, presence: true
validates :email, presence: true, email: { strict_mode: true }, uniqueness: true validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
validates :notification_email, presence: true, email: { strict_mode: true }
validates :bio, length: { maximum: 255 }, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username, validates :username,
...@@ -127,10 +132,12 @@ class User < ActiveRecord::Base ...@@ -127,10 +132,12 @@ class User < ActiveRecord::Base
validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? } validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token before_save :ensure_authentication_token
after_save :ensure_namespace_correct after_save :ensure_namespace_correct
...@@ -176,7 +183,6 @@ class User < ActiveRecord::Base ...@@ -176,7 +183,6 @@ class User < ActiveRecord::Base
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_state(:blocked) } scope :blocked, -> { with_state(:blocked) }
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active) }
scope :alphabetically, -> { order('name ASC') }
scope :in_team, ->(team){ where(id: team.member_ids) } scope :in_team, ->(team){ where(id: team.member_ids) }
scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
...@@ -201,11 +207,10 @@ class User < ActiveRecord::Base ...@@ -201,11 +207,10 @@ class User < ActiveRecord::Base
def sort(method) def sort(method)
case method.to_s case method.to_s
when 'recent_sign_in' then reorder('users.last_sign_in_at DESC') when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
when 'oldest_sign_in' then reorder('users.last_sign_in_at ASC') when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
when 'recently_created' then reorder('users.created_at DESC') else
when 'late_created' then reorder('users.created_at ASC') order_by(method)
else reorder("users.name ASC")
end end
end end
...@@ -251,6 +256,22 @@ class User < ActiveRecord::Base ...@@ -251,6 +256,22 @@ class User < ActiveRecord::Base
joins('LEFT JOIN identities ON identities.user_id = users.id'). joins('LEFT JOIN identities ON identities.user_id = users.id').
where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%') where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%')
end end
def clean_username(username)
username.gsub!(/@.*\z/, "")
username.gsub!(/\.git\z/, "")
username.gsub!(/\A-/, "")
username.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
counter = 0
base = username
while User.by_login(username).present? || Namespace.by_path(username).present?
counter += 1
username = "#{base}#{counter}"
end
username
end
end end
# #
...@@ -282,7 +303,8 @@ class User < ActiveRecord::Base ...@@ -282,7 +303,8 @@ class User < ActiveRecord::Base
def namespace_uniq def namespace_uniq
namespace_name = self.username namespace_name = self.username
if Namespace.find_by(path: namespace_name) existing_namespace = Namespace.by_path(namespace_name)
if existing_namespace && existing_namespace != self.namespace
self.errors.add :username, "already exists" self.errors.add :username, "already exists"
end end
end end
...@@ -297,11 +319,15 @@ class User < ActiveRecord::Base ...@@ -297,11 +319,15 @@ class User < ActiveRecord::Base
self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email) self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email)
end end
def owns_notification_email
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end
# Groups user has access to # Groups user has access to
def authorized_groups def authorized_groups
@authorized_groups ||= begin @authorized_groups ||= begin
group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
Group.where(id: group_ids).order('namespaces.name ASC') Group.where(id: group_ids)
end end
end end
...@@ -313,7 +339,7 @@ class User < ActiveRecord::Base ...@@ -313,7 +339,7 @@ class User < ActiveRecord::Base
project_ids.push(*groups_projects.pluck(:id)) project_ids.push(*groups_projects.pluck(:id))
project_ids.push(*projects.pluck(:id).uniq) project_ids.push(*projects.pluck(:id).uniq)
project_ids.push(*groups.joins(:shared_projects).pluck(:project_id)) project_ids.push(*groups.joins(:shared_projects).pluck(:project_id))
Project.where(id: project_ids).joins(:namespace).order('namespaces.name ASC') Project.where(id: project_ids)
end end
end end
...@@ -443,6 +469,12 @@ class User < ActiveRecord::Base ...@@ -443,6 +469,12 @@ class User < ActiveRecord::Base
end end
end end
def set_notification_email
if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
self.notification_email = self.email
end
end
def requires_ldap_check? def requires_ldap_check?
if !Gitlab.config.ldap.enabled if !Gitlab.config.ldap.enabled
false false
...@@ -516,6 +548,10 @@ class User < ActiveRecord::Base ...@@ -516,6 +548,10 @@ class User < ActiveRecord::Base
end end
end end
def all_emails
[self.email, *self.emails.map(&:email)]
end
def hook_attrs def hook_attrs
{ {
name: name, name: name,
...@@ -535,7 +571,7 @@ class User < ActiveRecord::Base ...@@ -535,7 +571,7 @@ class User < ActiveRecord::Base
def post_create_hook def post_create_hook
log_info("User \"#{self.name}\" (#{self.email}) was created") log_info("User \"#{self.name}\" (#{self.email}) was created")
notification_service.new_user(self, @reset_token) notification_service.new_user(self, @reset_token) if self.created_by_id
system_hook_service.execute_hooks_for(self, :create) system_hook_service.execute_hooks_for(self, :create)
end end
......
...@@ -17,7 +17,7 @@ class CreateBranchService < BaseService ...@@ -17,7 +17,7 @@ class CreateBranchService < BaseService
new_branch = repository.find_branch(branch_name) new_branch = repository.find_branch(branch_name)
if new_branch if new_branch
Event.create_ref_event(project, current_user, new_branch, 'add') EventCreateService.new.push_ref(project, current_user, new_branch, 'add')
return success(new_branch) return success(new_branch)
else else
return error('Invalid reference name') return error('Invalid reference name')
......
...@@ -26,7 +26,7 @@ class CreateTagService < BaseService ...@@ -26,7 +26,7 @@ class CreateTagService < BaseService
project.gitlab_ci_service.async_execute(push_data) project.gitlab_ci_service.async_execute(push_data)
end end
Event.create_ref_event(project, current_user, new_tag, 'add', 'refs/tags') EventCreateService.new.push_ref(project, current_user, new_tag, 'add', 'refs/tags')
success(new_tag) success(new_tag)
else else
error('Invalid reference name') error('Invalid reference name')
......
...@@ -25,7 +25,7 @@ class DeleteBranchService < BaseService ...@@ -25,7 +25,7 @@ class DeleteBranchService < BaseService
end end
if repository.rm_branch(branch_name) if repository.rm_branch(branch_name)
Event.create_ref_event(project, current_user, branch, 'rm') EventCreateService.new.push_ref(project, current_user, branch, 'rm')
success('Branch was removed') success('Branch was removed')
else else
return error('Failed to remove branch') return error('Failed to remove branch')
......
...@@ -7,58 +7,98 @@ ...@@ -7,58 +7,98 @@
# #
class EventCreateService class EventCreateService
def open_issue(issue, current_user) def open_issue(issue, current_user)
create_event(issue, current_user, Event::CREATED) create_record_event(issue, current_user, Event::CREATED)
end end
def close_issue(issue, current_user) def close_issue(issue, current_user)
create_event(issue, current_user, Event::CLOSED) create_record_event(issue, current_user, Event::CLOSED)
end end
def reopen_issue(issue, current_user) def reopen_issue(issue, current_user)
create_event(issue, current_user, Event::REOPENED) create_record_event(issue, current_user, Event::REOPENED)
end end
def open_mr(merge_request, current_user) def open_mr(merge_request, current_user)
create_event(merge_request, current_user, Event::CREATED) create_record_event(merge_request, current_user, Event::CREATED)
end end
def close_mr(merge_request, current_user) def close_mr(merge_request, current_user)
create_event(merge_request, current_user, Event::CLOSED) create_record_event(merge_request, current_user, Event::CLOSED)
end end
def reopen_mr(merge_request, current_user) def reopen_mr(merge_request, current_user)
create_event(merge_request, current_user, Event::REOPENED) create_record_event(merge_request, current_user, Event::REOPENED)
end end
def merge_mr(merge_request, current_user) def merge_mr(merge_request, current_user)
create_event(merge_request, current_user, Event::MERGED) create_record_event(merge_request, current_user, Event::MERGED)
end end
def open_milestone(milestone, current_user) def open_milestone(milestone, current_user)
create_event(milestone, current_user, Event::CREATED) create_record_event(milestone, current_user, Event::CREATED)
end end
def close_milestone(milestone, current_user) def close_milestone(milestone, current_user)
create_event(milestone, current_user, Event::CLOSED) create_record_event(milestone, current_user, Event::CLOSED)
end end
def reopen_milestone(milestone, current_user) def reopen_milestone(milestone, current_user)
create_event(milestone, current_user, Event::REOPENED) create_record_event(milestone, current_user, Event::REOPENED)
end end
def leave_note(note, current_user) def leave_note(note, current_user)
create_event(note, current_user, Event::COMMENTED) create_record_event(note, current_user, Event::COMMENTED)
end
def join_project(project, current_user)
create_event(project, current_user, Event::JOINED)
end
def leave_project(project, current_user)
create_event(project, current_user, Event::LEFT)
end
def create_project(project, current_user)
create_event(project, current_user, Event::CREATED)
end
def push_ref(project, current_user, ref, action = 'add', prefix = 'refs/heads')
commit = project.repository.commit(ref.target)
if action.to_s == 'add'
before = '00000000'
after = commit.id
else
before = commit.id
after = '00000000'
end
data = {
ref: "#{prefix}/#{ref.name}",
before: before,
after: after
}
push(project, current_user, data)
end
def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data)
end end
private private
def create_event(record, current_user, status) def create_record_event(record, current_user, status)
Event.create( create_event(record.project, current_user, status, target_id: record.id, target_type: record.class.name)
project: record.project, end
target_id: record.id,
target_type: record.class.name, def create_event(project, current_user, status, attributes = {})
attributes.reverse_merge!(
project: project,
action: status, action: status,
author_id: current_user.id author_id: current_user.id
) )
Event.create(attributes)
end end
end end
...@@ -52,8 +52,7 @@ class GitPushService ...@@ -52,8 +52,7 @@ class GitPushService
end end
@push_data = post_receive_data(oldrev, newrev, ref) @push_data = post_receive_data(oldrev, newrev, ref)
create_push_event(@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) project.execute_services(@push_data.dup)
end end
...@@ -61,15 +60,6 @@ class GitPushService ...@@ -61,15 +60,6 @@ class GitPushService
protected protected
def create_push_event(push_data)
Event.create!(
project: project,
action: Event::PUSHED,
data: push_data,
author_id: push_data[:user_id]
)
end
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
def process_commit_messages(ref) def process_commit_messages(ref)
......
...@@ -5,7 +5,7 @@ class GitTagPushService ...@@ -5,7 +5,7 @@ class GitTagPushService
@project, @user = project, user @project, @user = project, user
@push_data = create_push_data(oldrev, newrev, ref) @push_data = create_push_data(oldrev, newrev, ref)
create_push_event EventCreateService.new.push(project, user, @push_data)
project.repository.expire_cache project.repository.expire_cache
project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks)
...@@ -22,13 +22,4 @@ class GitTagPushService ...@@ -22,13 +22,4 @@ class GitTagPushService
Gitlab::PushDataBuilder. Gitlab::PushDataBuilder.
build(project, user, oldrev, newrev, ref, []) build(project, user, oldrev, newrev, ref, [])
end end
def create_push_event
Event.create!(
project: project,
action: Event::PUSHED,
data: push_data,
author_id: push_data[:user_id]
)
end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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