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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +24 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +47 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +14 -0
- data/LICENSE.md +69 -0
- data/README.adoc +742 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config.example.yaml +73 -0
- data/db/trac-test.db +0 -0
- data/docker/Dockerfile +19 -0
- data/docker/docker-compose.yml +68 -0
- data/exe/tractive +111 -0
- data/lib/tractive/attachment_exporter.rb +62 -0
- data/lib/tractive/github_api/client/issues.rb +78 -0
- data/lib/tractive/github_api/client/milestones.rb +35 -0
- data/lib/tractive/github_api/client.rb +16 -0
- data/lib/tractive/github_api.rb +3 -0
- data/lib/tractive/graceful_quit.rb +30 -0
- data/lib/tractive/info.rb +46 -0
- data/lib/tractive/main.rb +81 -0
- data/lib/tractive/migrator/converter/trac_to_github.rb +307 -0
- data/lib/tractive/migrator/converter/twf_to_markdown.rb +125 -0
- data/lib/tractive/migrator/converter.rb +3 -0
- data/lib/tractive/migrator/engine/migrate_from_db.rb +95 -0
- data/lib/tractive/migrator/engine/migrate_from_file.rb +100 -0
- data/lib/tractive/migrator/engine/migrate_to_file.rb +68 -0
- data/lib/tractive/migrator/engine.rb +131 -0
- data/lib/tractive/migrator.rb +3 -0
- data/lib/tractive/models/attachment.rb +10 -0
- data/lib/tractive/models/milestone.rb +6 -0
- data/lib/tractive/models/report.rb +6 -0
- data/lib/tractive/models/revision.rb +6 -0
- data/lib/tractive/models/session.rb +6 -0
- data/lib/tractive/models/ticket.rb +36 -0
- data/lib/tractive/models/ticket_change.rb +7 -0
- data/lib/tractive/revmap_generator.rb +111 -0
- data/lib/tractive/trac.rb +16 -0
- data/lib/tractive/utilities.rb +68 -0
- data/lib/tractive/version.rb +5 -0
- data/lib/tractive.rb +29 -0
- data/tractive.gemspec +37 -0
- 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" 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
|
+
""
|
108
|
+
elsif mod == "wiki"
|
109
|
+
_, file = path.split(":")
|
110
|
+
upload_path = "#{wiki_attachments_url}/#{file}"
|
111
|
+
""
|
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,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
|