meta_project 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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