tractive 1.0.11 → 1.0.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/README.adoc +8 -0
- data/exe/command_base.rb +2 -0
- data/exe/tractive +8 -0
- data/lib/tractive/attachment_exporter.rb +4 -3
- data/lib/tractive/github_api/graph_ql_client/issues.rb +29 -0
- data/lib/tractive/github_api/graph_ql_client.rb +31 -0
- data/lib/tractive/github_api.rb +1 -0
- data/lib/tractive/info.rb +13 -0
- data/lib/tractive/main.rb +5 -0
- data/lib/tractive/migrator/converter/trac_to_github.rb +19 -14
- data/lib/tractive/migrator/converter/twf_to_markdown.rb +237 -38
- data/lib/tractive/migrator/engine/migrate_from_db.rb +41 -0
- data/lib/tractive/migrator/engine.rb +2 -0
- data/lib/tractive/migrator/wikis/migrate_from_db.rb +27 -5
- data/lib/tractive/utilities.rb +4 -0
- data/lib/tractive/version.rb +1 -1
- data/tractive.gemspec +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8cde84b7e5731a9be33bbbdecc11abacca397665fa29e4734c01bc50c1823c46
|
4
|
+
data.tar.gz: e38bd03de9ed35f8bde8c371c30789815f9fbecbe9b5cb8535c1585ad9f2e4e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e5fd7afc6dfab34b8514a3fa0d12eec6d98d697938a500f7142891c33fc5a4d5f32442da7503be59b112c361d7010e617fb6dab9f9f90b65d0917a98bded050
|
7
|
+
data.tar.gz: 2c0247ec8af3ac372f4244d9c8ac0c2b2dc0a1358f5be3d7619507640404b8db088b03e9c884fa3c4612ae160bfa97828fa41992c2d31e7470a45cdb736342dd
|
data/.rubocop.yml
CHANGED
@@ -18,6 +18,7 @@ Metrics/ClassLength:
|
|
18
18
|
|
19
19
|
Metrics/BlockLength:
|
20
20
|
Max: 500
|
21
|
+
IgnoredMethods: ['describe']
|
21
22
|
|
22
23
|
Style/Documentation:
|
23
24
|
Enabled: false
|
@@ -35,7 +36,10 @@ Metrics/MethodLength:
|
|
35
36
|
Enabled: false
|
36
37
|
|
37
38
|
Metrics/ModuleLength:
|
38
|
-
Max:
|
39
|
+
Max: 250
|
40
|
+
|
41
|
+
Metrics/ParameterLists:
|
42
|
+
Max: 6
|
39
43
|
|
40
44
|
Security/Open:
|
41
45
|
Enabled: false
|
data/README.adoc
CHANGED
@@ -802,6 +802,14 @@ If attachment files are reachable via a URL we reference this here.
|
|
802
802
|
| Path to config file.
|
803
803
|
| String
|
804
804
|
|
805
|
+
| `-e`, `--wiki-extensions`
|
806
|
+
| Space separated list of extensions or filenames (if the file don't have an extension). This is required to convert file SVN source links to Github links. This is used to determine if a path belongs to a file or a directory.
|
807
|
+
| Array
|
808
|
+
|
809
|
+
| `-f`, `--source-folders`
|
810
|
+
| Space separated list of non standard folders in SVN that are used to find if a path is complete or partial.
|
811
|
+
| Array
|
812
|
+
|
805
813
|
|===
|
806
814
|
|
807
815
|
|
data/exe/command_base.rb
CHANGED
@@ -7,5 +7,7 @@ class CommandBase < Thor
|
|
7
7
|
desc: "Name of the logfile to output logs to."
|
8
8
|
class_option "config", type: :string, default: "tractive.config.yaml", banner: "<PATH>", aliases: "-c",
|
9
9
|
desc: "Set the configuration file"
|
10
|
+
class_option "git-token", type: :string,
|
11
|
+
desc: "The access token for Github actions."
|
10
12
|
class_option "verbose", type: :boolean, aliases: ["-v", "--verbose"], desc: "Verbose mode"
|
11
13
|
end
|
data/exe/tractive
CHANGED
@@ -47,6 +47,8 @@ class TractiveCommand < CommandBase
|
|
47
47
|
desc: "Put all issue comments in the first message."
|
48
48
|
method_option "start", type: :numeric, aliases: ["-s", "--start-at"], banner: "<ID>",
|
49
49
|
desc: "Start migration from ticket with number <ID>"
|
50
|
+
method_option "make-owners-labels", type: :boolean,
|
51
|
+
desc: "If true, this will make a tag like `owner:<owner name>` and add it to the issue."
|
50
52
|
def migrate_tickets
|
51
53
|
Tractive::Main.new(options).run
|
52
54
|
end
|
@@ -58,6 +60,12 @@ class TractiveCommand < CommandBase
|
|
58
60
|
desc: "Full path of the Trac sqlite3 database export file"
|
59
61
|
method_option "repo-path", type: :string, aliases: ["-r"], banner: "/GIT/ROOT/DIR",
|
60
62
|
desc: "Full path to the root of the git-repository that is our destination"
|
63
|
+
method_option "home-page-name", type: :string, aliases: ["-h"], default: "WikiStart",
|
64
|
+
desc: "Name of the SVN wiki to map to the home page in Github wiki"
|
65
|
+
method_option "wiki-extensions", type: :array, aliases: ["-e"], default: [".py", "changelog", "expire-ids"],
|
66
|
+
desc: "Array of strings to determinte whether a given path is a file path or a directory in wiki"
|
67
|
+
method_option "source-folders", type: :array, aliases: ["-f"], default: ["personal", "attic", "sprint", "branch/hawk"],
|
68
|
+
desc: "Array of strings to figure out if a path is complete or partial"
|
61
69
|
def migrate_wikis
|
62
70
|
Tractive::Main.new(options).migrate_wikis
|
63
71
|
end
|
@@ -53,9 +53,10 @@ module Tractive
|
|
53
53
|
$logger.info "Saving attachments of ticket #{attachment.id}... "
|
54
54
|
FileUtils.mkdir_p "#{output_dir}/#{attachment.id}"
|
55
55
|
|
56
|
-
File.
|
57
|
-
|
58
|
-
|
56
|
+
File.binwrite(
|
57
|
+
"#{output_dir}/#{attachment.id}/#{attachment.filename}",
|
58
|
+
URI.open(uri_parser.escape("#{trac_url}/#{attachment.id}/#{attachment.filename}")).read
|
59
|
+
)
|
59
60
|
end
|
60
61
|
end
|
61
62
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubApi
|
4
|
+
class GraphQlClient
|
5
|
+
# Methods for the Issues API
|
6
|
+
module Issues
|
7
|
+
DELETE_ISSUE_QUERY = <<~QUERY
|
8
|
+
mutation ($input: DeleteIssueInput!) {
|
9
|
+
deleteIssue(input: $input) {
|
10
|
+
repository {
|
11
|
+
name
|
12
|
+
url
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
QUERY
|
17
|
+
|
18
|
+
def delete_issue(issue_id)
|
19
|
+
variables = {
|
20
|
+
"input" => {
|
21
|
+
"issueId" => issue_id
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
Client.query(DeleteIssueQuery, variables: variables)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "graph_ql_client/issues"
|
4
|
+
|
5
|
+
require "graphql/client"
|
6
|
+
require "graphql/client/http"
|
7
|
+
|
8
|
+
# Service to perform github actions
|
9
|
+
module GithubApi
|
10
|
+
class GraphQlClient
|
11
|
+
include GithubApi::GraphQlClient::Issues
|
12
|
+
|
13
|
+
HttpAdapter = GraphQL::Client::HTTP.new("https://api.github.com/graphql") do
|
14
|
+
attr_writer :token
|
15
|
+
|
16
|
+
def headers(_context)
|
17
|
+
{
|
18
|
+
"Authorization" => "bearer #{@token}"
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.add_constants(token)
|
24
|
+
HttpAdapter.token = token
|
25
|
+
|
26
|
+
GithubApi::GraphQlClient.const_set("Schema", GraphQL::Client.load_schema(HttpAdapter))
|
27
|
+
GithubApi::GraphQlClient.const_set("Client", GraphQL::Client.new(schema: Schema, execute: HttpAdapter))
|
28
|
+
GithubApi::GraphQlClient.const_set("DeleteIssueQuery", Client.parse(DELETE_ISSUE_QUERY))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/tractive/github_api.rb
CHANGED
data/lib/tractive/info.rb
CHANGED
@@ -29,12 +29,25 @@ module Tractive
|
|
29
29
|
priorities = Ticket.distinct.select(:priority).select_map(:priority).compact
|
30
30
|
tracstates = Ticket.distinct.select(:status).select_map(:status).compact
|
31
31
|
|
32
|
+
keywords = Ticket.distinct
|
33
|
+
.select(:keywords)
|
34
|
+
.select_map(:keywords)
|
35
|
+
.map do |keyword|
|
36
|
+
keyword&.split(",")&.map do |k|
|
37
|
+
k.strip.gsub(" ", "_")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
.flatten
|
41
|
+
.uniq
|
42
|
+
.compact
|
43
|
+
|
32
44
|
{
|
33
45
|
"users" => Utilities.make_each_hash(users, %w[email name username]),
|
34
46
|
"milestones" => milestones,
|
35
47
|
"labels" => {
|
36
48
|
"type" => Utilities.make_hash("type_", types),
|
37
49
|
"resolution" => Utilities.make_hash("resolution_", resolutions),
|
50
|
+
"keywords" => Utilities.make_hash("keyword_", keywords),
|
38
51
|
"component" => Utilities.make_each_hash(components, %w[name color], "component: "),
|
39
52
|
"severity" => Utilities.make_each_hash(severity, %w[name color]),
|
40
53
|
"priority" => Utilities.make_each_hash(priorities, %w[name color]),
|
data/lib/tractive/main.rb
CHANGED
@@ -8,6 +8,11 @@ module Tractive
|
|
8
8
|
@opts = opts
|
9
9
|
@cfg = YAML.load_file(@opts[:config])
|
10
10
|
|
11
|
+
@cfg["github"] ||= {}
|
12
|
+
@cfg["github"]["token"] = @opts["git-token"] if @opts["git-token"]
|
13
|
+
|
14
|
+
GithubApi::GraphQlClient.add_constants(@cfg["github"]["token"]) unless @opts[:info]
|
15
|
+
|
11
16
|
Tractive::Utilities.setup_logger(output_stream: @opts[:logfile] || $stderr, verbose: @opts[:verbose])
|
12
17
|
@db = Tractive::Utilities.setup_db!(@opts["trac-database-path"] || @cfg["trac"]["database"])
|
13
18
|
rescue Sequel::DatabaseConnectionError, Sequel::AdapterNotFound, URI::InvalidURIError, Sequel::DatabaseError => e
|
@@ -16,6 +16,7 @@ module Migrator
|
|
16
16
|
@client = GithubApi::Client.new(access_token: args[:cfg]["github"]["token"])
|
17
17
|
@wiki_attachments_url = args[:cfg].dig("wiki", "attachments", "url")
|
18
18
|
@revmap_file_path = args[:opts][:revmapfile] || args[:cfg]["revmap_path"]
|
19
|
+
@make_owners_label = args[:opts]["make-owners-labels"] || args[:cfg]["make_owners_labels"]
|
19
20
|
@attachment_options = {
|
20
21
|
url: @attachurl,
|
21
22
|
hashed: args[:cfg].dig("ticket", "attachments", "hashed")
|
@@ -33,7 +34,8 @@ module Migrator
|
|
33
34
|
@attachment_options,
|
34
35
|
@changeset_base_url,
|
35
36
|
@wiki_attachments_url,
|
36
|
-
@revmap_file_path
|
37
|
+
@revmap_file_path,
|
38
|
+
git_repo: @repo, home_page_name: args[:opts]["home-page-name"]
|
37
39
|
)
|
38
40
|
end
|
39
41
|
|
@@ -100,14 +102,11 @@ module Migrator
|
|
100
102
|
|
101
103
|
labels.delete(nil)
|
102
104
|
|
103
|
-
keywords = ticket[:keywords]
|
104
|
-
|
105
|
-
|
106
|
-
labels.add(@labels_cfg.fetch("keywords", {})[ticket[:keywords].downcase])
|
107
|
-
else
|
108
|
-
badges.add(@labels_cfg.fetch("keywords", {})[ticket[:keywords]])
|
109
|
-
end
|
105
|
+
keywords = ticket[:keywords]&.split(",") || []
|
106
|
+
keywords.each do |keyword|
|
107
|
+
badges.add(@labels_cfg.fetch("keywords", {})[keyword.strip])
|
110
108
|
end
|
109
|
+
|
111
110
|
# If the field is not set, it will be nil and generate an unprocessable json
|
112
111
|
|
113
112
|
milestone = @milestonemap[ticket[:milestone]]
|
@@ -120,6 +119,14 @@ module Migrator
|
|
120
119
|
|
121
120
|
github_assignee = map_assignee(ticket[:owner])
|
122
121
|
|
122
|
+
unless github_assignee.nil? || github_assignee.empty?
|
123
|
+
if @make_owners_label
|
124
|
+
labels.add("name" => "owner:#{github_assignee}")
|
125
|
+
else
|
126
|
+
badges.add("owner:#{github_assignee}")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
123
130
|
badges = badges.to_a.compact.sort
|
124
131
|
badgetable = badges.map { |i| %(`#{i}`) }.join(" ")
|
125
132
|
badgetable += begin
|
@@ -131,8 +138,6 @@ module Migrator
|
|
131
138
|
|
132
139
|
# compose body
|
133
140
|
body = [badgetable, body, footer].join("\n\n___\n")
|
134
|
-
|
135
|
-
labels.add("name" => "owner:#{github_assignee}") unless github_assignee.nil? || github_assignee.empty?
|
136
141
|
labels = labels.map { |label| label["name"] }
|
137
142
|
|
138
143
|
issue = {
|
@@ -264,7 +269,7 @@ module Migrator
|
|
264
269
|
end
|
265
270
|
|
266
271
|
case kind
|
267
|
-
when "owner", "status", "title", "resolution", "priority", "component", "type", "severity", "platform", "milestone"
|
272
|
+
when "owner", "status", "title", "resolution", "priority", "component", "type", "severity", "platform", "milestone", "keywords"
|
268
273
|
old = meta[:oldvalue]
|
269
274
|
new = meta[:newvalue]
|
270
275
|
if old && new
|
@@ -280,7 +285,7 @@ module Migrator
|
|
280
285
|
# text += "created the issue\n\n"
|
281
286
|
if body && !body.lstrip.empty?
|
282
287
|
# text += "\n___\n" if not append
|
283
|
-
text += @twf_to_markdown.convert(body)
|
288
|
+
text += @twf_to_markdown.convert(body, id: meta[:ticket])
|
284
289
|
end
|
285
290
|
|
286
291
|
when "comment"
|
@@ -294,7 +299,7 @@ module Migrator
|
|
294
299
|
end
|
295
300
|
|
296
301
|
text += "\n___\n" unless append
|
297
|
-
text += @twf_to_markdown.convert(body) if body
|
302
|
+
text += @twf_to_markdown.convert(body, id: meta[:ticket]) if body
|
298
303
|
|
299
304
|
when "attachment"
|
300
305
|
text += "_uploaded file "
|
@@ -344,7 +349,7 @@ module Migrator
|
|
344
349
|
end
|
345
350
|
|
346
351
|
def interested_in_change?(kind, newvalue)
|
347
|
-
!(%w[
|
352
|
+
!(%w[cc reporter version].include?(kind) ||
|
348
353
|
(kind == "comment" && (newvalue.nil? || newvalue.lstrip.empty?)))
|
349
354
|
end
|
350
355
|
|
@@ -1,27 +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, attachment_options, changeset_base_url, wiki_attachments_url, revmap_file_path)
|
9
|
+
def initialize(base_url, attachment_options, changeset_base_url, wiki_attachments_url, revmap_file_path, options = {})
|
8
10
|
@base_url = base_url
|
9
11
|
@attach_url = attachment_options[:url]
|
10
12
|
@attach_hashed = attachment_options[:hashed]
|
11
13
|
@changeset_base_url = changeset_base_url
|
12
14
|
@wiki_attachments_url = wiki_attachments_url
|
13
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]
|
20
|
+
@source_folders = options[:source_folders]
|
14
21
|
end
|
15
22
|
|
16
|
-
def convert(str)
|
23
|
+
def convert(str, image_options = {})
|
24
|
+
# Fix 'Windows EOL' to 'Linux EOL'
|
25
|
+
str.gsub!("\r\n", "\n")
|
26
|
+
|
27
|
+
convert_tables(str)
|
17
28
|
convert_newlines(str)
|
29
|
+
convert_comments(str)
|
30
|
+
convert_html_snippets(str)
|
18
31
|
convert_code_snippets(str)
|
19
32
|
convert_headings(str)
|
20
|
-
convert_links(str)
|
33
|
+
convert_links(str, @git_repo)
|
21
34
|
convert_font_styles(str)
|
22
35
|
convert_changeset(str, @changeset_base_url)
|
23
|
-
convert_image(str, @base_url, @attach_url, @wiki_attachments_url)
|
36
|
+
convert_image(str, @base_url, @attach_url, @wiki_attachments_url, image_options)
|
24
37
|
convert_ticket(str, @base_url)
|
38
|
+
revert_intermediate_references(str)
|
25
39
|
|
26
40
|
str
|
27
41
|
end
|
@@ -46,18 +60,12 @@ module Migrator
|
|
46
60
|
revmap
|
47
61
|
end
|
48
62
|
|
49
|
-
# CommitTicketReference
|
50
|
-
def convert_ticket_reference(str)
|
51
|
-
str.gsub!(/\{\{\{\n(#!CommitTicketReference .+?)\}\}\}/m, '\1')
|
52
|
-
str.gsub!(/#!CommitTicketReference .+\n/, "")
|
53
|
-
end
|
54
|
-
|
55
63
|
# Ticket
|
56
64
|
def convert_ticket(str, base_url)
|
57
65
|
# replace a full ticket id with the github short refrence
|
58
66
|
if base_url
|
59
67
|
baseurlpattern = base_url.gsub("/", "\\/")
|
60
|
-
str.gsub!(%r{#{baseurlpattern}/(\d+)}
|
68
|
+
str.gsub!(%r{#{baseurlpattern}/(\d+)}, '#\1')
|
61
69
|
end
|
62
70
|
|
63
71
|
# Ticket
|
@@ -80,16 +88,35 @@ module Migrator
|
|
80
88
|
str.gsub!("\r\n", "\n")
|
81
89
|
end
|
82
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
|
+
|
83
109
|
# Code
|
84
110
|
def convert_code_snippets(str)
|
85
111
|
str.gsub!(/\{\{\{([^\n]+?)\}\}\}/, '`\1`')
|
112
|
+
str.gsub!(/\{\{\{#!(.*?)\n(.+?)\}\}\}/m, "```\\1\n\\2\n```")
|
86
113
|
str.gsub!(/\{\{\{(.+?)\}\}\}/m, '```\1```')
|
87
114
|
str.gsub!(/(?<=```)#!/m, "")
|
88
115
|
end
|
89
116
|
|
90
117
|
# Changeset
|
91
118
|
def convert_changeset(str, changeset_base_url)
|
92
|
-
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?
|
93
120
|
str.gsub!(/\[changeset:"r(\d+)".*\]/, '[changeset:\1]')
|
94
121
|
str.gsub!(/\[changeset:r(\d+)\]/, '[changeset:\1]')
|
95
122
|
str.gsub!(/\br(\d+)\b/) { Tractive::Utilities.map_changeset(Regexp.last_match[1], @revmap, changeset_base_url) }
|
@@ -103,44 +130,216 @@ module Migrator
|
|
103
130
|
def convert_font_styles(str)
|
104
131
|
str.gsub!(/'''(.+?)'''/, '**\1**')
|
105
132
|
str.gsub!(/''(.+?)''/, '*\1*')
|
106
|
-
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
|
107
146
|
end
|
108
147
|
|
109
148
|
# Links
|
110
|
-
def convert_links(str)
|
111
|
-
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)')
|
112
155
|
str.gsub!(/!(([A-Z][a-z0-9]+){2,})/, '\1')
|
113
156
|
end
|
114
157
|
|
115
|
-
def
|
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
|
+
|
297
|
+
def convert_image(str, base_url, attach_url, wiki_attachments_url, options = {})
|
116
298
|
# https://trac.edgewall.org/wiki/WikiFormatting#Images
|
117
299
|
# [[Image(picture.gif)]] Current page (Ticket, Wiki, Comment)
|
118
300
|
# [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
|
119
301
|
# [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
|
120
302
|
|
121
303
|
image_regex = /\[\[Image\((?:(?<module>(?:source|wiki)):)?(?<path>[^)]+)\)\]\]/
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
+
tmp = path.split(":")
|
320
|
+
id, file = case tmp.size
|
321
|
+
when 3
|
322
|
+
[tmp[1], tmp[2]]
|
323
|
+
when 2
|
324
|
+
tmp
|
325
|
+
else
|
326
|
+
[options[:id].to_s, tmp[0]]
|
327
|
+
end
|
328
|
+
file_path = "#{attach_url}/#{Tractive::Utilities.attachment_path(id, file, hashed: @attach_hashed)}"
|
329
|
+
"!{{#{path}}}(#{file_path})"
|
330
|
+
end
|
331
|
+
|
332
|
+
# There are also ticket references in the format of ticket:1 so
|
333
|
+
# changing this now and will revert it at the end again
|
334
|
+
converted_image.gsub(/ticket:(\d+)/, 'ImageTicket~\1')
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def revert_intermediate_references(str)
|
339
|
+
str.gsub!(/ImageTicket~(\d)/, 'ticket:\1')
|
340
|
+
str.gsub!("{{", "[")
|
341
|
+
str.gsub!("}}", "]")
|
342
|
+
str.gsub!(/(\{~)*/, "")
|
144
343
|
end
|
145
344
|
end
|
146
345
|
end
|
@@ -23,6 +23,7 @@ module Migrator
|
|
23
23
|
begin
|
24
24
|
lasttracid = tractickets.last[:id]
|
25
25
|
rescue StandardError
|
26
|
+
delete_mocked_tickets if can_delete_mocked_tickets?
|
26
27
|
raise("trac has no ticket #{start_ticket}")
|
27
28
|
end
|
28
29
|
|
@@ -89,6 +90,46 @@ module Migrator
|
|
89
90
|
|
90
91
|
@last_created_issue = ticket[:id]
|
91
92
|
end
|
93
|
+
|
94
|
+
delete_mocked_tickets if can_delete_mocked_tickets?
|
95
|
+
end
|
96
|
+
|
97
|
+
def can_delete_mocked_tickets?
|
98
|
+
@delete_mocked_tickets
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete_mocked_tickets
|
102
|
+
page = 1
|
103
|
+
issues = @client.issues(@repo, { filter: "all",
|
104
|
+
state: "closed",
|
105
|
+
page: page })
|
106
|
+
|
107
|
+
until issues.empty?
|
108
|
+
deleted = false
|
109
|
+
|
110
|
+
issues.each do |issue|
|
111
|
+
next if issue["title"] != "Placeholder issue #{issue["number"]} created to align Github issue and trac ticket numbers during migration."
|
112
|
+
|
113
|
+
response = @graph_ql_client.delete_issue(issue["node_id"])
|
114
|
+
|
115
|
+
if response.data.errors.any?
|
116
|
+
error_message = response.data
|
117
|
+
.errors
|
118
|
+
.messages
|
119
|
+
.map { |k, v| "#{k}: #{v}" }
|
120
|
+
.join(", ")
|
121
|
+
raise StandardError, error_message
|
122
|
+
end
|
123
|
+
|
124
|
+
deleted = true
|
125
|
+
puts "Successfully deleted issue ##{issue["number"]}, Title: #{issue["title"]}"
|
126
|
+
end
|
127
|
+
|
128
|
+
page += 1 unless deleted
|
129
|
+
issues = @client.issues(@repo, { filter: "all",
|
130
|
+
state: "closed",
|
131
|
+
page: page })
|
132
|
+
end
|
92
133
|
end
|
93
134
|
end
|
94
135
|
end
|
@@ -32,6 +32,7 @@ module Migrator
|
|
32
32
|
@trac = Tractive::Trac.new(db)
|
33
33
|
@repo = github["repo"]
|
34
34
|
@client = GithubApi::Client.new(access_token: github["token"])
|
35
|
+
@graph_ql_client = GithubApi::GraphQlClient.new
|
35
36
|
|
36
37
|
if input_file_name
|
37
38
|
@from_file = input_file_name
|
@@ -62,6 +63,7 @@ module Migrator
|
|
62
63
|
@safetychecks = safetychecks
|
63
64
|
@start_ticket = (start_ticket || (@last_created_issue + 1)).to_i
|
64
65
|
@filter_closed = filter_closed
|
66
|
+
@delete_mocked_tickets = args[:cfg]["ticket"]["delete_mocked"]
|
65
67
|
end
|
66
68
|
|
67
69
|
def migrate
|
@@ -13,12 +13,17 @@ module Migrator
|
|
13
13
|
@authors_map = @config["users"].to_h
|
14
14
|
|
15
15
|
@tracticketbaseurl = @config["trac"]["ticketbaseurl"]
|
16
|
+
@git_repo = @config["github"]["repo"]
|
16
17
|
@changeset_base_url = @config["trac"]["changeset_base_url"] || ""
|
17
|
-
@wiki_attachments_url = @options["attachment-base-url"] || @config.dig("wiki", "attachments", "url") || ""
|
18
|
-
@repo_path = @options["repo-path"] || ""
|
19
18
|
@revmap_path = @config["revmap_path"]
|
20
19
|
@attachments_hashed = @config.dig("wiki", "attachments", "hashed")
|
21
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
|
+
|
22
27
|
@attachment_options = {
|
23
28
|
hashed: @attachments_hashed
|
24
29
|
}
|
@@ -26,7 +31,17 @@ module Migrator
|
|
26
31
|
verify_options
|
27
32
|
verify_locations
|
28
33
|
|
29
|
-
@twf_to_markdown = Migrator::Converter::TwfToMarkdown.new(
|
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
|
+
)
|
30
45
|
end
|
31
46
|
|
32
47
|
def migrate_wikis
|
@@ -43,11 +58,12 @@ module Migrator
|
|
43
58
|
wiki[:comment].gsub('"', '\"')
|
44
59
|
end
|
45
60
|
|
46
|
-
file_name =
|
61
|
+
file_name = filename_for_wiki(wiki)
|
62
|
+
|
47
63
|
$logger.info("Working with file [#{file_name}]")
|
48
64
|
$logger.debug("Object: #{wiki}")
|
49
65
|
|
50
|
-
wiki_markdown_text = @twf_to_markdown.convert(wiki[:text])
|
66
|
+
wiki_markdown_text = @twf_to_markdown.convert(wiki[:text], id: wiki[:name])
|
51
67
|
wiki_markdown_text += wiki_attachments(wiki)
|
52
68
|
|
53
69
|
# Create file with content
|
@@ -75,6 +91,12 @@ module Migrator
|
|
75
91
|
|
76
92
|
private
|
77
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
|
+
|
78
100
|
def verify_options
|
79
101
|
$logger.info("Verifying options...")
|
80
102
|
|
data/lib/tractive/utilities.rb
CHANGED
data/lib/tractive/version.rb
CHANGED
data/tractive.gemspec
CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
+
spec.add_dependency "graphql-client"
|
30
31
|
spec.add_dependency "mysql2"
|
31
32
|
spec.add_dependency "ox"
|
32
33
|
spec.add_dependency "rest-client"
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tractive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphql-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: mysql2
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -145,6 +159,8 @@ files:
|
|
145
159
|
- lib/tractive/github_api/client/issues.rb
|
146
160
|
- lib/tractive/github_api/client/labels.rb
|
147
161
|
- lib/tractive/github_api/client/milestones.rb
|
162
|
+
- lib/tractive/github_api/graph_ql_client.rb
|
163
|
+
- lib/tractive/github_api/graph_ql_client/issues.rb
|
148
164
|
- lib/tractive/graceful_quit.rb
|
149
165
|
- lib/tractive/http/client.rb
|
150
166
|
- lib/tractive/http/client/request.rb
|