github 0.1.1 → 0.4.0

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 (47) hide show
  1. data/History.txt +37 -0
  2. data/Manifest +33 -12
  3. data/README.md +187 -0
  4. data/Rakefile +44 -0
  5. data/bin/gh +8 -0
  6. data/bin/github +4 -1
  7. data/github.gemspec +29 -34
  8. data/lib/commands/commands.rb +249 -0
  9. data/lib/commands/helpers.rb +486 -0
  10. data/lib/commands/issues.rb +17 -0
  11. data/lib/commands/network.rb +110 -0
  12. data/lib/github.rb +117 -29
  13. data/lib/github/command.rb +69 -14
  14. data/lib/github/extensions.rb +39 -0
  15. data/lib/github/ui.rb +19 -0
  16. data/setup.rb +1551 -0
  17. data/spec/command_spec.rb +82 -0
  18. data/spec/commands/command_browse_spec.rb +36 -0
  19. data/spec/commands/command_clone_spec.rb +87 -0
  20. data/spec/commands/command_create-from-local_spec.rb +7 -0
  21. data/spec/commands/command_fetch_spec.rb +56 -0
  22. data/spec/commands/command_fork_spec.rb +44 -0
  23. data/spec/commands/command_helper.rb +170 -0
  24. data/spec/commands/command_home_spec.rb +20 -0
  25. data/spec/commands/command_info_spec.rb +23 -0
  26. data/spec/commands/command_issues_spec.rb +97 -0
  27. data/spec/commands/command_network_spec.rb +30 -0
  28. data/spec/commands/command_pull-request_spec.rb +51 -0
  29. data/spec/commands/command_pull_spec.rb +82 -0
  30. data/spec/commands/command_search_spec.rb +34 -0
  31. data/spec/commands/command_track_spec.rb +82 -0
  32. data/spec/commands_spec.rb +49 -0
  33. data/spec/extensions_spec.rb +36 -0
  34. data/spec/github_spec.rb +85 -0
  35. data/spec/helper_spec.rb +368 -0
  36. data/spec/spec_helper.rb +160 -4
  37. data/spec/windoze_spec.rb +38 -0
  38. metadata +114 -47
  39. data/README +0 -49
  40. data/commands/commands.rb +0 -54
  41. data/commands/helpers.rb +0 -79
  42. data/spec/helpers/owner_spec.rb +0 -12
  43. data/spec/helpers/project_spec.rb +0 -12
  44. data/spec/helpers/public_url_for_spec.rb +0 -12
  45. data/spec/helpers/repo_for_spec.rb +0 -12
  46. data/spec/helpers/user_and_repo_from_spec.rb +0 -15
  47. data/spec/helpers/user_for_spec.rb +0 -12
@@ -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/([^/]+/[^/]+)$|: $1.split('/')
6
+ when %r|^(?:ssh://)?(?:git@)?github\.com:([^/]+/[^/]+)$|: $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