Commit e1f77b9be071fac9f57e85b2f3853d2f333aeaab

Authored by Dmitriy Zaporozhets
2 parents d452ffc2 1dab19d0

Merge branch 'link-to-issue-tracker' of https://github.com/Undev/gitlabhq into U…

…ndev-link-to-issue-tracker

Conflicts:
	Gemfile.lock
Gemfile
... ... @@ -46,6 +46,9 @@ gem "grape-entity", "~> 0.2.0"
46 46 # based on human-friendly examples
47 47 gem "stamp"
48 48  
  49 +# Enumeration fields
  50 +gem 'enumerize'
  51 +
49 52 # Pagination
50 53 gem "kaminari", "~> 0.14.1"
51 54  
... ... @@ -113,6 +116,7 @@ group :assets do
113 116 gem 'bootstrap-sass', "2.2.1.1"
114 117 gem "font-awesome-sass-rails", "~> 3.0.0"
115 118 gem "gemoji", "~> 1.2.1", require: 'emoji/railtie'
  119 + gem "gon"
116 120 end
117 121  
118 122 group :development do
... ...
Gemfile.lock
... ... @@ -146,6 +146,8 @@ GEM
146 146 email_spec (1.4.0)
147 147 launchy (~> 2.1)
148 148 mail (~> 2.2)
  149 + enumerize (0.5.1)
  150 + activesupport (>= 3.2)
149 151 erubis (2.7.0)
150 152 escape_utils (0.2.4)
151 153 eventmachine (1.0.0)
... ... @@ -184,10 +186,13 @@ GEM
184 186 pyu-ruby-sasl (~> 0.0.3.1)
185 187 rubyntlm (~> 0.1.1)
186 188 gitlab_yaml_db (1.0.0)
  189 + gon (4.0.2)
187 190 grape (0.3.1)
  191 + actionpack (>= 2.3.0)
188 192 activesupport
189 193 grape-entity (~> 0.2.0)
190 194 hashie (~> 1.2)
  195 + json
191 196 multi_json (>= 1.3.2)
192 197 multi_xml
193 198 rack
... ... @@ -473,6 +478,7 @@ DEPENDENCIES
473 478 devise (~> 2.1.0)
474 479 draper (~> 0.18.0)
475 480 email_spec
  481 + enumerize
476 482 factory_girl_rails
477 483 ffaker
478 484 font-awesome-sass-rails (~> 3.0.0)
... ... @@ -484,6 +490,7 @@ DEPENDENCIES
484 490 gitlab_meta (= 5.0)
485 491 gitlab_omniauth-ldap (= 1.0.2)
486 492 gitlab_yaml_db (= 1.0.0)
  493 + gon
487 494 grack!
488 495 grape (~> 0.3.1)
489 496 grape-entity (~> 0.2.0)
... ...
app/assets/javascripts/projects.js.coffee
... ... @@ -18,3 +18,18 @@ $ ->
18 18 # Ref switcher
19 19 $('.project-refs-select').on 'change', ->
20 20 $(@).parents('form').submit()
  21 +
  22 + $('#project_issues_enabled').change ->
  23 + if ($(this).is(':checked') == true)
  24 + $('#project_issues_tracker').removeAttr('disabled')
  25 + else
  26 + $('#project_issues_tracker').attr('disabled', 'disabled')
  27 +
  28 + $('#project_issues_tracker').change()
  29 +
  30 + $('#project_issues_tracker').change ->
  31 + if ($(this).val() == gon.default_issues_tracker || $(this).is(':disabled'))
  32 + $('#project_issues_tracker_id').attr('disabled', 'disabled')
  33 + else
  34 + $('#project_issues_tracker_id').removeAttr('disabled')
  35 +
... ...
app/controllers/application_controller.rb
... ... @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
5 5 before_filter :add_abilities
6 6 before_filter :dev_tools if Rails.env == 'development'
7 7 before_filter :default_headers
  8 + before_filter :add_gon_variables
8 9  
9 10 protect_from_forgery
10 11  
... ... @@ -148,4 +149,8 @@ class ApplicationController < ActionController::Base
148 149 headers['X-Frame-Options'] = 'DENY'
149 150 headers['X-XSS-Protection'] = '1; mode=block'
150 151 end
  152 +
  153 + def add_gon_variables
  154 + gon.default_issues_tracker = Project.issues_tracker.default_value
  155 + end
151 156 end
... ...
app/helpers/issues_helper.rb
... ... @@ -40,4 +40,39 @@ module IssuesHelper
40 40 def issues_active_milestones
41 41 @project.milestones.active.order("id desc").all
42 42 end
  43 +
  44 + def url_for_project_issues
  45 + return "" if @project.nil?
  46 +
  47 + if @project.used_default_issues_tracker?
  48 + project_issues_filter_path(@project)
  49 + else
  50 + url = Settings[:issues_tracker][@project.issues_tracker]["project_url"]
  51 + url.gsub(':project_id', @project.id.to_s)
  52 + .gsub(':issues_tracker_id', @project.issues_tracker_id.to_s)
  53 + end
  54 + end
  55 +
  56 + def url_for_issue(issue_id)
  57 + return "" if @project.nil?
  58 +
  59 + if @project.used_default_issues_tracker?
  60 + url = project_issue_url project_id: @project, id: issue_id
  61 + else
  62 + url = Settings[:issues_tracker][@project.issues_tracker]["issues_url"]
  63 + url.gsub(':id', issue_id.to_s)
  64 + .gsub(':project_id', @project.id.to_s)
  65 + .gsub(':issues_tracker_id', @project.issues_tracker_id.to_s)
  66 + end
  67 + end
  68 +
  69 + def title_for_issue(issue_id)
  70 + return "" if @project.nil?
  71 +
  72 + if @project.used_default_issues_tracker? && issue = @project.issues.where(id: issue_id).first
  73 + issue.title
  74 + else
  75 + ""
  76 + end
  77 + end
43 78 end
... ...
app/models/project.rb
... ... @@ -11,6 +11,7 @@
11 11 # creator_id :integer
12 12 # default_branch :string(255)
13 13 # issues_enabled :boolean default(TRUE), not null
  14 +# issues_tracker :string not null
14 15 # wall_enabled :boolean default(TRUE), not null
15 16 # merge_requests_enabled :boolean default(TRUE), not null
16 17 # wiki_enabled :boolean default(TRUE), not null
... ... @@ -22,11 +23,12 @@ require "grit"
22 23  
23 24 class Project < ActiveRecord::Base
24 25 include Gitolited
  26 + extend Enumerize
25 27  
26 28 class TransferError < StandardError; end
27 29  
28   - attr_accessible :name, :path, :description, :default_branch,
29   - :issues_enabled, :wall_enabled, :merge_requests_enabled,
  30 + attr_accessible :name, :path, :description, :default_branch, :issues_tracker,
  31 + :issues_enabled, :wall_enabled, :merge_requests_enabled, :issues_tracker_id,
30 32 :wiki_enabled, :public, :import_url, as: [:default, :admin]
31 33  
32 34 attr_accessible :namespace_id, :creator_id, as: :admin
... ... @@ -72,6 +74,7 @@ class Project &lt; ActiveRecord::Base
72 74 message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
73 75 validates :issues_enabled, :wall_enabled, :merge_requests_enabled,
74 76 :wiki_enabled, inclusion: { in: [true, false] }
  77 + validates :issues_tracker_id, length: { within: 0..255 }
75 78  
76 79 validates_uniqueness_of :name, scope: :namespace_id
77 80 validates_uniqueness_of :path, scope: :namespace_id
... ... @@ -93,6 +96,8 @@ class Project &lt; ActiveRecord::Base
93 96 scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) }
94 97 scope :public_only, -> { where(public: true) }
95 98  
  99 + enumerize :issues_tracker, :in => (Gitlab.config.issues_tracker.keys).append(:gitlab), :default => :gitlab
  100 +
96 101 class << self
97 102 def abandoned
98 103 project_ids = Event.select('max(created_at) as latest_date, project_id').
... ... @@ -201,6 +206,22 @@ class Project &lt; ActiveRecord::Base
201 206 issues.tag_counts_on(:labels)
202 207 end
203 208  
  209 + def issue_exists?(issue_id)
  210 + if used_default_issues_tracker?
  211 + self.issues.where(id: issue_id).first.present?
  212 + else
  213 + true
  214 + end
  215 + end
  216 +
  217 + def used_default_issues_tracker?
  218 + self.issues_tracker == Project.issues_tracker.default_value
  219 + end
  220 +
  221 + def can_have_issues_tracker_id?
  222 + self.issues_enabled && !self.used_default_issues_tracker?
  223 + end
  224 +
204 225 def services
205 226 [gitlab_ci_service].compact
206 227 end
... ...
app/views/admin/projects/_form.html.haml
... ... @@ -31,6 +31,15 @@
31 31 = f.label :issues_enabled, "Issues"
32 32 .input= f.check_box :issues_enabled
33 33  
  34 + - if Project.issues_tracker.values.count > 1
  35 + .clearfix
  36 + = f.label :issues_tracker, "Issues tracker", class: 'control-label'
  37 + .input= f.select(:issues_tracker, Project.issues_tracker.values, {}, { disabled: !@project.issues_enabled })
  38 +
  39 + .clearfix
  40 + = f.label :issues_tracker_id, "Project name or id in issues tracker", class: 'control-label'
  41 + .input= f.text_field :issues_tracker_id, class: "xxlarge", disabled: !@project.can_have_issues_tracker_id?
  42 +
34 43 .clearfix
35 44 = f.label :merge_requests_enabled, "Merge Requests"
36 45 .input= f.check_box :merge_requests_enabled
... ...
app/views/layouts/_head.html.haml
... ... @@ -7,6 +7,7 @@
7 7 = stylesheet_link_tag "application"
8 8 = javascript_include_tag "application"
9 9 = csrf_meta_tags
  10 + = include_gon
10 11  
11 12 -# Atom feed
12 13 - if current_user
... ...
app/views/layouts/project_resource.html.haml
... ... @@ -22,11 +22,12 @@
22 22 = nav_link(controller: %w(graph)) do
23 23 = link_to "Network", project_graph_path(@project, @ref || @repository.root_ref)
24 24  
25   - - if @project.issues_enabled
  25 + - if @project.issues_enabled
26 26 = nav_link(controller: %w(issues milestones labels)) do
27   - = link_to project_issues_filter_path(@project) do
  27 + = link_to url_for_project_issues do
28 28 Issues
29   - %span.count.issue_counter= @project.issues.opened.count
  29 + - if @project.used_default_issues_tracker?
  30 + %span.count.issue_counter= @project.issues.opened.count
30 31  
31 32 - if @project.repo_exists? && @project.merge_requests_enabled
32 33 = nav_link(controller: :merge_requests) do
... ...
app/views/projects/_form.html.haml
... ... @@ -24,6 +24,15 @@
24 24 = f.check_box :issues_enabled
25 25 %span.descr Lightweight issue tracking system for this project
26 26  
  27 + - if Project.issues_tracker.values.count > 1
  28 + .control-group
  29 + = f.label :issues_tracker, "Issues tracker", class: 'control-label'
  30 + .input= f.select(:issues_tracker, Project.issues_tracker.values, {}, { disabled: !@project.issues_enabled })
  31 +
  32 + .clearfix
  33 + = f.label :issues_tracker_id, "Project name or id in issues tracker", class: 'control-label'
  34 + .input= f.text_field :issues_tracker_id, class: "xxlarge", disabled: !@project.can_have_issues_tracker_id?
  35 +
27 36 .control-group
28 37 = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label'
29 38 .controls
... ...
config/gitlab.yml.example
... ... @@ -37,6 +37,22 @@ production: &amp;base
37 37 # signup_enabled: true # default: false - Account passwords are not sent via the email if signup is enabled.
38 38 # username_changing_enabled: false # default: true - User can change her username/namespace
39 39  
  40 +
  41 + ## External issues trackers
  42 + issues_tracker:
  43 + redmine:
  44 + ## If not nil, link 'Issues' on project page will be replaced tp this
  45 + ## Use placeholders:
  46 + ## :project_id - Gitlab project identifier
  47 + ## :issues_tracker_id - Project Name or Id in external issue tracker
  48 + project_url: "http://redmine.sample/projects/:issues_tracker_id"
  49 + ## If not nil, links from /#\d/ entities from commit messages will replaced to this
  50 + ## Use placeholders:
  51 + ## :project_id - Gitlab project identifier
  52 + ## :issues_tracker_id - Project Name or Id in external issue tracker
  53 + ## :id - Issue id (from commit messages)
  54 + issues_url: "http://redmine.sample/issues/:id"
  55 +
40 56 ## Gravatar
41 57 gravatar:
42 58 enabled: true # Use user avatar images from Gravatar.com (default: true)
... ... @@ -133,6 +149,10 @@ development:
133 149  
134 150 test:
135 151 <<: *base
  152 + issues_tracker:
  153 + redmine:
  154 + project_url: "http://redmine/projects/:issues_tracker_id"
  155 + issues_url: "http://redmine/:project_id/:issues_tracker_id/:id"
136 156  
137 157 staging:
138 158 <<: *base
... ...
config/initializers/1_settings.rb
... ... @@ -42,6 +42,8 @@ Settings[&#39;omniauth&#39;] ||= Settingslogic.new({})
42 42 Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil?
43 43 Settings.omniauth['providers'] ||= []
44 44  
  45 +Settings['issues_tracker'] ||= {}
  46 +
45 47 #
46 48 # GitLab
47 49 #
... ...
db/migrate/20130123114545_add_issues_tracker_to_project.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddIssuesTrackerToProject < ActiveRecord::Migration
  2 + def change
  3 + add_column :projects, :issues_tracker, :string, default: :gitlab, null: false
  4 + end
  5 +end
... ...
db/migrate/20130211085435_add_issues_tracker_id_to_project.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddIssuesTrackerIdToProject < ActiveRecord::Migration
  2 + def change
  3 + add_column :projects, :issues_tracker_id, :string
  4 + end
  5 +end
... ...
db/schema.rb
... ... @@ -106,11 +106,11 @@ ActiveRecord::Schema.define(:version =&gt; 20130220133245) do
106 106 add_index "milestones", ["project_id"], :name => "index_milestones_on_project_id"
107 107  
108 108 create_table "namespaces", :force => true do |t|
109   - t.string "name", :null => false
110   - t.string "path", :null => false
111   - t.integer "owner_id", :null => false
112   - t.datetime "created_at", :null => false
113   - t.datetime "updated_at", :null => false
  109 + t.string "name", :null => false
  110 + t.string "path", :null => false
  111 + t.integer "owner_id", :null => false
  112 + t.datetime "created_at", :null => false
  113 + t.datetime "updated_at", :null => false
114 114 t.string "type"
115 115 end
116 116  
... ... @@ -152,6 +152,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130220133245) do
152 152 t.boolean "wiki_enabled", :default => true, :null => false
153 153 t.integer "namespace_id"
154 154 t.boolean "public", :default => false, :null => false
  155 + t.string "issues_tracker", :default => "gitlab", :null => false
  156 + t.string "issues_tracker_id"
155 157 end
156 158  
157 159 add_index "projects", ["creator_id"], :name => "index_projects_on_owner_id"
... ... @@ -230,8 +232,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130220133245) do
230 232 t.string "name"
231 233 t.string "path"
232 234 t.integer "owner_id"
233   - t.datetime "created_at", :null => false
234   - t.datetime "updated_at", :null => false
  235 + t.datetime "created_at", :null => false
  236 + t.datetime "updated_at", :null => false
235 237 end
236 238  
237 239 create_table "users", :force => true do |t|
... ...
lib/gitlab/markdown.rb
... ... @@ -25,6 +25,8 @@ module Gitlab
25 25 # >> gfm(":trollface:")
26 26 # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
27 27 module Markdown
  28 + include IssuesHelper
  29 +
28 30 attr_reader :html_options
29 31  
30 32 # Public: Parse the provided text with GitLab-Flavored Markdown
... ... @@ -163,8 +165,11 @@ module Gitlab
163 165 end
164 166  
165 167 def reference_issue(identifier)
166   - if issue = @project.issues.where(id: identifier).first
167   - link_to("##{identifier}", project_issue_url(@project, issue), html_options.merge(title: "Issue: #{issue.title}", class: "gfm gfm-issue #{html_options[:class]}"))
  168 + if @project.issue_exists? identifier
  169 + url = url_for_issue(identifier)
  170 + title = title_for_issue(identifier)
  171 +
  172 + link_to("##{identifier}", url, html_options.merge(title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}"))
168 173 end
169 174 end
170 175  
... ...
spec/factories.rb
... ... @@ -29,6 +29,11 @@ FactoryGirl.define do
29 29 creator
30 30 end
31 31  
  32 + factory :redmine_project, parent: :project do
  33 + issues_tracker { "redmine" }
  34 + issues_tracker_id { "project_name_in_redmine" }
  35 + end
  36 +
32 37 factory :group do
33 38 sequence(:name) { |n| "group#{n}" }
34 39 path { name.downcase.gsub(/\s/, '_') }
... ...
spec/helpers/gitlab_markdown_helper_spec.rb
... ... @@ -2,6 +2,7 @@ require &quot;spec_helper&quot;
2 2  
3 3 describe GitlabMarkdownHelper do
4 4 include ApplicationHelper
  5 + include IssuesHelper
5 6  
6 7 let!(:project) { create(:project) }
7 8  
... ...
spec/helpers/issues_helper_spec.rb 0 → 100644
... ... @@ -0,0 +1,79 @@
  1 +require "spec_helper"
  2 +
  3 +describe IssuesHelper do
  4 + let(:project) { create :project }
  5 + let(:issue) { create :issue, project: project }
  6 + let(:ext_project) { create :redmine_project }
  7 +
  8 + describe :title_for_issue do
  9 + it "should return issue title if used internal tracker" do
  10 + @project = project
  11 + title_for_issue(issue.id).should eq issue.title
  12 + end
  13 +
  14 + it "should always return empty string if used external tracker" do
  15 + @project = ext_project
  16 + title_for_issue(rand(100)).should eq ""
  17 + end
  18 +
  19 + it "should always return empty string if project nil" do
  20 + @project = nil
  21 +
  22 + title_for_issue(rand(100)).should eq ""
  23 + end
  24 + end
  25 +
  26 + describe :url_for_project_issues do
  27 + let(:project_url) { Gitlab.config.issues_tracker.redmine.project_url}
  28 + let(:ext_expected) do
  29 + project_url.gsub(':project_id', ext_project.id.to_s)
  30 + .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
  31 + end
  32 + let(:int_expected) { polymorphic_path([project]) }
  33 +
  34 + it "should return internal path if used internal tracker" do
  35 + @project = project
  36 + url_for_project_issues.should match(int_expected)
  37 + end
  38 +
  39 + it "should return path to external tracker" do
  40 + @project = ext_project
  41 +
  42 + url_for_project_issues.should match(ext_expected)
  43 + end
  44 +
  45 + it "should return empty string if project nil" do
  46 + @project = nil
  47 +
  48 + url_for_project_issues.should eq ""
  49 + end
  50 + end
  51 +
  52 + describe :url_for_issue do
  53 + let(:issue_id) { 3 }
  54 + let(:issues_url) { Gitlab.config.issues_tracker.redmine.issues_url}
  55 + let(:ext_expected) do
  56 + issues_url.gsub(':id', issue_id.to_s)
  57 + .gsub(':project_id', ext_project.id.to_s)
  58 + .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
  59 + end
  60 + let(:int_expected) { polymorphic_path([project, issue]) }
  61 +
  62 + it "should return internal path if used internal tracker" do
  63 + @project = project
  64 + url_for_issue(issue.id).should match(int_expected)
  65 + end
  66 +
  67 + it "should return path to external tracker" do
  68 + @project = ext_project
  69 +
  70 + url_for_issue(issue_id).should match(ext_expected)
  71 + end
  72 +
  73 + it "should return empty string if project nil" do
  74 + @project = nil
  75 +
  76 + url_for_issue(issue.id).should eq ""
  77 + end
  78 + end
  79 +end
... ...
spec/models/project_spec.rb
... ... @@ -60,6 +60,7 @@ describe Project do
60 60 it { should ensure_inclusion_of(:wall_enabled).in_array([true, false]) }
61 61 it { should ensure_inclusion_of(:merge_requests_enabled).in_array([true, false]) }
62 62 it { should ensure_inclusion_of(:wiki_enabled).in_array([true, false]) }
  63 + it { should ensure_length_of(:issues_tracker_id).is_within(0..255) }
63 64  
64 65 it "should not allow new projects beyond user limits" do
65 66 project.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 1))
... ... @@ -190,4 +191,57 @@ describe Project do
190 191 Project.new(path: "empty").repository.should be_nil
191 192 end
192 193 end
  194 +
  195 + describe :issue_exists? do
  196 + let(:project) { create(:project) }
  197 + let(:existed_issue) { create(:issue, project: project) }
  198 + let(:not_existed_issue) { create(:issue) }
  199 + let(:ext_project) { create(:redmine_project) }
  200 +
  201 + it "should be true or if used internal tracker and issue exists" do
  202 + project.issue_exists?(existed_issue.id).should be_true
  203 + end
  204 +
  205 + it "should be false or if used internal tracker and issue not exists" do
  206 + project.issue_exists?(not_existed_issue.id).should be_false
  207 + end
  208 +
  209 + it "should always be true if used other tracker" do
  210 + ext_project.issue_exists?(rand(100)).should be_true
  211 + end
  212 + end
  213 +
  214 + describe :used_default_issues_tracker? do
  215 + let(:project) { create(:project) }
  216 + let(:ext_project) { create(:redmine_project) }
  217 +
  218 + it "should be true if used internal tracker" do
  219 + project.used_default_issues_tracker?.should be_true
  220 + end
  221 +
  222 + it "should be false if used other tracker" do
  223 + ext_project.used_default_issues_tracker?.should be_false
  224 + end
  225 + end
  226 +
  227 + describe :can_have_issues_tracker_id? do
  228 + let(:project) { create(:project) }
  229 + let(:ext_project) { create(:redmine_project) }
  230 +
  231 + it "should be true for projects with external issues tracker if issues enabled" do
  232 + ext_project.can_have_issues_tracker_id?.should be_true
  233 + end
  234 +
  235 + it "should be false for projects with internal issue tracker if issues enabled" do
  236 + project.can_have_issues_tracker_id?.should be_false
  237 + end
  238 +
  239 + it "should be always false if issues disbled" do
  240 + project.issues_enabled = false
  241 + ext_project.issues_enabled = false
  242 +
  243 + project.can_have_issues_tracker_id?.should be_false
  244 + ext_project.can_have_issues_tracker_id?.should be_false
  245 + end
  246 + end
193 247 end
... ...