tractive 1.0.8 → 1.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/README.adoc +88 -16
- data/db/trac-test.db +0 -0
- data/exe/command_base.rb +11 -0
- data/exe/generate.rb +49 -0
- data/exe/tractive +25 -51
- data/lib/tractive/attachment_exporter.rb +4 -3
- data/lib/tractive/github_api/client/issues.rb +6 -6
- data/lib/tractive/github_api/client/labels.rb +2 -2
- data/lib/tractive/github_api/client/milestones.rb +2 -2
- data/lib/tractive/github_api/client.rb +2 -0
- data/lib/tractive/http/client/request.rb +59 -0
- data/lib/tractive/http/client.rb +3 -0
- data/lib/tractive/main.rb +5 -1
- data/lib/tractive/migrator/converter/trac_to_github.rb +27 -12
- data/lib/tractive/migrator/converter/twf_to_markdown.rb +228 -36
- data/lib/tractive/migrator/engine.rb +4 -3
- data/lib/tractive/migrator/wikis/migrate_from_db.rb +167 -0
- data/lib/tractive/migrator/wikis.rb +3 -0
- data/lib/tractive/migrator.rb +1 -0
- data/lib/tractive/models/attachment.rb +1 -0
- data/lib/tractive/models/ticket.rb +16 -8
- data/lib/tractive/models/wiki.rb +18 -0
- data/lib/tractive/trac.rb +2 -1
- data/lib/tractive/utilities.rb +18 -2
- data/lib/tractive/version.rb +1 -1
- data/lib/tractive.rb +1 -0
- metadata +11 -2
@@ -4,8 +4,8 @@ module Migrator
|
|
4
4
|
module Converter
|
5
5
|
class TracToGithub
|
6
6
|
def initialize(args)
|
7
|
-
@
|
8
|
-
@attachurl = args[:opts][:attachurl] || args[:cfg].dig("attachments", "url")
|
7
|
+
@trac_ticket_base_url = args[:cfg]["trac"]["ticketbaseurl"]
|
8
|
+
@attachurl = args[:opts][:attachurl] || args[:cfg].dig("ticket", "attachments", "url")
|
9
9
|
@changeset_base_url = args[:cfg]["trac"]["changeset_base_url"] || ""
|
10
10
|
@singlepost = args[:opts][:singlepost]
|
11
11
|
@labels_cfg = args[:cfg]["labels"].transform_values(&:to_h)
|
@@ -14,8 +14,12 @@ module Migrator
|
|
14
14
|
@trac_mails_cache = {}
|
15
15
|
@repo = args[:cfg]["github"]["repo"]
|
16
16
|
@client = GithubApi::Client.new(access_token: args[:cfg]["github"]["token"])
|
17
|
-
@wiki_attachments_url = args[:cfg]
|
17
|
+
@wiki_attachments_url = args[:cfg].dig("wiki", "attachments", "url")
|
18
18
|
@revmap_file_path = args[:opts][:revmapfile] || args[:cfg]["revmap_path"]
|
19
|
+
@attachment_options = {
|
20
|
+
url: @attachurl,
|
21
|
+
hashed: args[:cfg].dig("ticket", "attachments", "hashed")
|
22
|
+
}
|
19
23
|
|
20
24
|
load_milestone_map
|
21
25
|
create_labels_on_github(@labels_cfg["severity"].values)
|
@@ -24,12 +28,19 @@ module Migrator
|
|
24
28
|
create_labels_on_github(@labels_cfg["component"].values)
|
25
29
|
|
26
30
|
@uri_parser = URI::Parser.new
|
27
|
-
@twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(
|
31
|
+
@twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(
|
32
|
+
@trac_ticket_base_url,
|
33
|
+
@attachment_options,
|
34
|
+
@changeset_base_url,
|
35
|
+
@wiki_attachments_url,
|
36
|
+
@revmap_file_path,
|
37
|
+
git_repo: @repo, home_page_name: args[:opts]["home-page-name"]
|
38
|
+
)
|
28
39
|
end
|
29
40
|
|
30
41
|
def compose(ticket)
|
31
|
-
body
|
32
|
-
|
42
|
+
body = ""
|
43
|
+
closed_time = nil
|
33
44
|
|
34
45
|
# summary line:
|
35
46
|
# body += %i[id component priority resolution].map do |cat|
|
@@ -70,7 +81,7 @@ module Migrator
|
|
70
81
|
@labels_cfg.fetch(x[:field], {})[x[:newvalue]]
|
71
82
|
labels.delete(del) if del
|
72
83
|
# labels.add(add) if add
|
73
|
-
|
84
|
+
closed_time = x[:time] if x[:field] == "status" && x[:newvalue] == "closed"
|
74
85
|
end
|
75
86
|
|
76
87
|
# we separate labels from badges
|
@@ -149,8 +160,12 @@ module Migrator
|
|
149
160
|
# issue["updated_at"] = format_time(ticket[:changetime])
|
150
161
|
end
|
151
162
|
|
152
|
-
if issue["closed"]
|
153
|
-
|
163
|
+
if issue["closed"]
|
164
|
+
issue["closed_at"] = if closed_time
|
165
|
+
format_time(closed_time)
|
166
|
+
else
|
167
|
+
format_time(ticket[:closed_at].to_i)
|
168
|
+
end
|
154
169
|
end
|
155
170
|
|
156
171
|
{
|
@@ -287,7 +302,7 @@ module Migrator
|
|
287
302
|
name = meta[:filename]
|
288
303
|
body = meta[:description]
|
289
304
|
if @attachurl
|
290
|
-
url = @uri_parser.escape("#{@attachurl}/#{meta[:id]
|
305
|
+
url = @uri_parser.escape("#{@attachurl}/#{Tractive::Utilities.attachment_path(meta[:id], name, @attachment_options)}")
|
291
306
|
text += "[`#{name}`](#{url})"
|
292
307
|
body += "\n![#{name}](#{url})" if [".png", ".jpg", ".gif"].include? File.extname(name).downcase
|
293
308
|
else
|
@@ -335,9 +350,9 @@ module Migrator
|
|
335
350
|
end
|
336
351
|
|
337
352
|
def trac_ticket_link(ticket)
|
338
|
-
return "trac:#{ticket[:id]}" unless @
|
353
|
+
return "trac:#{ticket[:id]}" unless @trac_ticket_base_url
|
339
354
|
|
340
|
-
"[trac:#{ticket[:id]}](#{@
|
355
|
+
"[trac:#{ticket[:id]}](#{@trac_ticket_base_url}/#{ticket[:id]})"
|
341
356
|
end
|
342
357
|
end
|
343
358
|
end
|
@@ -1,26 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "cgi"
|
4
|
+
|
3
5
|
module Migrator
|
4
6
|
module Converter
|
5
7
|
# twf => Trac wiki format
|
6
8
|
class TwfToMarkdown
|
7
|
-
def initialize(base_url,
|
9
|
+
def initialize(base_url, attachment_options, changeset_base_url, wiki_attachments_url, revmap_file_path, options = {})
|
8
10
|
@base_url = base_url
|
9
|
-
@attach_url =
|
11
|
+
@attach_url = attachment_options[:url]
|
12
|
+
@attach_hashed = attachment_options[:hashed]
|
10
13
|
@changeset_base_url = changeset_base_url
|
11
14
|
@wiki_attachments_url = wiki_attachments_url
|
12
15
|
@revmap = load_revmap_file(revmap_file_path)
|
16
|
+
|
17
|
+
@git_repo = options[:git_repo]
|
18
|
+
@home_page_name = options[:home_page_name]
|
19
|
+
@wiki_extensions = options[:wiki_extensions] # || [".py", "changelog", "expire-ids"]
|
20
|
+
@source_folders = options[:source_folders] # || %w[personal attic sprint branch/hawk]
|
13
21
|
end
|
14
22
|
|
15
23
|
def convert(str)
|
24
|
+
# Fix 'Windows EOL' to 'Linux EOL'
|
25
|
+
str.gsub!("\r\n", "\n")
|
26
|
+
|
27
|
+
convert_tables(str)
|
16
28
|
convert_newlines(str)
|
29
|
+
convert_comments(str)
|
30
|
+
convert_html_snippets(str)
|
17
31
|
convert_code_snippets(str)
|
18
32
|
convert_headings(str)
|
19
|
-
convert_links(str)
|
33
|
+
convert_links(str, @git_repo)
|
20
34
|
convert_font_styles(str)
|
21
35
|
convert_changeset(str, @changeset_base_url)
|
22
36
|
convert_image(str, @base_url, @attach_url, @wiki_attachments_url)
|
23
37
|
convert_ticket(str, @base_url)
|
38
|
+
revert_intermediate_references(str)
|
24
39
|
|
25
40
|
str
|
26
41
|
end
|
@@ -45,18 +60,12 @@ module Migrator
|
|
45
60
|
revmap
|
46
61
|
end
|
47
62
|
|
48
|
-
# CommitTicketReference
|
49
|
-
def convert_ticket_reference(str)
|
50
|
-
str.gsub!(/\{\{\{\n(#!CommitTicketReference .+?)\}\}\}/m, '\1')
|
51
|
-
str.gsub!(/#!CommitTicketReference .+\n/, "")
|
52
|
-
end
|
53
|
-
|
54
63
|
# Ticket
|
55
64
|
def convert_ticket(str, base_url)
|
56
65
|
# replace a full ticket id with the github short refrence
|
57
66
|
if base_url
|
58
67
|
baseurlpattern = base_url.gsub("/", "\\/")
|
59
|
-
str.gsub!(%r{#{baseurlpattern}/(\d+)}
|
68
|
+
str.gsub!(%r{#{baseurlpattern}/(\d+)}, '#\1')
|
60
69
|
end
|
61
70
|
|
62
71
|
# Ticket
|
@@ -79,16 +88,35 @@ module Migrator
|
|
79
88
|
str.gsub!("\r\n", "\n")
|
80
89
|
end
|
81
90
|
|
91
|
+
# Comments
|
92
|
+
def convert_comments(str)
|
93
|
+
str.gsub!(/\{\{\{(?>((?!(?:}}}|{{{)).+?|\g<0>))*\}\}\}/m) do |str_match|
|
94
|
+
str_match.gsub(/\{\{\{\s*#!comment(\s*)(.*)\}\}\}/m, '<!--\1\2\1-->')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# HTML Snippets
|
99
|
+
def convert_html_snippets(str)
|
100
|
+
str.gsub!(/\{\{\{#!html(.*?)\}\}\}/m, '\1')
|
101
|
+
end
|
102
|
+
|
103
|
+
# CommitTicketReference
|
104
|
+
def convert_ticket_reference(str)
|
105
|
+
str.gsub!(/\{\{\{\n(#!CommitTicketReference .+?)\}\}\}/m, '\1')
|
106
|
+
str.gsub!(/#!CommitTicketReference .+\n/, "")
|
107
|
+
end
|
108
|
+
|
82
109
|
# Code
|
83
110
|
def convert_code_snippets(str)
|
84
111
|
str.gsub!(/\{\{\{([^\n]+?)\}\}\}/, '`\1`')
|
112
|
+
str.gsub!(/\{\{\{#!(.*?)\n(.+?)\}\}\}/m, "```\\1\n\\2\n```")
|
85
113
|
str.gsub!(/\{\{\{(.+?)\}\}\}/m, '```\1```')
|
86
114
|
str.gsub!(/(?<=```)#!/m, "")
|
87
115
|
end
|
88
116
|
|
89
117
|
# Changeset
|
90
118
|
def convert_changeset(str, changeset_base_url)
|
91
|
-
str.gsub!(%r{#{Regexp.quote(changeset_base_url)}/(\d+)/?}, '[changeset:\1]') if changeset_base_url
|
119
|
+
str.gsub!(%r{#{Regexp.quote(changeset_base_url)}/(\d+)/?}, '[changeset:\1]') if changeset_base_url && !changeset_base_url.empty?
|
92
120
|
str.gsub!(/\[changeset:"r(\d+)".*\]/, '[changeset:\1]')
|
93
121
|
str.gsub!(/\[changeset:r(\d+)\]/, '[changeset:\1]')
|
94
122
|
str.gsub!(/\br(\d+)\b/) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
|
@@ -102,15 +130,170 @@ module Migrator
|
|
102
130
|
def convert_font_styles(str)
|
103
131
|
str.gsub!(/'''(.+?)'''/, '**\1**')
|
104
132
|
str.gsub!(/''(.+?)''/, '*\1*')
|
105
|
-
str.gsub!(%r{[^:]//(.+?[^:])//}, '
|
133
|
+
str.gsub!(%r{([^:])//(.+?[^:])//}, '\1_\2_')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Tables
|
137
|
+
def convert_tables(str)
|
138
|
+
str.gsub!(/^( *\|\|[^\n]+\|\| *[^\n|]+$)+$/, '\1 ||')
|
139
|
+
|
140
|
+
str.gsub!(/(?:^( *\|\|[^\n]+\|\| *)\n?)+/) do |match_result|
|
141
|
+
rows = match_result.gsub("||", "|").split("\n")
|
142
|
+
rows.insert(1, "| #{"--- | " * (rows[0].split("|").size - 1)}".strip)
|
143
|
+
|
144
|
+
"#{rows.join("\n")}\n"
|
145
|
+
end
|
106
146
|
end
|
107
147
|
|
108
148
|
# Links
|
109
|
-
def convert_links(str)
|
110
|
-
str
|
149
|
+
def convert_links(str, git_repo)
|
150
|
+
convert_camel_case_links(str, git_repo)
|
151
|
+
convert_double_bracket_wiki_links(str, git_repo)
|
152
|
+
convert_single_bracket_wiki_links(str, git_repo)
|
153
|
+
|
154
|
+
str.gsub!(/(^!)\[(http[^\s\[\]]+)\s([^\[\]]+)\]/, '[\2](\1)')
|
111
155
|
str.gsub!(/!(([A-Z][a-z0-9]+){2,})/, '\1')
|
112
156
|
end
|
113
157
|
|
158
|
+
def convert_single_bracket_wiki_links(str, git_repo)
|
159
|
+
str.gsub!(/(!?)\[((?:wiki|source):)?([^\s\]]*) ?(.*?)\]/) do |match_result|
|
160
|
+
source = Regexp.last_match[2]
|
161
|
+
path = Regexp.last_match[3]
|
162
|
+
name = Regexp.last_match[4]
|
163
|
+
|
164
|
+
formatted_link(
|
165
|
+
match_result,
|
166
|
+
git_repo,
|
167
|
+
{ source: source, path: path, name: name }
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def convert_double_bracket_wiki_links(str, git_repo)
|
173
|
+
str.gsub!(/(!?)\[\[((?:wiki|source):)?([^|\n]*)\|?(.*?)\]\]/) do |match_result|
|
174
|
+
source = Regexp.last_match[2]
|
175
|
+
path = Regexp.last_match[3]
|
176
|
+
name = Regexp.last_match[4]
|
177
|
+
|
178
|
+
formatted_link(
|
179
|
+
match_result,
|
180
|
+
git_repo,
|
181
|
+
{ source: source, path: path, name: name }
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def formatted_link(unformatted_text, git_repo, url_options = {})
|
187
|
+
return unformatted_text.gsub("!", "{~") if unformatted_text.start_with?("!")
|
188
|
+
|
189
|
+
if url_options[:source] == "wiki:"
|
190
|
+
link, internal_link = url_options[:path].split("#")
|
191
|
+
link = "Home" if link == @home_page_name
|
192
|
+
internal_link = Tractive::Utilities.dasharize(internal_link) if internal_link
|
193
|
+
url_options[:name] = link if url_options[:name].empty?
|
194
|
+
"{{#{url_options[:name]}}}(https://github.com/#{git_repo}/wiki/#{link}##{internal_link})"
|
195
|
+
elsif url_options[:source] == "source:"
|
196
|
+
url_options[:name] = url_options[:path] if url_options[:name].empty?
|
197
|
+
"{{#{url_options[:name]}}}(https://github.com/#{git_repo}/#{source_git_path(url_options[:path])})"
|
198
|
+
elsif url_options[:path].start_with?("http")
|
199
|
+
url_options[:name] = url_options[:path] if url_options[:name].empty?
|
200
|
+
"{{#{url_options[:name]}}}(#{url_options[:path]})"
|
201
|
+
else
|
202
|
+
unformatted_text
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def source_git_path(trac_path)
|
207
|
+
trac_path = trac_path.gsub("trunk/", "main/")
|
208
|
+
trac_path = trac_path.delete_prefix("/").delete_suffix("/")
|
209
|
+
|
210
|
+
return "" if trac_path.empty?
|
211
|
+
|
212
|
+
uri = URI.parse(trac_path)
|
213
|
+
|
214
|
+
trac_path = uri.path
|
215
|
+
line_number = uri.fragment
|
216
|
+
trac_path, revision = trac_path.split("@")
|
217
|
+
|
218
|
+
if trac_path.split("/").count <= 1
|
219
|
+
wiki_path(trac_path)
|
220
|
+
else
|
221
|
+
unless trac_path.start_with?("tags")
|
222
|
+
params = CGI.parse(uri.query || "")
|
223
|
+
revision ||= params["rev"].first
|
224
|
+
|
225
|
+
# TODO: Currently @ does not work with file paths except for main branch
|
226
|
+
sha = @revmap[revision]&.strip
|
227
|
+
|
228
|
+
trac_path = if sha && file?(trac_path)
|
229
|
+
trac_path.gsub("main/", "#{sha}/")
|
230
|
+
else
|
231
|
+
sha || trac_path
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
wiki_path(trac_path.delete_prefix("tags/"), line_number)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def index_paths
|
240
|
+
@index_paths ||= {
|
241
|
+
"tags" => "tags",
|
242
|
+
"tags/" => "tags",
|
243
|
+
"branch" => "branches/all"
|
244
|
+
}
|
245
|
+
end
|
246
|
+
|
247
|
+
def file?(trac_path)
|
248
|
+
return false unless trac_path
|
249
|
+
|
250
|
+
@wiki_extensions.any? { |extension| trac_path.end_with?(extension) }
|
251
|
+
end
|
252
|
+
|
253
|
+
def wiki_path(path, line_number = "")
|
254
|
+
# TODO: This will not work for folders given in the source_folder parameter and
|
255
|
+
# will not work for subfolders paths like `personal/rjs` unless given in the parameters.
|
256
|
+
return "branches/all?query=#{path}" if @source_folders.any? { |folder| folder == path }
|
257
|
+
return index_paths[path] if index_paths[path]
|
258
|
+
|
259
|
+
prefix = if file?(path)
|
260
|
+
"blob"
|
261
|
+
else
|
262
|
+
"tree"
|
263
|
+
end
|
264
|
+
|
265
|
+
"#{prefix}/#{path}#{"#" unless line_number.to_s.empty?}#{line_number}"
|
266
|
+
end
|
267
|
+
|
268
|
+
# CamelCase page names follow these rules:
|
269
|
+
# 1. The name must consist of alphabetic characters only;
|
270
|
+
# no digits, spaces, punctuation or underscores are allowed.
|
271
|
+
# 2. A name must have at least two capital letters.
|
272
|
+
# 3. The first character must be capitalized.
|
273
|
+
# 4. Every capital letter must be followed by one or more lower-case letters.
|
274
|
+
# 5. The use of slash ( / ) is permitted in page names, where it typically represents a hierarchy.
|
275
|
+
def convert_camel_case_links(str, git_repo)
|
276
|
+
name_regex = %r{(^| )(!?)(/?[A-Z][a-z]+(/?[A-Z][a-z]+)+/?)}
|
277
|
+
wiki_pages_names = Tractive::Wiki.select(:name).distinct.map(:name)
|
278
|
+
str.gsub!(name_regex) do
|
279
|
+
start = Regexp.last_match[2]
|
280
|
+
name = Regexp.last_match[3]
|
281
|
+
|
282
|
+
wiki_link = if start != "!" && wiki_pages_names.include?(name)
|
283
|
+
make_wiki_link(name, git_repo)
|
284
|
+
else
|
285
|
+
name
|
286
|
+
end
|
287
|
+
|
288
|
+
"#{Regexp.last_match[1]}#{wiki_link}"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def make_wiki_link(wiki_name, git_repo)
|
293
|
+
wiki_name = "Home" if wiki_name == @home_page_name
|
294
|
+
"[#{wiki_name}](https://github.com/#{git_repo}/wiki/#{wiki_name})"
|
295
|
+
end
|
296
|
+
|
114
297
|
def convert_image(str, base_url, attach_url, wiki_attachments_url)
|
115
298
|
# https://trac.edgewall.org/wiki/WikiFormatting#Images
|
116
299
|
# [[Image(picture.gif)]] Current page (Ticket, Wiki, Comment)
|
@@ -118,28 +301,37 @@ module Migrator
|
|
118
301
|
# [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
|
119
302
|
|
120
303
|
image_regex = /\[\[Image\((?:(?<module>(?:source|wiki)):)?(?<path>[^)]+)\)\]\]/
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
304
|
+
|
305
|
+
str.gsub!(image_regex) do
|
306
|
+
path = Regexp.last_match[:path]
|
307
|
+
mod = Regexp.last_match[:module]
|
308
|
+
|
309
|
+
converted_image = if mod == "source"
|
310
|
+
"!{{#{path.split("/").last}}}(#{base_url}#{path})"
|
311
|
+
elsif mod == "wiki"
|
312
|
+
id, file = path.split(":")
|
313
|
+
upload_path = "#{wiki_attachments_url}/#{Tractive::Utilities.attachment_path(id, file, hashed: @attach_hashed)}"
|
314
|
+
"!{{#{file}}}(#{upload_path})"
|
315
|
+
elsif path.start_with?("http")
|
316
|
+
# [[Image(http://example.org/s.jpg)]]
|
317
|
+
"!{{#{path}}}(#{path})"
|
318
|
+
else
|
319
|
+
_, id, file = path.split(":")
|
320
|
+
file_path = "#{attach_url}/#{Tractive::Utilities.attachment_path(id, file, hashed: @attach_hashed)}"
|
321
|
+
"!{{#{path}}}(#{file_path})"
|
322
|
+
end
|
323
|
+
|
324
|
+
# There are also ticket references in the format of ticket:1 so
|
325
|
+
# changing this now and will revert it at the end again
|
326
|
+
converted_image.gsub(/ticket:(\d+)/, 'ImageTicket~\1')
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def revert_intermediate_references(str)
|
331
|
+
str.gsub!(/ImageTicket~(\d)/, 'ticket:\1')
|
332
|
+
str.gsub!("{{", "[")
|
333
|
+
str.gsub!("}}", "]")
|
334
|
+
str.gsub!(/(\{~)*/, "")
|
143
335
|
end
|
144
336
|
end
|
145
337
|
end
|
@@ -27,7 +27,7 @@ module Migrator
|
|
27
27
|
input_file_name = args[:opts][:importfromfile]
|
28
28
|
|
29
29
|
@filter_applied = args[:opts][:filter]
|
30
|
-
@filter_options = { column_name: args[:opts][:columnname], operator: args[:opts][:operator], column_value: args[:opts][:columnvalue] }
|
30
|
+
@filter_options = { column_name: args[:opts][:columnname], operator: args[:opts][:operator], column_value: args[:opts][:columnvalue], include_null: args[:opts][:includenull] }
|
31
31
|
|
32
32
|
@trac = Tractive::Trac.new(db)
|
33
33
|
@repo = github["repo"]
|
@@ -96,7 +96,7 @@ module Migrator
|
|
96
96
|
|
97
97
|
def mock_ticket_details(ticket_id)
|
98
98
|
summary = if @filter_applied
|
99
|
-
"
|
99
|
+
"Placeholder issue #{ticket_id} created to align Github issue and trac ticket numbers during migration."
|
100
100
|
else
|
101
101
|
"DELETED in trac #{ticket_id}"
|
102
102
|
end
|
@@ -105,7 +105,8 @@ module Migrator
|
|
105
105
|
summary: summary,
|
106
106
|
time: Time.now.to_i,
|
107
107
|
status: "closed",
|
108
|
-
reporter: "tractive"
|
108
|
+
reporter: "tractive",
|
109
|
+
closed_at: Time.at(0).utc
|
109
110
|
}
|
110
111
|
end
|
111
112
|
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
|
5
|
+
module Migrator
|
6
|
+
module Wikis
|
7
|
+
class MigrateFromDb
|
8
|
+
def initialize(args)
|
9
|
+
$logger.debug("OPTIONS = #{args}")
|
10
|
+
|
11
|
+
@config = args[:cfg]
|
12
|
+
@options = args[:opts]
|
13
|
+
@authors_map = @config["users"].to_h
|
14
|
+
|
15
|
+
@tracticketbaseurl = @config["trac"]["ticketbaseurl"]
|
16
|
+
@git_repo = @config["github"]["repo"]
|
17
|
+
@changeset_base_url = @config["trac"]["changeset_base_url"] || ""
|
18
|
+
@revmap_path = @config["revmap_path"]
|
19
|
+
@attachments_hashed = @config.dig("wiki", "attachments", "hashed")
|
20
|
+
|
21
|
+
@wiki_attachments_url = @options["attachment-base-url"] || @config.dig("wiki", "attachments", "url") || ""
|
22
|
+
@repo_path = @options["repo-path"] || ""
|
23
|
+
@home_page_name = @options["home-page-name"]
|
24
|
+
@wiki_extensions = @options["wiki-extensions"]
|
25
|
+
@source_folders = @options["source-folders"]
|
26
|
+
|
27
|
+
@attachment_options = {
|
28
|
+
hashed: @attachments_hashed
|
29
|
+
}
|
30
|
+
|
31
|
+
verify_options
|
32
|
+
verify_locations
|
33
|
+
|
34
|
+
@twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(
|
35
|
+
@tracticketbaseurl,
|
36
|
+
@attachment_options,
|
37
|
+
@changeset_base_url,
|
38
|
+
@wiki_attachments_url,
|
39
|
+
@revmap_path,
|
40
|
+
git_repo: @git_repo,
|
41
|
+
home_page_name: @home_page_name,
|
42
|
+
wiki_extensions: @wiki_extensions,
|
43
|
+
source_folders: @source_folders
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def migrate_wikis
|
48
|
+
$logger.info("Processing the wiki...")
|
49
|
+
|
50
|
+
Dir.chdir(@options["repo-path"]) do
|
51
|
+
# For every version of every file in the wiki...
|
52
|
+
Tractive::Wiki.for_migration.each do |wiki|
|
53
|
+
next if skip_file(wiki[:name])
|
54
|
+
|
55
|
+
comment = if wiki[:comment].nil? || wiki[:comment].empty?
|
56
|
+
"Initial load of version #{wiki[:version]} of trac-file #{wiki[:name]}"
|
57
|
+
else
|
58
|
+
wiki[:comment].gsub('"', '\"')
|
59
|
+
end
|
60
|
+
|
61
|
+
file_name = filename_for_wiki(wiki)
|
62
|
+
|
63
|
+
$logger.info("Working with file [#{file_name}]")
|
64
|
+
$logger.debug("Object: #{wiki}")
|
65
|
+
|
66
|
+
wiki_markdown_text = @twf_to_markdown.convert(wiki[:text])
|
67
|
+
wiki_markdown_text += wiki_attachments(wiki)
|
68
|
+
|
69
|
+
# Create file with content
|
70
|
+
File.open(file_name, "w") do |f|
|
71
|
+
f.puts(wiki_markdown_text)
|
72
|
+
end
|
73
|
+
|
74
|
+
# git-add it
|
75
|
+
unless execute_command("git add #{file_name}").success?
|
76
|
+
$logger.error("ERROR at git-add #{file_name}!!!")
|
77
|
+
exit(1)
|
78
|
+
end
|
79
|
+
|
80
|
+
author = generate_author(wiki[:author])
|
81
|
+
|
82
|
+
# git-commit it
|
83
|
+
commit_command = "git commit --allow-empty -m \"#{comment}\" --author \"#{author}\" --date \"#{wiki[:fixeddate]}\""
|
84
|
+
unless execute_command(commit_command).success?
|
85
|
+
$logger.error("ERROR at git-commit #{file_name}!!!")
|
86
|
+
exit(1)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def filename_for_wiki(wiki)
|
95
|
+
return "Home.md" if @home_page_name == wiki[:name]
|
96
|
+
|
97
|
+
"#{cleanse_filename(wiki[:name])}.md"
|
98
|
+
end
|
99
|
+
|
100
|
+
def verify_options
|
101
|
+
$logger.info("Verifying options...")
|
102
|
+
|
103
|
+
missing_options = []
|
104
|
+
missing_options << "attachment-base-url" if @wiki_attachments_url.empty?
|
105
|
+
missing_options << "repo-path" if @repo_path.empty?
|
106
|
+
|
107
|
+
return if missing_options.empty?
|
108
|
+
|
109
|
+
$logger.error("Following options are missing: #{missing_options} - exiting...")
|
110
|
+
exit(1)
|
111
|
+
end
|
112
|
+
|
113
|
+
def verify_locations
|
114
|
+
$logger.info("Verifying locations...")
|
115
|
+
missing_directories = []
|
116
|
+
|
117
|
+
# git-root exists?
|
118
|
+
missing_directories << "repo-path" unless Dir.exist?(@repo_path)
|
119
|
+
|
120
|
+
return if missing_directories.empty?
|
121
|
+
|
122
|
+
$logger.error("Following directories are missing: #{missing_directories} - exiting ...")
|
123
|
+
exit(1)
|
124
|
+
end
|
125
|
+
|
126
|
+
def cleanse_filename(name)
|
127
|
+
# Get rid of 'magic' characters from potential filename - replace with '_'
|
128
|
+
# Magic: [ /<>- ]
|
129
|
+
name.gsub(%r{[/<>-]}, "_")
|
130
|
+
end
|
131
|
+
|
132
|
+
def skip_file(file_name)
|
133
|
+
file_name.start_with?("Trac") || (file_name.start_with?("Wiki") && !file_name.start_with?("WikiStart"))
|
134
|
+
end
|
135
|
+
|
136
|
+
def generate_author(author)
|
137
|
+
return "" if author.nil? || author.empty?
|
138
|
+
|
139
|
+
author_name = @authors_map[author]&.[]("name") || author.split("@").first
|
140
|
+
author_email = @authors_map[author]&.[]("email") || author
|
141
|
+
|
142
|
+
"#{author_name} <#{author_email}>"
|
143
|
+
end
|
144
|
+
|
145
|
+
def wiki_attachments(wiki)
|
146
|
+
attachments = wiki.attachments
|
147
|
+
return "" if attachments.count.zero?
|
148
|
+
|
149
|
+
attachments_list = ["# Attachments\n"]
|
150
|
+
|
151
|
+
attachments.each do |attachment|
|
152
|
+
attachment_path = Tractive::Utilities.attachment_path(
|
153
|
+
wiki.name, attachment.filename, hashed: @attachments_hashed
|
154
|
+
)
|
155
|
+
attachments_list << "- [#{attachment.filename}](#{@wiki_attachments_url}/#{attachment_path})"
|
156
|
+
end
|
157
|
+
|
158
|
+
attachments_list.join("\n")
|
159
|
+
end
|
160
|
+
|
161
|
+
def execute_command(command)
|
162
|
+
`#{command}`
|
163
|
+
$CHILD_STATUS
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/tractive/migrator.rb
CHANGED
@@ -19,14 +19,18 @@ module Tractive
|
|
19
19
|
def filter_column(options)
|
20
20
|
return self if options.nil? || options.values.compact.empty?
|
21
21
|
|
22
|
-
case options[:operator].downcase
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
22
|
+
query = case options[:operator].downcase
|
23
|
+
when "like"
|
24
|
+
Sequel.like(options[:column_name].to_sym, options[:column_value])
|
25
|
+
when "not like"
|
26
|
+
~Sequel.like(options[:column_name].to_sym, options[:column_value])
|
27
|
+
else
|
28
|
+
Sequel.lit("#{options[:column_name]} #{options[:operator]} '#{options[:column_value]}'")
|
29
|
+
end
|
30
|
+
|
31
|
+
query = Sequel.|(query, { options[:column_name].to_sym => nil }) if options[:include_null]
|
32
|
+
|
33
|
+
where { query }
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
@@ -35,5 +39,9 @@ module Tractive
|
|
35
39
|
change_arr = changes + attachments
|
36
40
|
change_arr.sort_by { |change| change[:time] }
|
37
41
|
end
|
42
|
+
|
43
|
+
def closed_comments
|
44
|
+
changes_dataset.where(field: "status", newvalue: "closed")
|
45
|
+
end
|
38
46
|
end
|
39
47
|
end
|