meta_project 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/CHANGES +178 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +64 -0
  4. data/Rakefile +166 -0
  5. data/TODO +9 -0
  6. data/doc/base_attrs.rdoc +2 -0
  7. data/lib/meta_project/project/base.rb +7 -0
  8. data/lib/meta_project/project/codehaus/codehaus_project_svn.rb +26 -0
  9. data/lib/meta_project/project/codehaus.rb +1 -0
  10. data/lib/meta_project/project/trac/trac_project.rb +26 -0
  11. data/lib/meta_project/project/trac.rb +1 -0
  12. data/lib/meta_project/project/xforge/ruby_forge.rb +46 -0
  13. data/lib/meta_project/project/xforge/session.rb +162 -0
  14. data/lib/meta_project/project/xforge/source_forge.rb +46 -0
  15. data/lib/meta_project/project/xforge/xfile.rb +45 -0
  16. data/lib/meta_project/project/xforge/xforge_base.rb +76 -0
  17. data/lib/meta_project/project/xforge.rb +5 -0
  18. data/lib/meta_project/project.rb +4 -0
  19. data/lib/meta_project/project_analyzer.rb +36 -0
  20. data/lib/meta_project/scm_web.rb +53 -0
  21. data/lib/meta_project/tracker/base.rb +18 -0
  22. data/lib/meta_project/tracker/digit_issues.rb +24 -0
  23. data/lib/meta_project/tracker/issue.rb +11 -0
  24. data/lib/meta_project/tracker/jira/jira_tracker.rb +68 -0
  25. data/lib/meta_project/tracker/jira.rb +1 -0
  26. data/lib/meta_project/tracker/trac/trac_tracker.rb +29 -0
  27. data/lib/meta_project/tracker/trac.rb +1 -0
  28. data/lib/meta_project/tracker/xforge/ruby_forge_tracker.rb +17 -0
  29. data/lib/meta_project/tracker/xforge/source_forge_tracker.rb +17 -0
  30. data/lib/meta_project/tracker/xforge/xforge_tracker.rb +83 -0
  31. data/lib/meta_project/tracker/xforge.rb +3 -0
  32. data/lib/meta_project/tracker.rb +6 -0
  33. data/lib/meta_project/version_parser.rb +48 -0
  34. data/lib/meta_project.rb +6 -0
  35. data/lib/rake/contrib/xforge/base.rb +42 -0
  36. data/lib/rake/contrib/xforge/news_publisher.rb +34 -0
  37. data/lib/rake/contrib/xforge/release.rb +78 -0
  38. data/lib/rake/contrib/xforge.rb +3 -0
  39. metadata +85 -0
@@ -0,0 +1,162 @@
1
+ require 'net/http'
2
+ require 'open-uri'
3
+
4
+ module MetaProject
5
+ module Project
6
+ module XForge
7
+
8
+ # A Session object allows authenticated interaction with a Project, such as releasing files.
9
+ #
10
+ # A Session object can be obtained via Project.login
11
+ #
12
+ class Session
13
+
14
+ # Simple enumeration of processors. Used from Session.release
15
+ class Processor
16
+ I386 = 1000
17
+ IA64 = 6000
18
+ ALPHA = 7000
19
+ ANY = 8000
20
+ PPC = 2000
21
+ MIPS = 3000
22
+ SPARC = 4000
23
+ ULTRA_SPARC = 5000
24
+ OTHER_PLATFORM = 9999
25
+ end
26
+
27
+ BOUNDARY = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
28
+
29
+ def initialize(host, project, cookie) # :nodoc:
30
+ @host = host
31
+ @project = project
32
+ @headers = { "Cookie" => cookie }
33
+ end
34
+
35
+ # The package_id of our project
36
+ def package_id
37
+ unless(@package_id)
38
+ release_uri = "http://#{@host}/frs/admin/?group_id=#{@project.group_id}"
39
+ release_data = open(release_uri, @headers) { |data| data.read }
40
+ @package_id = release_data[/[?&]package_id=(\d+)/, 1]
41
+ raise "Couldn't get package_id" unless @package_id
42
+ end
43
+ @package_id
44
+ end
45
+
46
+ # Creates a new release containing the files specified by +filenames+ (Array) and named +release_name+.
47
+ # Optional parameters are +processor+ (which should be one of the Processor constants), +release_notes+,
48
+ # +release_changes+ and +preformatted+ which will appear on the releas page of the associated project.
49
+ #
50
+ def release(release_name, filenames, release_notes="", release_changes="", preformatted=true, processor=Processor::ANY)
51
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
52
+ release_id = nil
53
+
54
+ puts "About to release '#{release_name}'"
55
+ puts "Files:"
56
+ puts " " + filenames.join("\n ")
57
+ puts "\nRelease Notes:\n"
58
+ puts release_notes
59
+ puts "\nRelease Changes:\n"
60
+ puts release_changes
61
+ puts "\nRelease Settings:\n"
62
+ puts "Preformatted: #{preformatted}"
63
+ puts "Processor: #{processor}"
64
+ puts "\nStarting release..."
65
+
66
+ xfiles = filenames.collect{|filename| XFile.new(filename)}
67
+ xfiles.each_with_index do |xfile, i|
68
+ first_file = i==0
69
+ puts "Releasing #{xfile.basename}..."
70
+
71
+ release_response = Net::HTTP.start(@host, 80) do |http|
72
+ query_hash = if first_file then
73
+ {
74
+ "group_id" => @project.group_id,
75
+ "package_id" => package_id,
76
+ "type_id" => xfile.bin_type_id,
77
+ "processor_id" => processor,
78
+
79
+ "release_name" => release_name,
80
+ "release_date" => release_date,
81
+ "release_notes" => release_notes,
82
+ "release_changes" => release_changes,
83
+ "preformatted" => preformatted ? "1" : "0",
84
+ "submit" => "1"
85
+ }
86
+ else
87
+ {
88
+ "group_id" => @project.group_id,
89
+ "package_id" => package_id,
90
+ "type_id" => xfile.bin_type_id,
91
+ "processor_id" => processor,
92
+
93
+ "step2" => "1",
94
+ "release_id" => release_id,
95
+ "submit" => "Add This File"
96
+ }
97
+ end
98
+
99
+ query = query(query_hash)
100
+
101
+ data = [
102
+ "--" + BOUNDARY,
103
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{xfile.basename}\"",
104
+ "Content-Type: application/octet-stream",
105
+ "Content-Transfer-Encoding: binary",
106
+ "", xfile.data, ""
107
+ ].join("\x0D\x0A")
108
+
109
+ headers = @headers.merge(
110
+ "Content-Type" => "multipart/form-data; boundary=#{BOUNDARY}"
111
+ )
112
+
113
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
114
+ http.post(target + query, data, headers)
115
+ end
116
+
117
+ if first_file then
118
+ release_id = release_response.body[/release_id=(\d+)/, 1]
119
+ raise("Couldn't get release id") unless release_id
120
+ end
121
+ end
122
+ puts "Done!"
123
+ end
124
+
125
+ def publish_news(subject, details)
126
+ puts "About to publish news"
127
+ puts "Subject: '#{subject}'"
128
+ puts "Details:"
129
+ puts details
130
+ puts ""
131
+
132
+ release_response = Net::HTTP.start(@host, 80) do |http|
133
+ query_hash = {
134
+ "group_id" => @project.group_id,
135
+ "package_id" => package_id,
136
+ "post_changes" => "y",
137
+ "summary" => subject,
138
+ "details" => details
139
+ }
140
+
141
+ target = "/news/submit.php"
142
+ headers = @headers.merge(
143
+ "Content-Type" => "multipart/form-data"
144
+ )
145
+ http.post(target + query(query_hash), "", headers)
146
+
147
+ end
148
+ puts "Done!"
149
+ end
150
+
151
+ private
152
+
153
+ def query(query_hash)
154
+ "?" + query_hash.map do |(name, value)|
155
+ [name, URI.encode(value.to_s)].join("=")
156
+ end.join("&")
157
+ end
158
+
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,46 @@
1
+ require 'meta_project/tracker/xforge'
2
+
3
+ module MetaProject
4
+ module Project
5
+ module XForge
6
+ class SourceForge < XForgeBase
7
+
8
+ def initialize(unix_name, cvs_mod=nil)
9
+ super("sourceforge.net", unix_name, cvs_mod)
10
+ end
11
+
12
+ def tracker_class
13
+ ::MetaProject::Tracker::XForge::SourceForgeTracker
14
+ end
15
+
16
+ protected
17
+
18
+ def create_cvs(unix_name, mod)
19
+ RSCM::Cvs.new(":pserver:anonymous@cvs.sourceforge.net:/cvsroot/#{unix_name}", mod)
20
+ end
21
+
22
+ def create_view_cvs(unix_name, mod)
23
+ view_cvs = "http://cvs.sourceforge.net/viewcvs.py/"
24
+ unix_name_mod = "#{unix_name}/#{mod}"
25
+ project_path = "#{unix_name_mod}/\#{path}"
26
+ rev = "rev=\#{revision}"
27
+
28
+ overview = "#{view_cvs}#{unix_name_mod}/"
29
+ history = "#{view_cvs}#{project_path}"
30
+ raw = "#{view_cvs}*checkout*/#{project_path}?#{rev}"
31
+ html = "#{history}?#{rev}&view=markup"
32
+ diff = "#{history}?r1=\#{previous_revision}&r2=\#{revision}"
33
+
34
+ ScmWeb.new(overview, history, raw, html, diff)
35
+ end
36
+
37
+ # Regexp used to find projects' home page
38
+ def home_page_regexp
39
+ # This seems a little volatile
40
+ /<A href=\"(\w*:\/\/[^\"]*)\">&nbsp;Project Home Page<\/A>/
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ module MetaProject
2
+ module Project
3
+ module XForge
4
+
5
+ class XFile # :nodoc:
6
+
7
+ # extension => [mime_type, rubyforge_bin_type_id, rubyforge_src_type_id]
8
+ FILE_TYPES = {
9
+ ".deb" => ["application/octet-stream", 1000],
10
+
11
+ # all of these can be source or binary
12
+ ".rpm" => ["application/octet-stream", 2000, 5100],
13
+ ".zip" => ["application/octet-stream", 3000, 5000],
14
+ ".bz2" => ["application/octet-stream", 3100, 5010],
15
+ ".gz" => ["application/octet-stream", 3110, 5020],
16
+ ".jpg" => ["application/octet-stream", 8000],
17
+ ".jpeg" => ["application/octet-stream", 8000],
18
+ ".txt" => ["text/plain", 8100, 8100],
19
+ ".html" => ["text/html", 8200, 8200],
20
+ ".pdf" => ["application/octet-stream", 8300],
21
+ ".ebuild" => ["application/octet-stream", 1300],
22
+ ".exe" => ["application/octet-stream", 1100],
23
+ ".dmg" => ["application/octet-stream", 1200],
24
+ ".gem" => ["application/octet-stream", 1400],
25
+ ".sig" => ["application/octet-stream", 8150]
26
+ }
27
+ FILE_TYPES.default = ["application/octet-stream", 9999, 5900] # default to "other", "other source"
28
+
29
+ attr_reader :basename, :ext, :content_type, :bin_type_id, :src_type_id
30
+
31
+ def initialize(filename)
32
+ @filename = filename
33
+ @basename = File.basename(filename)
34
+ @ext = File.extname(filename)
35
+ @content_type = FILE_TYPES[@ext][0]
36
+ @bin_type_id = FILE_TYPES[@ext][1]
37
+ end
38
+
39
+ def data
40
+ File.open(@filename, "rb") { |file| file.read }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,76 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'open-uri'
4
+
5
+ module MetaProject
6
+ module Project
7
+ module XForge
8
+ class XForgeBase < Base
9
+
10
+ def initialize(host, unix_name, cvs_mod)
11
+ @host = host
12
+ @unix_name = unix_name
13
+
14
+ @tracker = tracker_class.new(group_id_uri("tracker"), self)
15
+
16
+ unless(cvs_mod.nil?)
17
+ @scm = create_cvs(unix_name, cvs_mod)
18
+ @scm_web = create_view_cvs(unix_name, cvs_mod)
19
+ end
20
+ end
21
+
22
+ def to_yaml_properties
23
+ ["@host", "@unix_name"]
24
+ end
25
+
26
+ # Logs in and returns a Session
27
+ def login(user_name, password)
28
+ http = Net::HTTP.new(@host, 80)
29
+
30
+ login_response = http.start do |http|
31
+ data = [
32
+ "login=1",
33
+ "form_loginname=#{user_name}",
34
+ "form_pw=#{password}"
35
+ ].join("&")
36
+ http.post("/account/login.php", data)
37
+ end
38
+
39
+ cookie = login_response["set-cookie"]
40
+ raise "Login failed" unless cookie
41
+ Session.new(@host, self, cookie)
42
+ end
43
+
44
+ # The group_id of this project
45
+ def group_id
46
+ unless(@group_id)
47
+ regexp = /stats\/[?&]group_id=(\d+)/
48
+ html = open(project_uri) { |data| data.read }
49
+ @group_id = html[regexp, 1]
50
+ raise "Couldn't get group_id" unless @group_id
51
+ end
52
+ @group_id
53
+ end
54
+
55
+ def project_uri
56
+ "http://#{@host}/projects/#{@unix_name}/"
57
+ end
58
+
59
+ def group_id_uri(path, postfix="")
60
+ "http://#{@host}/#{path}/?group_id=#{group_id}#{postfix}"
61
+ end
62
+
63
+ # The home page of this project
64
+ def home_page
65
+ unless(@home_page)
66
+ html = open(project_uri) { |data| data.read }
67
+ @home_page = html[home_page_regexp, 1]
68
+ raise "Couldn't get home_page" unless @home_page
69
+ end
70
+ @home_page
71
+ end
72
+
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ require 'meta_project/project/xforge/xforge_base'
2
+ require 'meta_project/project/xforge/session'
3
+ require 'meta_project/project/xforge/xfile'
4
+ require 'meta_project/project/xforge/ruby_forge'
5
+ require 'meta_project/project/xforge/source_forge'
@@ -0,0 +1,4 @@
1
+ require 'meta_project/project/base'
2
+ require 'meta_project/project/xforge'
3
+ require 'meta_project/project/trac'
4
+ require 'meta_project/project/codehaus'
@@ -0,0 +1,36 @@
1
+ module MetaProject
2
+ module ProjectAnalyzer
3
+ # Creates a project from an scm web url. The project has a +tracker+, +scm+ and +scm_web+.
4
+ def project_from_scm_web(url, options=nil)
5
+ # RubyForge
6
+ if(url =~ /http:\/\/rubyforge.org\/cgi-bin\/viewcvs.cgi\/(.*)[\/]?\?cvsroot=(.*)/)
7
+ unix_name = $2
8
+ mod = $1[-1..-1] == "/" ? $1[0..-2] : $1
9
+ return Project::XForge::RubyForge.new(unix_name, mod)
10
+ end
11
+
12
+ # SourceForge
13
+ if(url =~ /http:\/\/cvs.sourceforge.net\/viewcvs.py\/([^\/]*)\/(.*)/)
14
+ unix_name = $1
15
+ mod = $2[-1..-1] == "/" ? $2[0..-2] : $2
16
+ return Project::XForge::SourceForge.new(unix_name, mod)
17
+ end
18
+
19
+ # Trac
20
+ if(url =~ /(http:\/\/.*)\/browser\/(.*)/)
21
+ trac_base_url = $1
22
+ svn_path = $2[-1..-1] == "/" ? $2[0..-2] : $2
23
+ return Project::Trac::TracProject.new(trac_base_url, options[:svn_root_url], svn_path)
24
+ end
25
+
26
+ # Codehaus SVN
27
+ if(url =~ /http:\/\/svn.(.*).codehaus.org\/(.*)/)
28
+ svn_id = $1
29
+ svn_path = $2[-1..-1] == "/" ? $2[0..-2] : $2
30
+ return Project::Codehaus::CodehausProjectSvn.new(svn_id, svn_path, options[:jira_id])
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ module MetaProject
2
+
3
+ # An ScmWeb instance is capable of generating URLs to various files and diffs
4
+ # in an online scm web interface.
5
+ class ScmWeb
6
+
7
+ # The variables to use in +uri_specs+ are:
8
+ #
9
+ # * path
10
+ # * revision
11
+ # * previous_revision
12
+ #
13
+ def initialize(overview_spec, history_spec, raw_spec, html_spec, diff_spec)
14
+ @overview_spec, @history_spec, @raw_spec, @html_spec, @diff_spec = overview_spec, history_spec, raw_spec, html_spec, diff_spec
15
+ end
16
+
17
+ def overview
18
+ file_uri(nil, nil, @overview_spec)
19
+ end
20
+
21
+ def history(path)
22
+ file_uri(path, nil, @history_spec)
23
+ end
24
+
25
+ def raw(path, revision)
26
+ file_uri(path, revision, @raw_spec)
27
+ end
28
+
29
+ def html(path, revision)
30
+ file_uri(path, revision, @html_spec)
31
+ end
32
+
33
+ def diff(path, revision, previous_revision)
34
+ file_uri(path, revision, @diff_spec, previous_revision)
35
+ end
36
+
37
+ private
38
+
39
+ # Returns the file URI for +path+. Valid options are:
40
+ #
41
+ # * :type => ["overview"|"html"|"diff"|"raw"]
42
+ # * :revision
43
+ # * :previous_revision
44
+ def file_uri(path="", revision=nil, spec=@overview_spec, previous_revision=nil)
45
+ begin
46
+ eval("\"#{spec}\"", binding)
47
+ rescue NameError
48
+ raise "Couldn't evaluate '#{spec}'"
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ module MetaProject
2
+ module Tracker
3
+
4
+ # Tracker objects are responsible for interacting with issue trackers (bug trackers).
5
+ # They know how to recognise issue identifiers in strings (typically from SCM commit
6
+ # messages) and turn these into HTML links that point to the associated issue on an
7
+ # issue tracker installation running somewhere else.
8
+ class Base
9
+ def self.classes
10
+ [
11
+ ::Tracker::Jira::JiraProject,
12
+ ::Tracker::XForge::RubyForgeProject,
13
+ ::Tracker::Trac::TracProject
14
+ ]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module MetaProject
2
+ module Tracker
3
+ # This module should be included by trackers that follow a digit-based issue scheme
4
+ module DigitIssues
5
+ def identifier_regexp
6
+ /#(\d+)/
7
+ end
8
+
9
+ def identifier_examples
10
+ ["#1926", "#1446"]
11
+ end
12
+
13
+ # TODO: find a way to extract just the issue summaries so they can be stored in dc as an array
14
+ # embedded in the revision object. that way we don't alter the original commit message
15
+ def markup(text)
16
+ text.gsub(identifier_regexp) do |match|
17
+ issue_identifier = $1
18
+ issue = issue(issue_identifier)
19
+ issue ? "<a href=\"#{issue.uri}\">#{issue.summary}</a>" : "\##{issue_identifier}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module MetaProject
2
+ module Tracker
3
+ class Issue
4
+ attr_reader :uri, :summary
5
+
6
+ def initialize(uri, summary)
7
+ @uri, @summary = uri, summary
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,68 @@
1
+ require 'xmlrpc/client'
2
+
3
+ module MetaProject
4
+ module Tracker
5
+ module Jira
6
+ class JiraTracker
7
+ JIRA_API = "jira1"
8
+
9
+ def initialize(rooturl=nil, identifier=nil, username=nil, password=nil)
10
+ @rooturl, @identifier, @username, @password = rooturl, identifier, username, password
11
+ end
12
+
13
+ def identifier_regexp
14
+ /([A-Z]+-[\d]+)/
15
+ end
16
+
17
+ def identifier_examples
18
+ ["DC-420", "PICO-12"]
19
+ end
20
+
21
+ def overview
22
+ "#{@rooturl}/browse/#{@identifier}"
23
+ end
24
+
25
+ def issue(issue_identifier)
26
+ session = login
27
+ begin
28
+ issue = session.getIssue(issue_identifier)
29
+ Issue.new("#{@rooturl}/browse/#{issue_identifier}", issue["summary"])
30
+ rescue XMLRPC::FaultException
31
+ # Probably bad issue number
32
+ nil
33
+ end
34
+ end
35
+
36
+ def markup(text)
37
+ text.gsub(identifier_regexp) do |match|
38
+ issue_identifier = $1
39
+ issue = issue(issue_identifier)
40
+ issue ? "<a href=\"#{issue.uri}\">#{issue_identifier}: #{issue.summary}</a>" : issue_identifier
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def login
47
+ client = XMLRPC::Client.new2("#{@rooturl}/rpc/xmlrpc")
48
+ token = client.call("#{JIRA_API}.login", @username, @password)
49
+ Session.new(client, token)
50
+ end
51
+
52
+ # This wrapper around XMLRPC::Client that allows simpler method calls
53
+ # via method_missing and doesn't require to manage the token
54
+ class Session
55
+ def initialize(client, token)
56
+ @client, @token = client, token
57
+ end
58
+
59
+ def method_missing(sym, args, &block)
60
+ token_args = [@token] << args
61
+ xmlrpc_method = "#{JIRA_API}.#{sym.to_s}"
62
+ @client.call(xmlrpc_method, *token_args)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1 @@
1
+ require 'meta_project/tracker/jira/jira_tracker'
@@ -0,0 +1,29 @@
1
+ module MetaProject
2
+ module Tracker
3
+ module Trac
4
+ class TracTracker < ::MetaProject::Tracker::Base
5
+ include ::MetaProject::Tracker::DigitIssues
6
+
7
+ def initialize(trac_base_url)
8
+ @trac_base_url = trac_base_url
9
+ end
10
+
11
+ def overview
12
+ "#{@trac_base_url}/report"
13
+ end
14
+
15
+ def issue(issue_identifier)
16
+ issue_uri = "#{@trac_base_url}/ticket/#{issue_identifier}"
17
+ begin
18
+ html = open(issue_uri) { |data| data.read }
19
+ summary = html[/Ticket ##{issue_identifier}\s*<\/h1>\s*<h2>([^<]*)<\/h2>/n, 1]
20
+ ::MetaProject::Tracker::Issue.new(issue_uri, summary)
21
+ rescue OpenURI::HTTPError
22
+ nil
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1 @@
1
+ require 'meta_project/tracker/trac/trac_tracker'
@@ -0,0 +1,17 @@
1
+ module MetaProject
2
+ module Tracker
3
+ module XForge
4
+ class RubyForgeTracker < XForgeTracker
5
+
6
+ def subtracker_regexp
7
+ /\/tracker\/\?atid=(\d+)&group_id=\d*&func=browse/
8
+ end
9
+
10
+ def issue_regexp(identifier)
11
+ /<a href=\"\/tracker\/index.php\?func=detail&aid=#{identifier}&group_id=\d+&atid=\d+\">([^<]*)<\/a>/
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module MetaProject
2
+ module Tracker
3
+ module XForge
4
+ class SourceForgeTracker < XForgeTracker
5
+
6
+ def subtracker_regexp
7
+ /\/tracker\/\?atid=(\d+)&amp;group_id=\d*&amp;func=browse/
8
+ end
9
+
10
+ def issue_regexp(identifier)
11
+ /<a href=\"\/tracker\/index.php\?func=detail&amp;aid=#{identifier}&amp;group_id=\d+&amp;atid=\d+\">([^<]*)<\/a>/
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end