wfarr-github 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,486 @@
1
+ DEV_NULL = File.exist?("/dev/null") ? "/dev/null" : "nul:" unless const_defined?("DEV_NULL")
2
+
3
+ helper :user_and_repo_from do |url|
4
+ case url
5
+ when %r|^git://github\.com/([^/]+/[^/]+)$| then $1.split('/')
6
+ when %r|^(?:ssh://)?(?:git@)?github\.com:([^/]+/[^/]+)$| then $1.split('/')
7
+ end
8
+ end
9
+
10
+ helper :user_and_repo_for do |remote|
11
+ user_and_repo_from(url_for(remote))
12
+ end
13
+
14
+ helper :user_for do |remote|
15
+ user_and_repo_for(remote).try.first
16
+ end
17
+
18
+ helper :repo_for do |remote|
19
+ user_and_repo_for(remote).try.last
20
+ end
21
+
22
+ helper :origin do
23
+ orig = `git config --get github.origin`.chomp
24
+ orig = nil if orig.empty?
25
+ orig || 'origin'
26
+ end
27
+
28
+ helper :project do
29
+ repo = repo_for(origin)
30
+ if repo.nil?
31
+ if url_for(origin) == ""
32
+ STDERR.puts "Error: missing remote 'origin'"
33
+ else
34
+ STDERR.puts "Error: remote 'origin' is not a github URL"
35
+ end
36
+ exit 1
37
+ end
38
+ repo.chomp('.git')
39
+ end
40
+
41
+ helper :url_for do |remote|
42
+ `git config --get remote.#{remote}.url`.chomp
43
+ end
44
+
45
+ helper :local_heads do
46
+ `git show-ref --heads --hash`.split("\n")
47
+ end
48
+
49
+ helper :has_commit? do |sha|
50
+ `git show #{sha} >#{DEV_NULL} 2>#{DEV_NULL}`
51
+ $?.exitstatus == 0
52
+ end
53
+
54
+ helper :resolve_commits do |treeish|
55
+ if treeish
56
+ if treeish.match(/\.\./)
57
+ commits = `git rev-list #{treeish}`.split("\n")
58
+ else
59
+ commits = `git rev-parse #{treeish}`.split("\n")
60
+ end
61
+ else
62
+ # standard in
63
+ puts 'reading from stdin...'
64
+ commits = $stdin.read.split("\n")
65
+ end
66
+ commits.select { |a| a.size == 40 } # only the shas, not the ^SHAs
67
+ end
68
+
69
+ helper :ignore_file_path do
70
+ dir = `git rev-parse --git-dir`.chomp
71
+ File.join(dir, 'ignore-shas')
72
+ end
73
+
74
+ helper :ignore_sha_array do
75
+ File.open( ignore_file_path ) { |yf| YAML::load( yf ) } rescue {}
76
+ end
77
+
78
+ helper :remove_ignored do |array, ignore_array|
79
+ array.reject { |id| ignore_array[id] }
80
+ end
81
+
82
+ helper :ignore_shas do |shas|
83
+ ignores = ignore_sha_array
84
+ shas.each do |sha|
85
+ puts 'ignoring ' + sha
86
+ ignores[sha] = true
87
+ end
88
+ File.open( ignore_file_path, 'w' ) do |out|
89
+ YAML.dump( ignores, out )
90
+ end
91
+ end
92
+
93
+ helper :get_commits do |rev_array|
94
+ list = rev_array.select { |a| has_commit?(a) }.join(' ')
95
+ `git log --pretty=format:"%H::%ae::%s::%ar::%ad" --no-merges #{list}`.split("\n").map { |a| a.split('::') }
96
+ end
97
+
98
+ helper :get_cherry do |branch|
99
+ `git cherry HEAD #{branch} | git name-rev --stdin`.split("\n").map { |a| a.split(' ') }
100
+ end
101
+
102
+ helper :get_common do |branch|
103
+ `git rev-list ..#{branch} --boundary | tail -1 | git name-rev --stdin`.split(' ')[1] rescue 'unknown'
104
+ end
105
+
106
+ helper :print_commits do |our_commits, options|
107
+ ignores = ignore_sha_array
108
+
109
+ case options[:sort]
110
+ when 'branch'
111
+ our_commits.sort! { |a, b| a[0][2] <=> b[0][2] }
112
+ when 'author'
113
+ our_commits.sort! { |a, b| a[1][1] <=> b[1][1] }
114
+ else
115
+ our_commits.sort! { |a, b| Date.parse(a[1][4]) <=> Date.parse(b[1][4]) } rescue 'cant parse dates'
116
+ end
117
+
118
+ shown_commits = {}
119
+ before = Date.parse(options[:before]) if options[:before] rescue puts 'cant parse before date'
120
+ after = Date.parse(options[:after]) if options[:after] rescue puts 'cant parse after date'
121
+ our_commits.each do |cherry, commit|
122
+ status, sha, ref_name = cherry
123
+ ref_name ||= ""
124
+ next if shown_commits[sha] || ignores[sha]
125
+ next if options[:project] && !ref_name.match(Regexp.new(options[:project]))
126
+ ref_name = ref_name.gsub('remotes/', '')
127
+ if status == '+' && commit
128
+ next if options[:author] && !commit[1].match(Regexp.new(options[:author]))
129
+ next if options[:before] && before && (before < Date.parse(commit[4])) rescue false
130
+ next if options[:after] && after && (after > Date.parse(commit[4])) rescue false
131
+ applies = applies_cleanly(sha)
132
+ next if options[:applies] && !applies
133
+ next if options[:noapply] && applies
134
+ if options[:shas]
135
+ puts sha
136
+ else
137
+ common = options[:common] ? get_common(sha) : ''
138
+ puts [sha[0,6], ref_name.ljust(25), commit[1][0,20].ljust(21),
139
+ commit[2][0, 36].ljust(38), commit[3][0,15], common].join(" ")
140
+ end
141
+ end
142
+ shown_commits[sha] = true
143
+ end
144
+ end
145
+
146
+ helper :applies_cleanly do |sha|
147
+ `git diff ...#{sha} | git apply --check >#{DEV_NULL} 2>#{DEV_NULL}`
148
+ $?.exitstatus == 0
149
+ end
150
+
151
+ helper :remotes do
152
+ regexp = '^remote\.(.+)\.url$'
153
+ `git config --get-regexp '#{regexp}'`.split("\n").inject({}) do |memo, line|
154
+ name_string, url = line.split(/ /, 2)
155
+ m, name = *name_string.match(/#{regexp}/)
156
+ memo[name.to_sym] = url
157
+ memo
158
+ end
159
+ end
160
+
161
+ helper :remote_branches_for do |user|
162
+ `git ls-remote -h #{user} 2> #{DEV_NULL}`.split(/\n/).inject({}) do |memo, line|
163
+ hash, head = line.split(/\t/, 2)
164
+ head = head[%r{refs/heads/(.+)$},1] unless head.nil?
165
+ memo[head] = hash unless head.nil?
166
+ memo
167
+ end if !(user.nil? || user.strip.empty?)
168
+ end
169
+
170
+ helper :remote_branch? do |user, branch|
171
+ remote_branches_for(user).key?(branch)
172
+ end
173
+
174
+ # see if there are any cached or tracked files that have been modified
175
+ # originally, we were going to use git-ls-files but that could only
176
+ # report modified track files...not files that have been staged
177
+ # for committal
178
+ helper :branch_dirty? do
179
+ !( system("git diff --quiet 2>#{DEV_NULL}") ||
180
+ !system("git diff --cached --quiet 2>#{DEV_NULL}")
181
+ )
182
+ end
183
+
184
+ helper :tracking do
185
+ remotes.inject({}) do |memo, (name, url)|
186
+ if ur = user_and_repo_from(url)
187
+ memo[name] = ur.first
188
+ else
189
+ memo[name] = url
190
+ end
191
+ memo
192
+ end
193
+ end
194
+
195
+ helper :tracking? do |user|
196
+ tracking.values.include?(user)
197
+ end
198
+
199
+ helper :owner do
200
+ user_for(origin)
201
+ end
202
+
203
+ helper :current_branch do
204
+ `git rev-parse --symbolic-full-name HEAD`.chomp.sub(/^refs\/heads\//, '')
205
+ end
206
+
207
+ helper :user_and_branch do
208
+ raw_branch = current_branch
209
+ user, branch = raw_branch.split(/\//, 2)
210
+ if branch
211
+ [user, branch]
212
+ else
213
+ [owner, user]
214
+ end
215
+ end
216
+
217
+ helper :branch_user do
218
+ user_and_branch.first
219
+ end
220
+
221
+ helper :branch_name do
222
+ user_and_branch.last
223
+ end
224
+
225
+ helper :public_url_for_user_and_repo do |user, repo|
226
+ "git://github.com/#{user}/#{repo}.git"
227
+ end
228
+
229
+ helper :private_url_for_user_and_repo do |user, repo|
230
+ "git@github.com:#{user}/#{repo}.git"
231
+ end
232
+
233
+ helper :public_url_for do |user|
234
+ public_url_for_user_and_repo user, project
235
+ end
236
+
237
+ helper :private_url_for do |user|
238
+ private_url_for_user_and_repo user, project
239
+ end
240
+
241
+ helper :homepage_for do |user, branch|
242
+ "https://github.com/#{user}/#{project}/tree/#{branch}"
243
+ end
244
+
245
+ helper :network_page_for do |user|
246
+ "https://github.com/#{user}/#{project}/network"
247
+ end
248
+
249
+ helper :network_meta_for do |user|
250
+ "http://github.com/#{user}/#{project}/network_meta"
251
+ end
252
+
253
+ helper :issues_page_for do |user|
254
+ "https://github.com/#{user}/#{project}/issues"
255
+ end
256
+
257
+ helper :list_issues_for do |user, state|
258
+ "http://github.com/api/v2/yaml/issues/list/#{user}/#{project}/#{state}"
259
+ end
260
+
261
+ helper :has_launchy? do |blk|
262
+ begin
263
+ gem 'launchy'
264
+ require 'launchy'
265
+ blk.call
266
+ rescue Gem::LoadError
267
+ STDERR.puts "Sorry, you need to install launchy: `gem install launchy`"
268
+ end
269
+ end
270
+
271
+ helper :open do |url|
272
+ has_launchy? proc {
273
+ Launchy::Browser.new.visit url
274
+ }
275
+ end
276
+
277
+ helper :print_network_help do
278
+ puts "
279
+ You have to provide a command :
280
+
281
+ web [user] - opens your web browser to the network graph page for this
282
+ project, or for the graph page for [user] if provided
283
+
284
+ list - shows the projects in your network that have commits
285
+ that you have not pulled in yet, and branch names
286
+
287
+ fetch - adds all projects in your network as remotes and fetches
288
+ any objects from them that you don't have yet
289
+
290
+ commits - will show you a list of all commits in your network that
291
+ you have not ignored or have not merged or cherry-picked.
292
+ This will automatically fetch objects you don't have yet.
293
+
294
+ --project (user/branch) - only show projects that match string
295
+ --author (email) - only show projects that match string
296
+ --after (date) - only show commits after date
297
+ --before (date) - only show commits before date
298
+ --shas - only print shas (can pipe through 'github ignore')
299
+ --applies - filter to patches that still apply cleanly
300
+ --sort - how to sort the commits (date, branch, author)
301
+ "
302
+ end
303
+
304
+ helper :print_network_cherry_help do
305
+ $stderr.puts "
306
+ =========================================================================================
307
+ These are all the commits that other people have pushed that you have not
308
+ applied or ignored yet (see 'github ignore'). Some things you might want to do:
309
+
310
+ * You can run 'github fetch user/branch' (sans '~N') to pull into a local branch for testing
311
+ * You can run 'github cherry-pick [SHA]' to apply a single patch
312
+ * You can run 'github merge user/branch' to merge a commit and all the '~N' variants.
313
+ * You can ignore all commits from a branch with 'github ignore ..user/branch'
314
+ =========================================================================================
315
+
316
+ "
317
+ end
318
+
319
+ helper :argv do
320
+ GitHub.original_args
321
+ end
322
+
323
+ helper :network_members do |user, options|
324
+ get_network_data(user, options)['users'].map { |u| u['name'] }
325
+ end
326
+
327
+
328
+ helper :get_network_data do |user, options|
329
+ if options[:cache] && has_cache?
330
+ return get_cache
331
+ end
332
+ if cache_network_data(options)
333
+ begin
334
+ return cache_data(user)
335
+ rescue SocketError
336
+ STDERR.puts "*** Warning: There was a problem accessing the network."
337
+ rv = get_cache
338
+ STDERR.puts "Using cached data."
339
+ rv
340
+ end
341
+ else
342
+ return get_cache
343
+ end
344
+ end
345
+
346
+ helper :cache_commits do |commits|
347
+ File.open( commits_cache_path, 'w' ) do |out|
348
+ out.write(commits.to_yaml)
349
+ end
350
+ end
351
+
352
+ helper :commits_cache do
353
+ YAML.load(File.open(commits_cache_path))
354
+ end
355
+
356
+ helper :cache_commits_data do |options|
357
+ cache_expired? || options[:nocache] || !has_commits_cache?
358
+ end
359
+
360
+ helper :cache_network_data do |options|
361
+ cache_expired? || options[:nocache] || !has_cache?
362
+ end
363
+
364
+ helper :network_cache_path do
365
+ dir = `git rev-parse --git-dir`.chomp
366
+ File.join(dir, 'network-cache')
367
+ end
368
+
369
+ helper :commits_cache_path do
370
+ dir = `git rev-parse --git-dir`.chomp
371
+ File.join(dir, 'commits-cache')
372
+ end
373
+
374
+ helper :cache_data do |user|
375
+ raw_data = Kernel.open(network_meta_for(user)).read
376
+ File.open( network_cache_path, 'w' ) do |out|
377
+ out.write(raw_data)
378
+ end
379
+ data = JSON.parse(raw_data)
380
+ end
381
+
382
+ helper :cache_expired? do
383
+ return true if !has_cache?
384
+ age = Time.now - File.stat(network_cache_path).mtime
385
+ return true if age > (60 * 60) # 1 hour
386
+ false
387
+ end
388
+
389
+ helper :has_cache? do
390
+ File.file?(network_cache_path)
391
+ end
392
+
393
+ helper :has_commits_cache? do
394
+ File.file?(commits_cache_path)
395
+ end
396
+
397
+ helper :get_cache do
398
+ JSON.parse(File.read(network_cache_path))
399
+ end
400
+
401
+ helper :print_issues_help do
402
+ puts <<-EOHELP
403
+ You have to provide a command :
404
+
405
+ open - shows open tickets for this project
406
+ closed - shows closed tickets for this project
407
+
408
+ --user=<username> - show issues from <username>'s repository
409
+ --after=<date> - only show issues updated after <date>
410
+
411
+ EOHELP
412
+ end
413
+
414
+ helper :distance_of_time do |from_time, to_time|
415
+ # this is a dumbed-down version of actionpack's helper.
416
+ from_time = Time.parse(from_time) if from_time.is_a?(String)
417
+ to_time = Time.parse(to_time) if to_time.is_a?(String)
418
+
419
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
420
+ words = case distance_in_minutes
421
+ when 0 then "less than 1 minute"
422
+ when 2..44 then "%d minutes" % distance_in_minutes
423
+ when 45..89 then "about 1 hour"
424
+ when 90..1439 then "about %d hours" % (distance_in_minutes.to_f / 60.0).round
425
+ when 1440..2879 then "1 day"
426
+ when 2880..43199 then "%d days" % (distance_in_minutes / 1440).round
427
+ when 43200..86399 then "about 1 month"
428
+ when 86400..525599 then "%d months" % (distance_in_minutes / 43200).round
429
+ when 525600..1051199 then "about 1 year"
430
+ else "over %d years" % (distance_in_minutes / 525600).round
431
+ end
432
+
433
+ "#{words} ago"
434
+ end
435
+
436
+ helper :format_issue do |issue, options|
437
+ options ||= {}
438
+ report = []
439
+ report << "Issue ##{issue['number']} (#{issue['votes']} votes): #{issue['title']}"
440
+ report << "* URL: http://github.com/#{options[:user]}/#{project}/issues/#issue/#{issue['number']}" if options[:user]
441
+ report << "* Opened #{distance_of_time(issue['created_at'], Time.now)} by #{issue['user']}" if issue['created_at']
442
+ report << "* Closed #{distance_of_time(issue['closed_at'], Time.now)}" if issue['closed_at']
443
+ report << "* Last updated #{distance_of_time(issue['updated_at'], Time.now)}" if issue['updated_at']
444
+ report << "* Labels: #{issue['labels'].join(', ')}" if issue['labels'] && issue['labels'].length > 0
445
+ report << ""
446
+ report << issue['body']
447
+ report << ""
448
+ report.join("\n")
449
+ end
450
+
451
+ # Converts an array of {"name" => "foo", "description" => "some description"} items
452
+ # as a string list like:
453
+ # foo # some description
454
+ # bar-tar # another description
455
+ helper :format_list do |items|
456
+ longest_name = items.inject("") do |name, item|
457
+ name = item["name"] if item["name"] && item["name"].size > name.size
458
+ name
459
+ end
460
+ longest = longest_name.size + 1
461
+ lines = items.map do |item|
462
+ cmdstr = "%-#{longest}s" % item["name"]
463
+ if (description = item["description"]) && description.length > 0
464
+ cmdstr += "# #{description}"
465
+ end
466
+ cmdstr
467
+ end.join("\n")
468
+ end
469
+
470
+ helper :filter_issue do |issue, options|
471
+ if options[:after] && ! options[:after].instance_of?(Time)
472
+ options[:after] = Time.parse(options[:after]) rescue (puts 'cant parse after date')
473
+ end
474
+ return true if options[:after] && (options[:after] > issue['updated_at']) rescue false
475
+ return true if options[:label] && (issue['labels'].nil? || issue['labels'].empty? || ! issue['labels'].include?(options[:label]))
476
+ return false
477
+ end
478
+
479
+ helper :print_issues do |issues, options|
480
+ issues.sort_by {|issue| issue['updated_at']}.reverse.each do |issue|
481
+ next if filter_issue(issue, options)
482
+ puts "-----"
483
+ puts format_issue(issue, options)
484
+ end
485
+ puts "-----"
486
+ end