tractive 1.0.8 → 1.0.12
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 +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" 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
|