missue 1.0.2

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/bin/missue.rb +400 -0
  3. metadata +46 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fe559dea3d1c0dcb0acb013af221bbdc7ad57b294b332fa843bf3a6e8a26d674
4
+ data.tar.gz: 3c8e1d522d0d0f6ca058f1c6b986b89ac99e1a481fa623350019099f5194e183
5
+ SHA512:
6
+ metadata.gz: 7effa903cc1c278d319bd27f03ae7f761fe7e2a278a86a70665e6065319a0443aa18305e8002a8c9c64d84ed95e9776ebe7b4df0b55c1aad66a7dda1a696d304
7
+ data.tar.gz: 2c4fd26d7b741b4ca0e0c50fc2744de21a48b3ab67858be6ef373b41b34e36141b99b513a17d4e7f0d4b7ef410e6e1e06b7b0babd2af25c3f76c3aa9f76d593b
data/bin/missue.rb ADDED
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env ruby
2
+ # gh-missue.rb -- A GitHub issue migration script written in Ruby
3
+ #==================================================================================================
4
+ # Author: E:V:A
5
+ # Date: 2018-04-10
6
+ # Change: 2022-01-25
7
+ # Version: 1.0.2
8
+ # License: ISC
9
+ # Formatting UTF-8 with 4-space TAB stops and no TAB chars.
10
+ # URL: https://github.com/E3V3A/gh-missue
11
+ # Based On: https://github.com/TimothyBritt/github-issue-migrate
12
+ #
13
+ # Description: A Ruby script for migrating selected GitHub issues to your own repository and
14
+ # using OAuth2 authentication to increase speed and prevent rate limiting.
15
+ #
16
+ # Dependencies:
17
+ # [1] docopt https://github.com/docopt/docopt.rb/ # option parser
18
+ # [2] octokit https://github.com/octokit/octokit.rb/ # GitHubs API library
19
+ #
20
+ # NOTE:
21
+ #
22
+ # 1. To make this run, you need to install Ruby with:
23
+ # (a) winget install ruby
24
+ # (b) gem install octokit
25
+ # (c) gem install docopt
26
+ #
27
+ # 2. Clone latest version of this file
28
+ #
29
+ # 3. You should also consider creating a personal authentication token on GitHub,
30
+ # to avoid getting rate-limited by a large number of requests in short time.
31
+ #
32
+ # ToDo:
33
+ #
34
+ # [ ] Add -a option to NOT copy original author & URL into migrated issue
35
+ # [ ] Fix username/password authentication ?? (Maybe Deprecated?)
36
+ # [ ] Check environment variable for OAUTH token:
37
+ # access_token = "#{ENV['GITHUB_OAUTH_TOKEN']}"
38
+ # [ ] Fix inclusion of CLI options: -d, -n
39
+ # -d - show debug info with full option list, raw requests & responses etc.
40
+ # -n <1,3-6,9> - only migrate issues given by the list. Can be a range.
41
+ # [/] Fix new Authentication issues
42
+ # [ ] Make the issue vs PR selection smarter!
43
+ # - Now it just takes ALL and filters using list_source_issues()
44
+ # [ ] ? Add <type> option to selec pr, vs issue: '-p <type>' where <type> = [issue, pr]
45
+ #
46
+ # References:
47
+ #
48
+ # [1] https://developer.github.com/changes/2020-02-10-deprecating-auth-through-query-param/
49
+ # [2] https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow
50
+ # [3]
51
+ #
52
+ #==================================================================================================
53
+ require 'docopt'
54
+ require 'octokit'
55
+ require 'net/http'
56
+ require 'json'
57
+
58
+ VERSION = '1.0.2'
59
+ options = {}
60
+
61
+ #--------------------------------------------------------------------------------------------------
62
+ # The cli options parser
63
+ #--------------------------------------------------------------------------------------------------
64
+ doc = <<DOCOPT
65
+
66
+ Description:
67
+
68
+ gh-missue is a Ruby program that migrate issues from one github repository to another.
69
+ Please note that you can only migrate issues to your own repo, unless you have an OAuth2
70
+ authentication token.
71
+
72
+ Usage:
73
+ #{__FILE__} [-c | -n <ilist> | -t <itype>] <source_repo> <target_repo>
74
+ #{__FILE__} [-c | -n <ilist> | -t <itype>] <oauth2_token> <source_repo> <target_repo>
75
+ #{__FILE__} [-c | -n <ilist> | -t <itype>] <username> <password> <source_repo> <target_repo>
76
+ #{__FILE__} [-d] -l <itype> [<oauth2_token>] <repo>
77
+ #{__FILE__} -n <ilist>
78
+ #{__FILE__} -t <itype>
79
+ #{__FILE__} [-d] -r [<oauth2_token>]
80
+ #{__FILE__} -d
81
+ #{__FILE__} -v
82
+ #{__FILE__} -h
83
+
84
+ Options:
85
+
86
+ -c - only copy all issue labels from <source> to <target> repos, including name, color and description
87
+ -l <itype> <repo> - list available issues of type <itype> (all,open,closed) and all labels in repository <repo>
88
+ -t <itype> - specify what type (all,open,closed) of issues to migrate. [default: open]
89
+ -r - show current rate limit and authentication method for your IP
90
+ -d - show debug info with full option list, raw requests & responses etc.
91
+ -n <ilist> - only migrate issues with comma separated numbers given by the list. Can include a range.
92
+ -h, --help - show this help message and exit
93
+ -v, --version - show version and exit
94
+
95
+ Examples:
96
+
97
+ #{__FILE__} -r
98
+ #{__FILE__} -l open E3V3A/MMM-VOX
99
+ #{__FILE__} -t closed "E3V3A/TESTO" "USERNAME/REPO"
100
+ #{__FILE__} -n 1,4-5 "E3V3A/TESTO" "USERNAME/REPO"
101
+
102
+ Dependencies:
103
+ #{__FILE__} depends on the following gem packages: octokit, docopt.
104
+
105
+ DOCOPT
106
+
107
+ #--------------------------------------------------------------------------------------------------
108
+ # The IssueMigrator
109
+ #--------------------------------------------------------------------------------------------------
110
+ class IssueMigrator
111
+
112
+ # attr_accessor :issues, :client, :target_repo, :source_repo
113
+ attr_accessor :access_token, :issues, :ilist, :itype, :client, :target_repo, :source_repo
114
+
115
+ def hex2rgb(hex)
116
+ # Usage: print hex2rgb("d73a4a") - prints a RGB colored box from hex
117
+ r,g,b = hex.match(/^(..)(..)(..)$/).captures.map(&:hex)
118
+ s = "\e[48;2;#{r};#{g};#{b}m \e[0m"
119
+ end
120
+
121
+ # curl -v -H "Authorization: token <MY-40-CHAR-TOKEN>" \
122
+ # -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/E3V3A/gh-missue/issues
123
+ def initialize(access_token, source_repo, target_repo)
124
+ @client = Octokit::Client.new(
125
+ :access_token => access_token,
126
+ :accept => 'application/vnd.github.v3+json',
127
+ :headers => { "Authorization" => "token " + access_token },
128
+ # :headers => { "X-GitHub-OTP" => "<your 2FA token>" }
129
+
130
+ # // Personal OAuth2 Access Token
131
+ #:access_token => "YOUR_40_CHAR_OATH2_TOKEN"
132
+
133
+ # // OAuth2 App Credentials (DEPRECATED!)
134
+ # NEW use:
135
+ # curl -u my_client_id:my_client_secret https://api.github.com/user/repos
136
+ #:client_id => "<YOUR_20_CHAR_OATH2_ID>",
137
+ #:client_secret => "<YOUR_40_CHAR_OATH2_SECRET>"
138
+
139
+ per_page: 100
140
+ )
141
+ user = client.user
142
+ user.login
143
+ @source_repo = source_repo
144
+ @target_repo = target_repo
145
+ @itype = itype
146
+ end
147
+
148
+ def pull_source_issues(itype) # ilist => nil ??
149
+ @client.auto_paginate = true
150
+ @issues = @client.issues(@source_repo, :state => itype) # The issue type: <itype>: [open/closed/all]
151
+ # @issues = @client.issues(@source_repo, :issue => ilist) # The issue list: <ilist>: "1,2,5-7,19"
152
+ puts "Found #{issues.size} issues of type: #{itype}\n"
153
+ end
154
+
155
+ def list_source_issues(itype)
156
+ pull_source_issues(itype)
157
+ @issues.each do |source_issue|
158
+ #puts "[#{source_issue.number}]\t #{source_issue.title}"
159
+ puts "[#{source_issue.number}]".ljust(10) + "#{source_issue.title}"
160
+ end
161
+ puts
162
+ end
163
+
164
+ def list_source_labels
165
+ @client.auto_paginate = true
166
+ @labels = @client.labels(@source_repo.freeze, accept: 'application/vnd.github.symmetra-preview+json')
167
+ puts "Found #{@labels.size} issue labels:"
168
+ # ToDo: check and handle length (in case > 20)
169
+ @labels.each do |label|
170
+ # ToDo: Add " " colored "boxes" using the color of the tag.
171
+ color_box = hex2rgb("#{label.color}") + " "
172
+ #puts "[#{label.color}] " + "#{label.name}".ljust(20) + ": #{label.description}"
173
+ puts "[#{label.color}] " + color_box + "#{label.name}".ljust(20) + ": #{label.description}"
174
+ end
175
+ puts
176
+ end
177
+
178
+ def create_target_labels
179
+ @client.auto_paginate = true
180
+ @source_labels = @client.labels(@source_repo.freeze, accept: 'application/vnd.github.symmetra-preview+json')
181
+ # @target_labels = @client.add_label(@target_repo.freeze, accept: 'application/vnd.github.symmetra-preview+json')
182
+ puts "Found #{@source_labels.size} issue labels in <source_repo>:"
183
+ puts "Copying labels..."
184
+ tlabel = "" # nil
185
+ @source_labels.each do |lbl|
186
+ # ToDo: Add " " colored "boxes" using the color of the tag.
187
+ #puts "[#{lbl.color}] #{lbl.name} : #{lbl.description}"
188
+ puts "[#{lbl.color}] " + "#{lbl.name}".ljust(20) + ": #{lbl.description}"
189
+ #tlabel = {"name": lbl.name, "description": lbl.description, "color": lbl.color}
190
+ #tlabel = {lbl.name, lbl.color, description: lbl.description}
191
+ #lab = client.add_label(@target_repo.freeze, accept: 'application/vnd.github.symmetra-preview+json', tlabel)
192
+ lab = client.add_label(@target_repo.freeze, lbl.name, lbl.color, accept: 'application/vnd.github.symmetra-preview+json', description: lbl.description)
193
+ sleep(2)
194
+ end
195
+ puts "done."
196
+ end
197
+
198
+ def push_issues
199
+ @issues.reverse!
200
+ n = 0
201
+ @issues.each do |source_issue|
202
+ n += 1
203
+ print "Processing issue: #{source_issue.number} (#{n}/#{issues.size})\r"
204
+ source_labels = get_source_labels(source_issue)
205
+ source_comments = get_source_comments(source_issue)
206
+ if !source_issue.key?(:pull_request) || source_issue.pull_request.empty?
207
+
208
+ # PR#2
209
+ issue_body = "*Originally created by @#{source_issue.user[:login]} (#{source_issue.html_url}):*\n\n#{source_issue.body}"
210
+ target_issue = @client.create_issue(@target_repo, source_issue.title, issue_body, {labels: source_labels})
211
+
212
+ #target_issue = @client.create_issue(@target_repo, source_issue.title, source_issue.body, {labels: source_labels})
213
+
214
+ push_comments(target_issue, source_comments) unless source_comments.empty?
215
+ @client.close_issue(@target_repo, target_issue.number) if source_issue.state === 'closed'
216
+ end
217
+ # We need to set a rate limit, even for OA2, it is 0.5 [req/sec]
218
+ sleep(90) if ( issues.size > 1 ) # [sec]
219
+ end
220
+ puts "\n"
221
+ end
222
+
223
+ # API bug: missing color/description
224
+ def get_source_labels(source_issue)
225
+ labels = []
226
+ source_issue.labels.each do |lbl|
227
+ labels << {"name": lbl.name, "description": lbl.description, "color": lbl.color}
228
+ end
229
+ #puts "Labels: #{labels}"
230
+ labels
231
+ end
232
+
233
+ def get_source_comments(source_issue)
234
+ comments = []
235
+ source_comments = @client.issue_comments(@source_repo, source_issue.number)
236
+ source_comments.each do |cmt|
237
+ comments << cmt.body
238
+ end
239
+ comments
240
+ end
241
+
242
+ def push_comments(target_issue, source_comments)
243
+ source_comments.each do |cmt|
244
+ @client.add_comment(@target_repo, target_issue.number, cmt)
245
+ end
246
+ end
247
+ end
248
+
249
+ #--------------------------------------------------------------------------------------------------
250
+ # MAIN
251
+ #--------------------------------------------------------------------------------------------------
252
+
253
+ #if __FILE__ == $0
254
+ begin
255
+
256
+ hLine = "-"*72
257
+ puts
258
+
259
+ def sort_list(ilist)
260
+ # "12,3-5,2,6,35-38" --> [2,3,4,5,6,12,35,36,37,38]
261
+ ilist.gsub(/(\d+)-(\d+)/) { ($1..$2).to_a.join(',') }.split(',').map(&:to_i).sort.uniq
262
+ end
263
+
264
+ #----------------------------------------------------------------------
265
+ # CLI Options
266
+ #----------------------------------------------------------------------
267
+ options = Docopt::docopt(doc, version: VERSION) # help: true
268
+
269
+ if options['-d']
270
+ debug = true
271
+ #pp Docopt::docopt(doc, version: VERSION)
272
+ puts "\nAvailable options are:\n#{options.inspect}\n"
273
+ puts "\nThe supplied CLI options were:\n#{ARGV.inspect}\n\n"
274
+ end
275
+
276
+ if options['<oauth2_token>']
277
+ access_token = options['<oauth2_token>']
278
+ if access_token.size != 40
279
+ puts "Error: The github access token has to be 40 characters long!"
280
+ puts " (Yours was: #{access_token.size} characters.)"
281
+ exit
282
+ else
283
+ puts "Using access_token: #{access_token}" if options['-d']
284
+ end
285
+ end
286
+
287
+ # -l <itype> <source_repo>
288
+ # https://docs.github.com/en/rest/reference/issues#list-repository-issues
289
+ # GET /repos/{owner}/{repo}/issues
290
+ # curl -v -H "Authorization: token <MY-40-CHAR-TOKEN>" \
291
+ # -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/E3V3A/gh-missue/issues
292
+ if ( options['-l'] )
293
+ itype = options['-l']
294
+ source_repo = options['<repo>']
295
+ target_repo = "E3V3A/TESTT" # a dummy repo
296
+ im = IssueMigrator.new("#{access_token}", "#{source_repo}", "#{target_repo}")
297
+ im.list_source_issues(itype)
298
+ im.list_source_labels
299
+ end
300
+
301
+ # -n <ilist>
302
+ if ( options['-n'] )
303
+ ilist = options['-n']
304
+ puts "The \"-n\" option has not yet been implemented!"
305
+ puts "The supplied issue list: #{ilist}"
306
+ #sorted = ilist.split(",").sort_by(&:to_i)
307
+ sorted = sort_list(ilist)
308
+ puts "The sorted issue list : #{sorted}"
309
+ end
310
+
311
+ # -r
312
+ # https://docs.github.com/en/rest/reference/rate-limit
313
+ # NEW: curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/rate_limit
314
+ # curl -H "Accept: application/vnd.github.v3+json" \
315
+ # -H "Authorization: token <MY-40-CHAR-TOKEN>" https://api.github.com/rate_limit
316
+
317
+ if ( options['-r'] )
318
+
319
+ #access_token = "#{ENV['GITHUB_OAUTH_TOKEN']}"
320
+ #access_token = "<MY-40-CHAR-TOKEN>"
321
+
322
+ url = URI("https://api.github.com/rate_limit")
323
+ http = Net::HTTP.new(url.host, url.port)
324
+ http.use_ssl = true
325
+
326
+ req = Net::HTTP::Get.new(url)
327
+ req["User-Agent"] = "gh-missue"
328
+ req["Accept"] = "application/vnd.github.v3+json"
329
+
330
+ if (access_token)
331
+ puts "Using access_token: #{access_token}"
332
+ req["Authorization"] = "token #{access_token}"
333
+ end
334
+
335
+ res = http.request(req)
336
+
337
+ if (debug)
338
+ puts res.read_body
339
+ end
340
+
341
+ if (res.message != "OK") # 200
342
+ puts "ERROR: Bad reponse code: #{res.code}\n"
343
+ puts res.body
344
+ else
345
+ #debug = false
346
+ if (debug)
347
+ puts hLine + "\nResponse Headers:\n" + hLine
348
+ puts "#{res.to_hash.inspect}\n"
349
+ puts hLine + "\nBody:\n" + hLine
350
+ puts "#{res.body}\n" + hLine
351
+ end
352
+
353
+ #----------------------------------------------------------------------
354
+ # NEW: resources: {core, graphql, integration_manifest, search }
355
+ # (There are more!)
356
+ # Rate Limit Status:
357
+ # core : for all non-search-related resources in the REST API.
358
+ # search : for the Search API.
359
+ # graphql : for the GraphQL API.
360
+ # integration_manifest : for the GitHub App Manifest code conversion endpoint.
361
+ #----------------------------------------------------------------------
362
+ rbr = JSON.parse(res.body)['resources']['core']
363
+ RTc = Time.at(rbr['reset'])
364
+ puts "\nCore"
365
+ puts " Rate limit : #{rbr['limit']}"
366
+ puts " Remaining : #{rbr['remaining']}"
367
+ puts " Refresh at : #{RTc}"
368
+ puts "Search"
369
+ rbs = JSON.parse(res.body)['resources']['search']
370
+ RTs = Time.at(rbs['reset'])
371
+ puts " Search limit : #{rbs['limit']}"
372
+ puts " Remaining : #{rbs['remaining']}"
373
+ puts " Refresh at : #{RTs}"
374
+ end
375
+ end
376
+
377
+ # MAIN
378
+ if ( options['<source_repo>'] and options['<target_repo>'] )
379
+ itype = options['-t']
380
+ #ilist = options['-n']
381
+ #puts "<itype>: #{itype}" # debug
382
+ source_repo = options['<source_repo>']
383
+ target_repo = options['<target_repo>']
384
+ im = IssueMigrator.new("#{access_token}", "#{source_repo}", "#{target_repo}")
385
+ if options['-c']
386
+ im.create_target_labels
387
+ exit
388
+ end
389
+ #exit if options['-c']
390
+ im.pull_source_issues(itype) # add ilist
391
+ #im.list_source_issues(itype) #
392
+ im.push_issues
393
+ end
394
+
395
+ rescue Docopt::Exit => e
396
+ puts e.message
397
+ #puts e.backtrace.inspect
398
+ end
399
+
400
+ puts "\nDone!\n"
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: missue
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - E3V3A
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-01-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This gem will list, migrate, and copy issues across github repos with
14
+ original authors and links.
15
+ email: xdae3v3a@gmail.com
16
+ executables:
17
+ - missue.rb
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/missue.rb
22
+ homepage: https://github.com/E3V3A/gh-missue
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message: Thanks! Now you can migrate like a boss!
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 2.7.0
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements:
41
+ - docopt, oktokit
42
+ rubygems_version: 3.3.3
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: Migrate github issues like a boss!
46
+ test_files: []