hub 1.6.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of hub might be problematic. Click here for more details.

@@ -1,66 +1,274 @@
1
1
  require 'shellwords'
2
+ require 'forwardable'
2
3
 
3
4
  module Hub
4
5
  # Provides methods for inspecting the environment, such as GitHub user/token
5
6
  # settings, repository info, and similar.
6
7
  module Context
7
- private
8
+ extend Forwardable
9
+
10
+ NULL = defined?(File::NULL) ? File::NULL : File.exist?('/dev/null') ? '/dev/null' : 'NUL'
11
+
12
+ # Shells out to git to get output of its commands
13
+ class GitReader
14
+ attr_reader :executable
15
+
16
+ def initialize(executable = nil, &read_proc)
17
+ @executable = executable || 'git'
18
+ # caches output when shelling out to git
19
+ read_proc ||= lambda { |cache, cmd|
20
+ result = %x{#{command_to_string(cmd)} 2>#{NULL}}.chomp
21
+ cache[cmd] = $?.success? && !result.empty? ? result : nil
22
+ }
23
+ @cache = Hash.new(&read_proc)
24
+ end
25
+
26
+ def add_exec_flags(flags)
27
+ @executable = Array(executable).concat(flags)
28
+ end
8
29
 
9
- class ShellOutCache < Hash
10
- attr_accessor :executable
30
+ def read_config(cmd, all = false)
31
+ config_cmd = ['config', (all ? '--get-all' : '--get'), *cmd]
32
+ config_cmd = config_cmd.join(' ') unless cmd.respond_to? :join
33
+ read config_cmd
34
+ end
35
+
36
+ def read(cmd)
37
+ @cache[cmd]
38
+ end
39
+
40
+ def stub_config_value(key, value, get = '--get')
41
+ stub_command_output "config #{get} #{key}", value
42
+ end
43
+
44
+ def stub_command_output(cmd, value)
45
+ @cache[cmd] = value.nil? ? nil : value.to_s
46
+ end
11
47
 
12
- def initialize(executable = nil, &block)
13
- super(&block)
14
- @executable = executable
48
+ def stub!(values)
49
+ @cache.update values
15
50
  end
16
51
 
52
+ private
53
+
17
54
  def to_exec(args)
18
55
  args = Shellwords.shellwords(args) if args.respond_to? :to_str
19
56
  Array(executable) + Array(args)
20
57
  end
58
+
59
+ def command_to_string(cmd)
60
+ full_cmd = to_exec(cmd)
61
+ full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ')
62
+ end
63
+ end
64
+
65
+ module GitReaderMethods
66
+ extend Forwardable
67
+
68
+ def_delegator :git_reader, :read_config, :git_config
69
+ def_delegator :git_reader, :read, :git_command
70
+
71
+ def self.extended(base)
72
+ base.extend Forwardable
73
+ base.def_delegators :'self.class', :git_config, :git_command
74
+ end
21
75
  end
22
76
 
23
- # Caches output when shelling out to git
24
- GIT_CONFIG = ShellOutCache.new(ENV['GIT'] || 'git') do |cache, cmd|
25
- full_cmd = cache.to_exec(cmd)
26
- cmd_string = full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ')
27
- result = %x{#{cmd_string}}.chomp
28
- cache[cmd] = $?.success? && !result.empty? ? result : nil
77
+ private
78
+
79
+ def git_reader
80
+ @git_reader ||= GitReader.new ENV['GIT']
29
81
  end
30
82
 
31
- # Parses URLs for git remotes and stores info
32
- REMOTES = Hash.new do |cache, remote|
33
- if remote
34
- urls = GIT_CONFIG["config --get-all remote.#{remote}.url"].to_s.split("\n")
83
+ include GitReaderMethods
84
+ private :git_config, :git_command
85
+
86
+ def local_repo
87
+ @local_repo ||= begin
88
+ LocalRepo.new git_reader, current_dir if is_repo?
89
+ end
90
+ end
35
91
 
36
- if urls.find { |u| u =~ %r{\bgithub\.com[:/](.+)/(.+).git$} }
37
- cache[remote] = { :user => $1, :repo => $2 }
92
+ repo_methods = [
93
+ :current_branch, :master_branch,
94
+ :current_project, :upstream_project,
95
+ :repo_owner,
96
+ :remotes, :remotes_group, :origin_remote
97
+ ]
98
+ def_delegator :local_repo, :name, :repo_name
99
+ def_delegators :local_repo, *repo_methods
100
+ private :repo_name, *repo_methods
101
+
102
+ class LocalRepo < Struct.new(:git_reader, :dir)
103
+ include GitReaderMethods
104
+
105
+ def name
106
+ if project = main_project
107
+ project.name
38
108
  else
39
- cache[remote] = { }
109
+ File.basename(dir)
110
+ end
111
+ end
112
+
113
+ def repo_owner
114
+ if project = main_project
115
+ project.owner
40
116
  end
41
- else
42
- cache[remote] = { }
117
+ end
118
+
119
+ def main_project
120
+ remote = origin_remote and remote.project
121
+ end
122
+
123
+ def upstream_project
124
+ if upstream = current_branch.upstream
125
+ remote = remote_by_name upstream.remote_name
126
+ remote.project
127
+ end
128
+ end
129
+
130
+ def current_project
131
+ upstream_project || main_project
132
+ end
133
+
134
+ def current_branch
135
+ if branch = git_command('symbolic-ref -q HEAD')
136
+ Branch.new self, branch
137
+ end
138
+ end
139
+
140
+ def master_branch
141
+ Branch.new self, 'refs/heads/master'
142
+ end
143
+
144
+ def remotes
145
+ @remotes ||= begin
146
+ # TODO: is there a plumbing command to get a list of remotes?
147
+ list = git_command('remote').to_s.split("\n")
148
+ # force "origin" to be first in the list
149
+ main = list.delete('origin') and list.unshift(main)
150
+ list.map { |name| Remote.new self, name }
151
+ end
152
+ end
153
+
154
+ def remotes_group(name)
155
+ git_config "remotes.#{name}"
156
+ end
157
+
158
+ def origin_remote
159
+ remotes.first
160
+ end
161
+
162
+ def remote_by_name(remote_name)
163
+ remotes.find {|r| r.name == remote_name }
43
164
  end
44
165
  end
45
166
 
46
- LGHCONF = "http://help.github.com/git-email-settings/"
167
+ class GithubProject < Struct.new(:local_repo, :owner, :name)
168
+ def name_with_owner
169
+ "#{owner}/#{name}"
170
+ end
171
+
172
+ def ==(other)
173
+ name_with_owner == other.name_with_owner
174
+ end
175
+
176
+ def remote
177
+ local_repo.remotes.find { |r| r.project == self }
178
+ end
179
+
180
+ def web_url(path = nil)
181
+ project_name = name_with_owner
182
+ if project_name.sub!(/\.wiki$/, '')
183
+ unless '/wiki' == path
184
+ path = if path =~ %r{^/commits/} then '/_history'
185
+ else path.to_s.sub(/\w+/, '_\0')
186
+ end
187
+ path = '/wiki' + path
188
+ end
189
+ end
190
+ 'https://github.com/' + project_name + path.to_s
191
+ end
47
192
 
48
- def repo_owner
49
- REMOTES[default_remote][:user]
193
+ def git_url(options = {})
194
+ if options[:https] then 'https://github.com/'
195
+ elsif options[:private] then 'git@github.com:'
196
+ else 'git://github.com/'
197
+ end + name_with_owner + '.git'
198
+ end
50
199
  end
51
200
 
52
- def repo_user
53
- REMOTES[current_remote][:user]
201
+ class Branch < Struct.new(:local_repo, :name)
202
+ alias to_s name
203
+
204
+ def short_name
205
+ name.split('/').last
206
+ end
207
+
208
+ def master?
209
+ short_name == 'master'
210
+ end
211
+
212
+ def upstream
213
+ if branch = local_repo.git_command("rev-parse --symbolic-full-name #{short_name}@{upstream}")
214
+ Branch.new local_repo, branch
215
+ end
216
+ end
217
+
218
+ def remote?
219
+ name.index('refs/remotes/') == 0
220
+ end
221
+
222
+ def remote_name
223
+ name =~ %r{^refs/remotes/([^/]+)} and $1 or
224
+ raise "can't get remote name from #{name.inspect}"
225
+ end
54
226
  end
55
227
 
56
- def repo_name
57
- REMOTES[default_remote][:repo] || current_dirname
228
+ class Remote < Struct.new(:local_repo, :name)
229
+ alias to_s name
230
+
231
+ def ==(other)
232
+ other.respond_to?(:to_str) ? name == other.to_str : super
233
+ end
234
+
235
+ def project
236
+ if urls.find { |u| u =~ %r{\bgithub\.com[:/](.+)/(.+).git$} }
237
+ GithubProject.new local_repo, $1, $2
238
+ end
239
+ end
240
+
241
+ def urls
242
+ @urls ||= local_repo.git_config("remote.#{name}.url", :all).to_s.split("\n")
243
+ end
58
244
  end
59
245
 
246
+ ## helper methods for local repo, GH projects
247
+
248
+ def github_project(name, owner = nil)
249
+ if owner and owner.index('/')
250
+ owner, name = owner.split('/', 2)
251
+ elsif name and name.index('/')
252
+ owner, name = name.split('/', 2)
253
+ else
254
+ name ||= repo_name
255
+ owner ||= github_user
256
+ end
257
+
258
+ GithubProject.new local_repo, owner, name
259
+ end
260
+
261
+ def git_url(owner = nil, name = nil, options = {})
262
+ project = github_project(name, owner)
263
+ project.git_url({:https => https_protocol?}.update(options))
264
+ end
265
+
266
+ LGHCONF = "http://help.github.com/set-your-user-name-email-and-github-token/"
267
+
60
268
  # Either returns the GitHub user as set by git-config(1) or aborts
61
269
  # with an error message.
62
270
  def github_user(fatal = true)
63
- if user = ENV['GITHUB_USER'] || GIT_CONFIG['config github.user']
271
+ if user = ENV['GITHUB_USER'] || git_config('github.user')
64
272
  user
65
273
  elsif fatal
66
274
  abort("** No GitHub user set. See #{LGHCONF}")
@@ -68,121 +276,71 @@ module Hub
68
276
  end
69
277
 
70
278
  def github_token(fatal = true)
71
- if token = ENV['GITHUB_TOKEN'] || GIT_CONFIG['config github.token']
279
+ if token = ENV['GITHUB_TOKEN'] || git_config('github.token')
72
280
  token
73
281
  elsif fatal
74
282
  abort("** No GitHub token set. See #{LGHCONF}")
75
283
  end
76
284
  end
77
285
 
78
- def current_branch
79
- GIT_CONFIG['symbolic-ref -q HEAD']
80
- end
81
-
82
- def tracked_branch
83
- branch = current_branch && tracked_for(current_branch)
84
- normalize_branch(branch) if branch
85
- end
86
-
87
- def remotes
88
- list = GIT_CONFIG['remote'].to_s.split("\n")
89
- main = list.delete('origin') and list.unshift(main)
90
- list
91
- end
92
-
93
- def remotes_group(name)
94
- GIT_CONFIG["config remotes.#{name}"]
95
- end
96
-
97
- def current_remote
98
- return if remotes.empty?
99
- (current_branch && remote_for(current_branch)) || default_remote
100
- end
101
-
102
- def default_remote
103
- remotes.first
286
+ # legacy setting
287
+ def http_clone?
288
+ git_config('--bool hub.http-clone') == 'true'
104
289
  end
105
290
 
106
- def normalize_branch(branch)
107
- branch.sub('refs/heads/', '')
291
+ def https_protocol?
292
+ git_config('hub.protocol') == 'https' or http_clone?
108
293
  end
109
294
 
110
- def remote_for(branch)
111
- GIT_CONFIG['config branch.%s.remote' % normalize_branch(branch)]
295
+ def git_alias_for(name)
296
+ git_config "alias.#{name}"
112
297
  end
113
298
 
114
- def tracked_for(branch)
115
- GIT_CONFIG['config branch.%s.merge' % normalize_branch(branch)]
116
- end
299
+ PWD = Dir.pwd
117
300
 
118
- def http_clone?
119
- GIT_CONFIG['config --bool hub.http-clone'] == 'true'
301
+ def current_dir
302
+ PWD
120
303
  end
121
304
 
122
- def git_alias_for(name)
123
- GIT_CONFIG["config alias.#{name}"]
305
+ def git_dir
306
+ git_command 'rev-parse -q --git-dir'
124
307
  end
125
308
 
126
- # Core.repositoryformatversion should exist for all git
127
- # repositories, and be blank for all non-git repositories. If
128
- # there's a better config setting to check here, this can be
129
- # changed without breaking anything.
130
309
  def is_repo?
131
- GIT_CONFIG['config core.repositoryformatversion']
310
+ !!git_dir
132
311
  end
133
312
 
134
- def github_url(options = {})
135
- repo = options[:repo]
136
- user, repo = repo.split('/') if repo && repo.index('/')
137
- user ||= options[:user] || github_user
138
- repo ||= repo_name
139
- secure = options[:private]
140
-
141
- if options[:web]
142
- scheme = secure ? 'https:' : 'http:'
143
- path = options[:web] == true ? '' : options[:web].to_s
144
- if repo =~ /\.wiki$/
145
- repo = repo.sub(/\.wiki$/, '')
146
- unless '/wiki' == path
147
- path = '/wiki%s' % if path =~ %r{^/commits/} then '/_history'
148
- else path.sub(/\w+/, '_\0')
149
- end
150
- end
151
- end
152
- '%s//github.com/%s/%s%s' % [scheme, user, repo, path]
153
- else
154
- if secure
155
- url = 'git@github.com:%s/%s.git'
156
- elsif http_clone?
157
- url = 'http://github.com/%s/%s.git'
158
- else
159
- url = 'git://github.com/%s/%s.git'
160
- end
161
-
162
- url % [user, repo]
163
- end
164
- end
165
-
166
- DIRNAME = File.basename(Dir.pwd)
167
-
168
- def current_dirname
169
- DIRNAME
313
+ def git_editor
314
+ # possible: ~/bin/vi, $SOME_ENVIRONMENT_VARIABLE, "C:\Program Files\Vim\gvim.exe" --nofork
315
+ editor = git_command 'var GIT_EDITOR'
316
+ editor = ENV[$1] if editor =~ /^\$(\w+)$/
317
+ editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/
318
+ editor.shellsplit
170
319
  end
171
320
 
172
321
  # Cross-platform web browser command; respects the value set in $BROWSER.
173
322
  #
174
323
  # Returns an array, e.g.: ['open']
175
324
  def browser_launcher
176
- require 'rbconfig'
177
- browser = ENV['BROWSER'] ||
178
- (RbConfig::CONFIG['host_os'].include?('darwin') && 'open') ||
179
- (RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ && 'start') ||
325
+ browser = ENV['BROWSER'] || (
326
+ osx? ? 'open' : windows? ? 'start' :
180
327
  %w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm }
328
+ )
181
329
 
182
330
  abort "Please set $BROWSER to a web launcher to use this command." unless browser
183
331
  Array(browser)
184
332
  end
185
333
 
334
+ def osx?
335
+ require 'rbconfig'
336
+ RbConfig::CONFIG['host_os'].to_s.include?('darwin')
337
+ end
338
+
339
+ def windows?
340
+ require 'rbconfig'
341
+ RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/
342
+ end
343
+
186
344
  # Cross-platform way of finding an executable in the $PATH.
187
345
  #
188
346
  # which('ruby') #=> /usr/bin/ruby