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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ebb8048967ee1732b68d54ed6c10798e0959d533e7ed9744a843f1a129f764d
4
- data.tar.gz: 4799587764dcaa848e76971b50df220e72ffa914a5f1bc7c3cad20ef2e5de83d
3
+ metadata.gz: 8cde84b7e5731a9be33bbbdecc11abacca397665fa29e4734c01bc50c1823c46
4
+ data.tar.gz: e38bd03de9ed35f8bde8c371c30789815f9fbecbe9b5cb8535c1585ad9f2e4e5
5
5
  SHA512:
6
- metadata.gz: 0c052a1f936f6e4d4749e4fd9f8ec9fcab3042f4220e2c4f10e6a7ed127f2c1ee8a856f49a58ae8b960a38f12ffdc35d9097a55361142b39bfa605a269ea2d1b
7
- data.tar.gz: 6d0025c7682c74526aa3a0fb8a4d93d4f46fad4cab5e560898e3b2c504e8981cffe10203e59afb7c8d818d3598efe3729b854c2939d7b8ca64105846de55f131
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: 150
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.open("#{output_dir}/#{attachment.id}/#{attachment.filename}", "wb") do |file|
57
- file.write URI.open(uri_parser.escape("#{trac_url}/#{attachment.id}/#{attachment.filename}")).read
58
- end
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
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "github_api/client"
4
+ require_relative "github_api/graph_ql_client"
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
- if keywords
105
- if ticket[:keywords].downcase == "discuss"
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[keywords cc reporter version].include?(kind) ||
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+)}) { "ticket:#{Regexp.last_match[1]}" }
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{[^:]//(.+?[^:])//}, '_\1_')
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.gsub!(/\[(http[^\s\[\]]+)\s([^\[\]]+)\]/, '[\2](\1)')
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 convert_image(str, base_url, attach_url, wiki_attachments_url)
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
- d = image_regex.match(str)
123
- return if d.nil?
124
-
125
- path = d[:path]
126
- mod = d[:module]
127
-
128
- image_path = if mod == "source"
129
- "![#{path.split("/").last}](#{base_url}#{path})"
130
- elsif mod == "wiki"
131
- id, file = path.split(":")
132
- upload_path = "#{wiki_attachments_url}/#{Tractive::Utilities.attachment_path(id, file, hashed: @attach_hashed)}"
133
- "![#{file}](#{upload_path})"
134
- elsif path.start_with?("http")
135
- # [[Image(http://example.org/s.jpg)]]
136
- "![#{d[:path]}](#{d[:path]})"
137
- else
138
- _, id, file = path.split(":")
139
- file_path = "#{attach_url}/#{Tractive::Utilities.attachment_path(id, file, hashed: @attach_hashed)}"
140
- "![#{d[:path]}](#{file_path})"
141
- end
142
-
143
- str.gsub!(image_regex, image_path)
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(@tracticketbaseurl, @attachment_options, @changeset_base_url, @wiki_attachments_url, @revmap_path)
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 = "#{cleanse_filename(wiki[:name])}.md"
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
 
@@ -36,6 +36,10 @@ module Tractive
36
36
  db
37
37
  end
38
38
 
39
+ def dasharize(str)
40
+ str.gsub(/([a-z\d])([A-Z])/, '\1-\2').downcase
41
+ end
42
+
39
43
  def attachment_path(id, filename, options = {})
40
44
  return "#{id}/#{filename}" unless options[:hashed]
41
45
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tractive
4
- VERSION = "1.0.11"
4
+ VERSION = "1.0.15"
5
5
  end
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.11
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: 2021-12-17 00:00:00.000000000 Z
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