tractive 1.0.0

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +24 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +47 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/Gemfile +14 -0
  8. data/LICENSE.md +69 -0
  9. data/README.adoc +742 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/config.example.yaml +73 -0
  14. data/db/trac-test.db +0 -0
  15. data/docker/Dockerfile +19 -0
  16. data/docker/docker-compose.yml +68 -0
  17. data/exe/tractive +111 -0
  18. data/lib/tractive/attachment_exporter.rb +62 -0
  19. data/lib/tractive/github_api/client/issues.rb +78 -0
  20. data/lib/tractive/github_api/client/milestones.rb +35 -0
  21. data/lib/tractive/github_api/client.rb +16 -0
  22. data/lib/tractive/github_api.rb +3 -0
  23. data/lib/tractive/graceful_quit.rb +30 -0
  24. data/lib/tractive/info.rb +46 -0
  25. data/lib/tractive/main.rb +81 -0
  26. data/lib/tractive/migrator/converter/trac_to_github.rb +307 -0
  27. data/lib/tractive/migrator/converter/twf_to_markdown.rb +125 -0
  28. data/lib/tractive/migrator/converter.rb +3 -0
  29. data/lib/tractive/migrator/engine/migrate_from_db.rb +95 -0
  30. data/lib/tractive/migrator/engine/migrate_from_file.rb +100 -0
  31. data/lib/tractive/migrator/engine/migrate_to_file.rb +68 -0
  32. data/lib/tractive/migrator/engine.rb +131 -0
  33. data/lib/tractive/migrator.rb +3 -0
  34. data/lib/tractive/models/attachment.rb +10 -0
  35. data/lib/tractive/models/milestone.rb +6 -0
  36. data/lib/tractive/models/report.rb +6 -0
  37. data/lib/tractive/models/revision.rb +6 -0
  38. data/lib/tractive/models/session.rb +6 -0
  39. data/lib/tractive/models/ticket.rb +36 -0
  40. data/lib/tractive/models/ticket_change.rb +7 -0
  41. data/lib/tractive/revmap_generator.rb +111 -0
  42. data/lib/tractive/trac.rb +16 -0
  43. data/lib/tractive/utilities.rb +68 -0
  44. data/lib/tractive/version.rb +5 -0
  45. data/lib/tractive.rb +29 -0
  46. data/tractive.gemspec +37 -0
  47. metadata +189 -0
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Migrator
4
+ module Converter
5
+ class TracToGithub
6
+ def initialize(args)
7
+ @tracticketbaseurl = args[:cfg]["trac"]["ticketbaseurl"]
8
+ @attachurl = args[:opts][:attachurl] || args[:cfg].dig("attachments", "url")
9
+ @changeset_base_url = args[:cfg]["trac"]["changeset_base_url"]
10
+ @singlepost = args[:opts][:singlepost]
11
+ @labels_cfg = args[:cfg]["labels"].transform_values(&:to_h)
12
+ @milestonesfromtrac = args[:cfg]["milestones"]
13
+ @users = args[:cfg]["users"].to_h
14
+ @trac_mails_cache = {}
15
+ @repo = args[:cfg]["github"]["repo"]
16
+ @client = GithubApi::Client.new(access_token: args[:cfg]["github"]["token"])
17
+ @wiki_attachments_url = args[:cfg]["trac"]["wiki_attachments_url"]
18
+
19
+ load_milestone_map
20
+
21
+ @uri_parser = URI::Parser.new
22
+ @twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(@tracticketbaseurl, @attachurl, @changeset_base_url, @wiki_attachments_url)
23
+ end
24
+
25
+ def compose(ticket)
26
+ body = ""
27
+ closed = nil
28
+
29
+ # summary line:
30
+ # body += %i[id component priority resolution].map do |cat|
31
+ # ticket[cat] and !ticket[cat].to_s.lstrip.empty? and
32
+ # "**#{cat}:** #{ticket[cat]}"
33
+ # end.select { |x| x }.join(" | ")
34
+
35
+ # Initial report
36
+ # TODO: respect ticket[:changetime]
37
+ body += "\n\n" unless @singlepost
38
+ body += ticket_change(@singlepost, {
39
+ ticket: ticket[:id],
40
+ time: ticket[:time],
41
+ author: ticket[:reporter],
42
+ assigne: ticket[:owner],
43
+ field: :initial,
44
+ oldvalue: nil,
45
+ newvalue: ticket[:description]
46
+ })["body"]
47
+
48
+ changes = if ticket.is_a? Hash
49
+ []
50
+ else
51
+ ticket.all_changes
52
+ end
53
+
54
+ # replay all changes in chronological order:
55
+ comments = changes.map { |x| ticket_change(@singlepost, x) }.select { |x| x }.to_a
56
+ if @singlepost
57
+ body += comments.map { |x| x["body"] }.join("\n")
58
+ comments = []
59
+ end
60
+
61
+ labels = Set[]
62
+ changes.each do |x|
63
+ del = @labels_cfg.fetch(x[:field], {})[x[:oldvalue]]
64
+ # add = @labels_cfg.fetch(x[:field], {})[x[:newvalue]]
65
+ @labels_cfg.fetch(x[:field], {})[x[:newvalue]]
66
+ labels.delete(del) if del
67
+ # labels.add(add) if add
68
+ closed = x[:time] if (x[:field] == "status") && (x[:newvalue] == "closed")
69
+ end
70
+
71
+ # we separate labels from badges
72
+ # labels: are changed frequently in the lifecycle of a ticket, therefore are transferred to github lables
73
+ # badges: are basically fixed and are transferred to a metadata table in the ticket
74
+
75
+ badges = Set[]
76
+
77
+ badges.add(@labels_cfg.fetch("component", {})[ticket[:component]])
78
+ badges.add(@labels_cfg.fetch("type", {})[ticket[:type]])
79
+ badges.add(@labels_cfg.fetch("resolution", {})[ticket[:resolution]])
80
+ badges.add(@labels_cfg.fetch("version", {})[ticket[:version]])
81
+
82
+ labels.add(@labels_cfg.fetch("severity", {})[ticket[:severity]])
83
+ labels.add(@labels_cfg.fetch("priority", {})[ticket[:priority]])
84
+ labels.add(@labels_cfg.fetch("tracstate", {})[ticket[:status]])
85
+ labels.delete(nil)
86
+
87
+ keywords = ticket[:keywords]
88
+ if keywords
89
+ if ticket[:keywords].downcase == "discuss"
90
+ labels.add(@labels_cfg.fetch("keywords", {})[ticket[:keywords].downcase])
91
+ else
92
+ badges.add(@labels_cfg.fetch("keywords", {})[ticket[:keywords]])
93
+ end
94
+ end
95
+ # If the field is not set, it will be nil and generate an unprocessable json
96
+
97
+ milestone = @milestonemap[ticket[:milestone]]
98
+
99
+ # compute footer
100
+ footer = "_Issue migrated from trac:#{ticket[:id]} at #{Time.now}_"
101
+
102
+ # compute badgetabe
103
+ #
104
+
105
+ github_assignee = map_assignee(ticket[:owner])
106
+
107
+ badges = badges.to_a.compact.sort
108
+ badgetable = badges.map { |i| %(`#{i}`) }.join(" ")
109
+ badgetable += begin
110
+ " | by #{trac_mail(ticket[:reporter])}"
111
+ rescue StandardError
112
+ "deleted Ticket"
113
+ end
114
+ # badgetable += " | **->#{ticket[:owner]}**" # note that from github to gitlab we loose the assigne
115
+
116
+ # compose body
117
+ body = [badgetable, body, footer].join("\n\n___\n")
118
+
119
+ labels.add("owner:#{github_assignee}")
120
+
121
+ issue = {
122
+ "title" => ticket[:summary],
123
+ "body" => body,
124
+ "labels" => labels.to_a,
125
+ "closed" => ticket[:status] == "closed",
126
+ "created_at" => format_time(ticket[:time]),
127
+ "milestone" => milestone
128
+ }
129
+
130
+ if @users.key?(ticket[:owner])
131
+ owner = trac_mail(ticket[:owner])
132
+ github_owner = @users[owner]
133
+ $logger.debug("..owner in trac: #{owner}")
134
+ $logger.debug("..assignee in GitHub: #{github_owner}")
135
+ issue["assignee"] = github_owner
136
+ end
137
+
138
+ ### as the assignee stuff is pretty fragile, we do not assign at all
139
+ # issue['assignee'] = github_assignee if github_assignee
140
+
141
+ if ticket[:changetime]
142
+ # issue["updated_at"] = format_time(ticket[:changetime])
143
+ end
144
+
145
+ if issue["closed"] && closed
146
+ # issue["closed_at"] = format_time(closed)
147
+ end
148
+
149
+ {
150
+ "issue" => issue,
151
+ "comments" => comments
152
+ }
153
+ end
154
+
155
+ private
156
+
157
+ def map_user(user)
158
+ @users[user] || user
159
+ end
160
+
161
+ def map_assignee(user)
162
+ @users[user]
163
+ end
164
+
165
+ def load_milestone_map
166
+ read_milestones_from_github
167
+
168
+ newmilestonekeys = @milestonesfromtrac.keys - @milestonemap.keys
169
+
170
+ newmilestonekeys.each do |milestonelabel|
171
+ milestone = {
172
+ "title" => milestonelabel.to_s,
173
+ "state" => @milestonesfromtrac[milestonelabel][:completed].nil? ? "open" : "closed",
174
+ "description" => @milestonesfromtrac[milestonelabel][:description] || "no description in trac",
175
+ "due_on" => "2012-10-09T23:39:01Z"
176
+ }
177
+ due = @milestonesfromtrac[milestonelabel][:due]
178
+ milestone["due_on"] = Time.at(due / 1_000_000).strftime("%Y-%m-%dT%H:%M:%SZ") if due
179
+
180
+ $logger.info "creating #{milestone}"
181
+
182
+ @client.create_milestone(@repo, milestone)
183
+ end
184
+
185
+ read_milestones_from_github
186
+ nil
187
+ end
188
+
189
+ def read_milestones_from_github
190
+ milestonesongithub = @client.milestones(@repo, { state: "all",
191
+ sort: "due_on",
192
+ direction: "desc" })
193
+ @milestonemap = milestonesongithub.map { |i| [i["title"], i["number"]] }.to_h
194
+ nil
195
+ end
196
+
197
+ def ticket_change(append, meta)
198
+ # kind
199
+ kind = if meta[:ticket]
200
+ meta[:field].to_s
201
+ else
202
+ "attachment"
203
+ end
204
+ kind = "title" if kind == "summary"
205
+
206
+ # don't care
207
+ return unless interested_in_change?(kind, meta[:newvalue])
208
+
209
+ # author
210
+ author = meta[:author]
211
+ author = trac_mail(author)
212
+ author = "@#{map_user(author)}" if @users.key?(author)
213
+
214
+ text = ""
215
+
216
+ if kind != "initial"
217
+ text += "\n___\n" if append
218
+ text += "_#{author}_ " if author
219
+ end
220
+
221
+ case kind
222
+ when "owner", "status", "title", "resolution", "priority", "component", "type", "severity", "platform", "milestone"
223
+ old = meta[:oldvalue]
224
+ new = meta[:newvalue]
225
+ if old && new
226
+ text += "_changed #{kind} from `#{old}` to `#{new}`_"
227
+ elsif old
228
+ text += "_removed #{kind} (was `#{old}`)_"
229
+ elsif new
230
+ text += "_set #{kind} to `#{new}`_"
231
+ end
232
+
233
+ when :initial, "initial"
234
+ body = meta[:newvalue]
235
+ # text += "created the issue\n\n"
236
+ if body && !body.lstrip.empty?
237
+ # text += "\n___\n" if not append
238
+ text += @twf_to_markdown.convert(body)
239
+ end
240
+
241
+ when "comment"
242
+ body = meta[:newvalue]
243
+ changeset = body.match(/In \[changeset:"(\d+)/).to_a[1]
244
+ text += if changeset
245
+ # changesethash = @revmap[changeset]
246
+ "_committed #{Tractive::Utilities.map_changeset(changeset)}_"
247
+ else
248
+ "_commented_\n\n"
249
+ end
250
+
251
+ text += "\n___\n" unless append
252
+ text += @twf_to_markdown.convert(body) if body
253
+
254
+ when "attachment"
255
+ text += "_uploaded file "
256
+ name = meta[:filename]
257
+ body = meta[:description]
258
+ if @attachurl
259
+ url = @uri_parser.escape("#{@attachurl}/#{meta[:id]}/#{name}")
260
+ text += "[`#{name}`](#{url})"
261
+ body += "\n![#{name}](#{url})" if [".png", ".jpg", ".gif"].include? File.extname(name).downcase
262
+ else
263
+ text += "`#{name}`"
264
+ end
265
+ text += " (#{(meta[:size] / 1024.0).round(1)} KiB)_"
266
+ text += "\n\n#{body}"
267
+
268
+ when "description"
269
+ # (ticket[:description] already contains the new value,
270
+ # so there is no need to update)
271
+ text += "_edited the issue description_"
272
+
273
+ else
274
+ # this should not happen
275
+ text += "changed #{kind} which not transferred by tractive"
276
+ end
277
+
278
+ {
279
+ "body" => text,
280
+ "created_at" => format_time(meta[:time])
281
+ }
282
+ end
283
+
284
+ # returns the author mail if found, otherwise author itself
285
+ def trac_mail(author)
286
+ return @trac_mails_cache[author] if @trac_mails_cache.key?(author)
287
+
288
+ # tries to retrieve the email from trac db
289
+ data = Tractive::Session.select(:value).where(Sequel.lit('name = "email" AND sid = ?', author))
290
+ return (@trac_mails_cache[author] = data.first[:value]) if data.count == 1
291
+
292
+ (@trac_mails_cache[author] = author) # not found
293
+ end
294
+
295
+ # Format time for github API
296
+ def format_time(time)
297
+ time = Time.at(time / 1e6, time % 1e6)
298
+ time.strftime("%FT%TZ")
299
+ end
300
+
301
+ def interested_in_change?(kind, newvalue)
302
+ !(%w[keywords cc reporter version].include?(kind) ||
303
+ (kind == "comment" && (newvalue.nil? || newvalue.lstrip.empty?)))
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Migrator
4
+ module Converter
5
+ # twf => Trac wiki format
6
+ class TwfToMarkdown
7
+ def initialize(base_url, attach_url, changeset_base_url, wiki_attachments_url)
8
+ @base_url = base_url
9
+ @attach_url = attach_url
10
+ @changeset_base_url = changeset_base_url
11
+ @wiki_attachments_url = wiki_attachments_url
12
+ end
13
+
14
+ def convert(str)
15
+ convert_newlines(str)
16
+ convert_code_snippets(str)
17
+ convert_headings(str)
18
+ convert_links(str)
19
+ convert_font_styles(str)
20
+ convert_changeset(str, @changeset_base_url)
21
+ convert_image(str, @base_url, @attach_url, @wiki_attachments_url)
22
+ convert_ticket(str, @base_url)
23
+
24
+ str
25
+ end
26
+
27
+ private
28
+
29
+ # CommitTicketReference
30
+ def convert_ticket_reference(str)
31
+ str.gsub!(/\{\{\{\n(#!CommitTicketReference .+?)\}\}\}/m, '\1')
32
+ str.gsub!(/#!CommitTicketReference .+\n/, "")
33
+ end
34
+
35
+ # Ticket
36
+ def convert_ticket(str, base_url)
37
+ # replace a full ticket id with the github short refrence
38
+ if base_url
39
+ baseurlpattern = base_url.gsub("/", "\\/")
40
+ str.gsub!(%r{#{baseurlpattern}/(\d+)}) { "ticket:#{Regexp.last_match[1]}" }
41
+ end
42
+
43
+ # Ticket
44
+ str.gsub!(/ticket:(\d+)/, '#\1')
45
+ end
46
+
47
+ # Headings
48
+ def convert_headings(str)
49
+ str.gsub!(/======\s(.+?)\s======/, '###### \1')
50
+ str.gsub!(/=====\s(.+?)\s=====/, '##### \1')
51
+ str.gsub!(/====\s(.+?)\s====/, '#### \1')
52
+ str.gsub!(/===\s(.+?)\s===/, '### \1')
53
+ str.gsub!(/==\s(.+?)\s==/, '## \1')
54
+ str.gsub!(/=\s(.+?)\s=/, '# \1')
55
+ end
56
+
57
+ # Line endings
58
+ def convert_newlines(str)
59
+ str.gsub!(/\[\[br\]\]/i, "\n")
60
+ str.gsub!("\r\n", "\n")
61
+ end
62
+
63
+ # Code
64
+ def convert_code_snippets(str)
65
+ str.gsub!(/\{\{\{([^\n]+?)\}\}\}/, '`\1`')
66
+ str.gsub!(/\{\{\{(.+?)\}\}\}/m, '```\1```')
67
+ str.gsub!(/(?<=```)#!/m, "")
68
+ end
69
+
70
+ # Changeset
71
+ def convert_changeset(str, changeset_base_url)
72
+ str.gsub!(%r{#{Regexp.quote(changeset_base_url)}/(\d+)/?}, '[changeset:\1]') if changeset_base_url
73
+ str.gsub!(/\[changeset:"r(\d+)".*\]/, '[changeset:\1]')
74
+ str.gsub!(/\[changeset:r(\d+)\]/, '[changeset:\1]')
75
+ str.gsub!(/\br(\d+)\b/) { Tractive::Utilities.map_changeset(Regexp.last_match[1]) }
76
+ str.gsub!(/\[changeset:"(\d+)".*\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1]) }
77
+ str.gsub!(/\[changeset:"(\d+).*\]/) { Tractive::Utilities.map_changeset(Regexp.last_match[1]) }
78
+ end
79
+
80
+ # Font styles
81
+ def convert_font_styles(str)
82
+ str.gsub!(/'''(.+?)'''/, '**\1**')
83
+ str.gsub!(/''(.+?)''/, '*\1*')
84
+ str.gsub!(%r{[^:]//(.+?[^:])//}, '_\1_')
85
+ end
86
+
87
+ # Links
88
+ def convert_links(str)
89
+ str.gsub!(/\[(http[^\s\[\]]+)\s([^\[\]]+)\]/, '[\2](\1)')
90
+ str.gsub!(/!(([A-Z][a-z0-9]+){2,})/, '\1')
91
+ end
92
+
93
+ def convert_image(str, base_url, attach_url, wiki_attachments_url)
94
+ # https://trac.edgewall.org/wiki/WikiFormatting#Images
95
+ # [[Image(picture.gif)]] Current page (Ticket, Wiki, Comment)
96
+ # [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
97
+ # [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
98
+
99
+ image_regex = /\[\[Image\((?:(?<module>(?:source|wiki)):)?(?<path>[^)]+)\)\]\]/
100
+ d = image_regex.match(str)
101
+ return if d.nil?
102
+
103
+ path = d[:path]
104
+ mod = d[:module]
105
+
106
+ image_path = if mod == "source"
107
+ "![#{path.split("/").last}](#{base_url}#{path})"
108
+ elsif mod == "wiki"
109
+ _, file = path.split(":")
110
+ upload_path = "#{wiki_attachments_url}/#{file}"
111
+ "![#{file}](#{upload_path})"
112
+ elsif path.start_with?("http")
113
+ # [[Image(http://example.org/s.jpg)]]
114
+ "![#{d[:path]}](#{d[:path]})"
115
+ else
116
+ _, id, file = path.split(":")
117
+ file_path = "#{attach_url}/#{id}/#{file}"
118
+ "![#{d[:path]}](#{file_path})"
119
+ end
120
+
121
+ str.gsub!(image_regex, image_path)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "converter/trac_to_github"
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Migrator
4
+ class Engine
5
+ module MigrateFromDb
6
+ def migrate_from_db
7
+ Tractive::GracefulQuit.enable
8
+ migrate_tickets_from_db(@start_ticket, @filter_closed)
9
+ rescue RuntimeError => e
10
+ $logger.error e.message
11
+ end
12
+
13
+ private
14
+
15
+ # Creates github issues for trac tickets.
16
+ def migrate_tickets_from_db(start_ticket, filterout_closed)
17
+ $logger.info("migrating issues")
18
+ # We match the issue title to determine whether an issue exists already.
19
+ tractickets = @trac.tickets
20
+ .for_migration(start_ticket, filterout_closed, @filter_options)
21
+ .all
22
+
23
+ begin
24
+ lasttracid = tractickets.last[:id]
25
+ rescue StandardError
26
+ raise("trac has no ticket #{start_ticket}")
27
+ end
28
+
29
+ (start_ticket.to_i..lasttracid).each do |ticket_id|
30
+ ticket = tractickets.select { |i| i[:id] == ticket_id }.first
31
+ @current_ticket_id = ticket_id # used to build filename for attachments
32
+
33
+ if ticket.nil?
34
+ next unless @mockdeleted
35
+
36
+ ticket = mock_ticket_details(ticket_id)
37
+ end
38
+
39
+ raise("tickets out of sync #{ticket_id} - #{ticket[:id]}") if ticket[:id] != ticket_id
40
+
41
+ Tractive::GracefulQuit.check("quitting after processing ticket ##{@last_created_issue}")
42
+
43
+ if @safetychecks
44
+ begin
45
+ # issue exists already:
46
+ @client.issue(@repo, ticket[:id])
47
+ $logger.info("found ticket #{ticket[:id]}")
48
+ next
49
+ rescue StandardError
50
+ end
51
+ end
52
+
53
+ $logger.info(%{creating issue for trac #{ticket[:id]} "#{ticket[:summary]}" (#{ticket[:reporter]})})
54
+ # API details: https://gist.github.com/jonmagic/5282384165e0f86ef105
55
+ request = Migrator::Converter::TracToGithub.new(@config).compose(ticket)
56
+
57
+ response = @client.create_issue(@repo, request)
58
+
59
+ if @safetychecks # - it is not really faster if we do not wait for the processing
60
+ while response["status"] == "pending"
61
+ sleep 1
62
+ $logger.info("Checking import status: #{response["id"]}")
63
+ $logger.debug("you can manually check: #{response["url"]}")
64
+ response = @client.issue_import_status(@repo, response["id"])
65
+ end
66
+
67
+ $logger.info("Status: #{response["status"]}")
68
+
69
+ if response["status"] == "failed"
70
+ $logger.error(response["errors"])
71
+ exit 1
72
+ end
73
+
74
+ issue_id = response["issue_url"].match(/\d+$/).to_s.to_i
75
+
76
+ $logger.info("created issue ##{issue_id} for trac ticket #{ticket[:id]}")
77
+
78
+ update_comment_ref(issue_id) if request.to_s.include?("Replying to [comment:")
79
+
80
+ # assert correct issue number
81
+ if issue_id != ticket[:id]
82
+ $logger.warn("mismatch issue ##{issue_id} for ticket #{ticket[:id]}")
83
+ exit 1
84
+ end
85
+ else
86
+ # to allow manual verification:
87
+ $logger.info(response["url"])
88
+ end
89
+
90
+ @last_created_issue = ticket[:id]
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Migrator
4
+ class Engine
5
+ module MigrateFromFile
6
+ def migrate_from_file
7
+ Tractive::GracefulQuit.enable
8
+ migrate_tickets_from_file(@start_ticket, @filter_closed)
9
+ rescue RuntimeError => e
10
+ $logger.error e.message
11
+ end
12
+
13
+ private
14
+
15
+ # Creates github issues for trac tickets.
16
+ def migrate_tickets_from_file(start_ticket, filterout_closed)
17
+ $logger.info("migrating issues")
18
+ # We match the issue title to determine whether an issue exists already.
19
+ tractickets = @trac.tickets.order(:id).where { id >= start_ticket }.all
20
+ begin
21
+ lasttracid = @input_file.keys.map(&:to_i).max
22
+ rescue StandardError
23
+ raise("trac has no ticket #{start_ticket}")
24
+ end
25
+
26
+ (start_ticket.to_i..lasttracid).each do |ticket_id|
27
+ ticket = tractickets.select { |i| i[:id] == ticket_id }.first
28
+
29
+ @current_ticket_id = ticket_id # used to build filename for attachments
30
+
31
+ if ticket.nil?
32
+ next unless @mockdeleted
33
+
34
+ ticket = {
35
+ id: ticket_id,
36
+ summary: "DELETED in trac #{ticket_id}",
37
+ time: Time.now.to_i,
38
+ status: "closed",
39
+ reporter: "tractive"
40
+ }
41
+ end
42
+
43
+ raise("tickets out of sync #{ticket_id} - #{ticket[:id]}") if ticket[:id] != ticket_id
44
+
45
+ next if filterout_closed && (ticket[:status] == "closed")
46
+
47
+ Tractive::GracefulQuit.check("quitting after processing ticket ##{@last_created_issue}")
48
+
49
+ if @safetychecks
50
+ begin
51
+ # issue exists already:
52
+ @client.issue(@repo, ticket[:id])
53
+ $logger.info("found ticket #{ticket[:id]}")
54
+ next
55
+ rescue StandardError
56
+ end
57
+ end
58
+
59
+ $logger.info(%{creating issue from file for trac #{ticket[:id]} "#{ticket[:summary]}" (#{ticket[:reporter]})})
60
+ # API details: https://gist.github.com/jonmagic/5282384165e0f86ef105
61
+ request = @input_file[@current_ticket_id.to_s]
62
+ response = @client.create_issue(@repo, request)
63
+
64
+ if @safetychecks # - it is not really faster if we do not wait for the processing
65
+ while response["status"] == "pending"
66
+ sleep 1
67
+ $logger.info("Checking import status: #{response["id"]}")
68
+ $logger.info("you can manually check: #{response["url"]}")
69
+ response = @client.issue_import_status(@repo, response["id"])
70
+ end
71
+
72
+ $logger.info("Status: #{response["status"]}")
73
+
74
+ if response["status"] == "failed"
75
+ $logger.error(response["errors"])
76
+ exit 1
77
+ end
78
+
79
+ issue_id = response["issue_url"].match(/\d+$/).to_s.to_i
80
+
81
+ $logger.info("created issue ##{issue_id} for trac ticket #{ticket[:id]}")
82
+
83
+ update_comment_ref(issue_id) if request.to_s.include?("Replying to [comment:")
84
+
85
+ # assert correct issue number
86
+ if issue_id != ticket[:id]
87
+ $logger.warn("mismatch issue ##{issue_id} for ticket #{ticket[:id]}")
88
+ exit 1
89
+ end
90
+ else
91
+ # to allow manual verification:
92
+ $logger.info(response["url"])
93
+ end
94
+
95
+ @last_created_issue = ticket[:id]
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end