hubflow 1.7.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.
- data/LICENSE +20 -0
- data/README.md +385 -0
- data/Rakefile +140 -0
- data/bin/hubflow +7 -0
- data/lib/hub.rb +5 -0
- data/lib/hub/args.rb +117 -0
- data/lib/hub/commands.rb +977 -0
- data/lib/hub/context.rb +367 -0
- data/lib/hub/runner.rb +73 -0
- data/lib/hub/standalone.rb +58 -0
- data/lib/hub/version.rb +3 -0
- data/man/hub.1 +438 -0
- data/man/hub.1.html +437 -0
- data/man/hub.1.ronn +192 -0
- data/test/alias_test.rb +40 -0
- data/test/deps.rip +1 -0
- data/test/fakebin/git +11 -0
- data/test/fakebin/open +3 -0
- data/test/helper.rb +111 -0
- data/test/hub_test.rb +1224 -0
- data/test/standalone_test.rb +48 -0
- metadata +106 -0
data/lib/hub/context.rb
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Hub
|
5
|
+
# Provides methods for inspecting the environment, such as GitHub user/token
|
6
|
+
# settings, repository info, and similar.
|
7
|
+
module Context
|
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
|
29
|
+
|
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
|
47
|
+
|
48
|
+
def stub!(values)
|
49
|
+
@cache.update values
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def to_exec(args)
|
55
|
+
args = Shellwords.shellwords(args) if args.respond_to? :to_str
|
56
|
+
Array(executable) + Array(args)
|
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
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def git_reader
|
80
|
+
@git_reader ||= GitReader.new ENV['GIT']
|
81
|
+
end
|
82
|
+
|
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
|
91
|
+
|
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
|
108
|
+
else
|
109
|
+
File.basename(dir)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def repo_owner
|
114
|
+
if project = main_project
|
115
|
+
project.owner
|
116
|
+
end
|
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 }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
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
|
192
|
+
|
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
|
199
|
+
end
|
200
|
+
|
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
|
226
|
+
end
|
227
|
+
|
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
|
244
|
+
end
|
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
|
+
|
268
|
+
# Either returns the GitHub user as set by git-config(1) or aborts
|
269
|
+
# with an error message.
|
270
|
+
def github_user(fatal = true)
|
271
|
+
if user = ENV['GITHUB_USER'] || git_config('github.user')
|
272
|
+
user
|
273
|
+
elsif fatal
|
274
|
+
abort("** No GitHub user set. See #{LGHCONF}")
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def github_token(fatal = true)
|
279
|
+
if token = ENV['GITHUB_TOKEN'] || git_config('github.token')
|
280
|
+
token
|
281
|
+
elsif fatal
|
282
|
+
abort("** No GitHub token set. See #{LGHCONF}")
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# legacy setting
|
287
|
+
def http_clone?
|
288
|
+
git_config('--bool hub.http-clone') == 'true'
|
289
|
+
end
|
290
|
+
|
291
|
+
def https_protocol?
|
292
|
+
git_config('hub.protocol') == 'https' or http_clone?
|
293
|
+
end
|
294
|
+
|
295
|
+
def git_alias_for(name)
|
296
|
+
git_config "alias.#{name}"
|
297
|
+
end
|
298
|
+
|
299
|
+
PWD = Dir.pwd
|
300
|
+
|
301
|
+
def current_dir
|
302
|
+
PWD
|
303
|
+
end
|
304
|
+
|
305
|
+
def git_dir
|
306
|
+
git_command 'rev-parse -q --git-dir'
|
307
|
+
end
|
308
|
+
|
309
|
+
def is_repo?
|
310
|
+
!!git_dir
|
311
|
+
end
|
312
|
+
|
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
|
319
|
+
end
|
320
|
+
|
321
|
+
# Cross-platform web browser command; respects the value set in $BROWSER.
|
322
|
+
#
|
323
|
+
# Returns an array, e.g.: ['open']
|
324
|
+
def browser_launcher
|
325
|
+
browser = ENV['BROWSER'] || (
|
326
|
+
osx? ? 'open' : windows? ? 'start' :
|
327
|
+
%w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm }
|
328
|
+
)
|
329
|
+
|
330
|
+
abort "Please set $BROWSER to a web launcher to use this command." unless browser
|
331
|
+
Array(browser)
|
332
|
+
end
|
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
|
+
|
344
|
+
# Cross-platform way of finding an executable in the $PATH.
|
345
|
+
#
|
346
|
+
# which('ruby') #=> /usr/bin/ruby
|
347
|
+
def which(cmd)
|
348
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
349
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
350
|
+
exts.each { |ext|
|
351
|
+
exe = "#{path}/#{cmd}#{ext}"
|
352
|
+
return exe if File.executable? exe
|
353
|
+
}
|
354
|
+
end
|
355
|
+
return nil
|
356
|
+
end
|
357
|
+
|
358
|
+
# Checks whether a command exists on this system in the $PATH.
|
359
|
+
#
|
360
|
+
# name - The String name of the command to check for.
|
361
|
+
#
|
362
|
+
# Returns a Boolean.
|
363
|
+
def command?(name)
|
364
|
+
!which(name).nil?
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
data/lib/hub/runner.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Hub
|
2
|
+
# The Hub runner expects to be initialized with `ARGV` and primarily
|
3
|
+
# exists to run a git command.
|
4
|
+
#
|
5
|
+
# The actual functionality, that is, the code it runs when it needs to
|
6
|
+
# augment a git command, is kept in the `Hub::Commands` module.
|
7
|
+
class Runner
|
8
|
+
attr_reader :args
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
@args = Args.new(args)
|
12
|
+
Commands.run(@args)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Shortcut
|
16
|
+
def self.execute(*args)
|
17
|
+
new(*args).execute
|
18
|
+
end
|
19
|
+
|
20
|
+
# A string representation of the command that would run.
|
21
|
+
def command
|
22
|
+
if args.skip?
|
23
|
+
''
|
24
|
+
else
|
25
|
+
commands.join('; ')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# An array of all commands as strings.
|
30
|
+
def commands
|
31
|
+
args.commands.map do |cmd|
|
32
|
+
if cmd.respond_to?(:join)
|
33
|
+
# a simplified `Shellwords.join` but it's OK since this is only used to inspect
|
34
|
+
cmd.map { |arg| arg = arg.to_s; (arg.index(' ') || arg.empty?) ? "'#{arg}'" : arg }.join(' ')
|
35
|
+
else
|
36
|
+
cmd.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Runs the target git command with an optional callback. Replaces
|
42
|
+
# the current process.
|
43
|
+
#
|
44
|
+
# If `args` is empty, this will skip calling the git command. This
|
45
|
+
# allows commands to print an error message and cancel their own
|
46
|
+
# execution if they don't make sense.
|
47
|
+
def execute
|
48
|
+
if args.noop?
|
49
|
+
puts commands
|
50
|
+
elsif not args.skip?
|
51
|
+
if args.chained?
|
52
|
+
execute_command_chain
|
53
|
+
else
|
54
|
+
exec(*args.to_exec)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Runs multiple commands in succession; exits at first failure.
|
60
|
+
def execute_command_chain
|
61
|
+
commands = args.commands
|
62
|
+
commands.each_with_index do |cmd, i|
|
63
|
+
if cmd.respond_to?(:call) then cmd.call
|
64
|
+
elsif i == commands.length - 1
|
65
|
+
# last command in chain
|
66
|
+
exec(*cmd)
|
67
|
+
else
|
68
|
+
exit($?.exitstatus) unless system(*cmd)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hub
|
2
|
+
module Standalone
|
3
|
+
extend self
|
4
|
+
|
5
|
+
RUBY_BIN = if File.executable? '/usr/bin/ruby' then '/usr/bin/ruby'
|
6
|
+
else
|
7
|
+
require 'rbconfig'
|
8
|
+
File.join RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']
|
9
|
+
end
|
10
|
+
|
11
|
+
PREAMBLE = <<-preamble
|
12
|
+
#!#{RUBY_BIN}
|
13
|
+
#
|
14
|
+
# This file, hub, is generated code.
|
15
|
+
# Please DO NOT EDIT or send patches for it.
|
16
|
+
#
|
17
|
+
# Please take a look at the source from
|
18
|
+
# https://github.com/defunkt/hub
|
19
|
+
# and submit patches against the individual files
|
20
|
+
# that build hub.
|
21
|
+
#
|
22
|
+
|
23
|
+
preamble
|
24
|
+
|
25
|
+
POSTAMBLE = "Hub::Runner.execute(*ARGV)\n"
|
26
|
+
__DIR__ = File.dirname(__FILE__)
|
27
|
+
MANPAGE = "__END__\n#{File.read(__DIR__ + '/../../man/hub.1')}"
|
28
|
+
|
29
|
+
def save(filename, path = '.')
|
30
|
+
target = File.join(File.expand_path(path), filename)
|
31
|
+
File.open(target, 'w') do |f|
|
32
|
+
f.puts build
|
33
|
+
f.chmod 0755
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build
|
38
|
+
root = File.dirname(__FILE__)
|
39
|
+
|
40
|
+
standalone = ''
|
41
|
+
standalone << PREAMBLE
|
42
|
+
|
43
|
+
files = Dir["#{root}/*.rb"].sort - [__FILE__]
|
44
|
+
# ensure context.rb appears before others
|
45
|
+
ctx = files.find {|f| f['context.rb'] } and files.unshift(files.delete(ctx))
|
46
|
+
|
47
|
+
files.each do |file|
|
48
|
+
File.readlines(file).each do |line|
|
49
|
+
standalone << line if line !~ /^\s*#/
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
standalone << POSTAMBLE
|
54
|
+
standalone << MANPAGE
|
55
|
+
standalone
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|