hubflow 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/bin/hubflow
ADDED
data/lib/hub.rb
ADDED
data/lib/hub/args.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
module Hub
|
2
|
+
# The Args class exists to make it more convenient to work with
|
3
|
+
# command line arguments intended for git from within the Hub
|
4
|
+
# codebase.
|
5
|
+
#
|
6
|
+
# The ARGV array is converted into an Args instance by the Hub
|
7
|
+
# instance when instantiated.
|
8
|
+
class Args < Array
|
9
|
+
attr_accessor :executable
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
@executable = ENV["GIT"] || "git"
|
14
|
+
@after = nil
|
15
|
+
@skip = @noop = false
|
16
|
+
@original_args = args.first
|
17
|
+
@chain = [nil]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds an `after` callback.
|
21
|
+
# A callback can be a command or a proc.
|
22
|
+
def after(cmd_or_args = nil, args = nil, &block)
|
23
|
+
@chain.insert(-1, normalize_callback(cmd_or_args, args, block))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds a `before` callback.
|
27
|
+
# A callback can be a command or a proc.
|
28
|
+
def before(cmd_or_args = nil, args = nil, &block)
|
29
|
+
@chain.insert(@chain.index(nil), normalize_callback(cmd_or_args, args, block))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tells if there are multiple (chained) commands or not.
|
33
|
+
def chained?
|
34
|
+
@chain.size > 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of all commands.
|
38
|
+
def commands
|
39
|
+
chain = @chain.dup
|
40
|
+
chain[chain.index(nil)] = self.to_exec
|
41
|
+
chain
|
42
|
+
end
|
43
|
+
|
44
|
+
# Skip running this command.
|
45
|
+
def skip!
|
46
|
+
@skip = true
|
47
|
+
end
|
48
|
+
|
49
|
+
# Boolean indicating whether this command will run.
|
50
|
+
def skip?
|
51
|
+
@skip
|
52
|
+
end
|
53
|
+
|
54
|
+
# Mark that this command shouldn't really run.
|
55
|
+
def noop!
|
56
|
+
@noop = true
|
57
|
+
end
|
58
|
+
|
59
|
+
def noop?
|
60
|
+
@noop
|
61
|
+
end
|
62
|
+
|
63
|
+
# Array of `executable` followed by all args suitable as arguments
|
64
|
+
# for `exec` or `system` calls.
|
65
|
+
def to_exec(args = self)
|
66
|
+
Array(executable) + args
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_exec_flags(flags)
|
70
|
+
self.executable = Array(executable).concat(flags)
|
71
|
+
end
|
72
|
+
|
73
|
+
# All the words (as opposed to flags) contained in this argument
|
74
|
+
# list.
|
75
|
+
#
|
76
|
+
# args = Args.new([ 'remote', 'add', '-f', 'tekkub' ])
|
77
|
+
# args.words == [ 'remote', 'add', 'tekkub' ]
|
78
|
+
def words
|
79
|
+
reject { |arg| arg.index('-') == 0 }
|
80
|
+
end
|
81
|
+
|
82
|
+
# All the flags (as opposed to words) contained in this argument
|
83
|
+
# list.
|
84
|
+
#
|
85
|
+
# args = Args.new([ 'remote', 'add', '-f', 'tekkub' ])
|
86
|
+
# args.flags == [ '-f' ]
|
87
|
+
def flags
|
88
|
+
self - words
|
89
|
+
end
|
90
|
+
|
91
|
+
# Tests if arguments were modified since instantiation
|
92
|
+
def changed?
|
93
|
+
chained? or self != @original_args
|
94
|
+
end
|
95
|
+
|
96
|
+
def has_flag?(*flags)
|
97
|
+
pattern = flags.flatten.map { |f| Regexp.escape(f) }.join('|')
|
98
|
+
!grep(/^#{pattern}(?:=|$)/).empty?
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def normalize_callback(cmd_or_args, args, block)
|
104
|
+
if block
|
105
|
+
block
|
106
|
+
elsif args
|
107
|
+
[cmd_or_args].concat args
|
108
|
+
elsif Array === cmd_or_args
|
109
|
+
self.to_exec cmd_or_args
|
110
|
+
elsif cmd_or_args
|
111
|
+
cmd_or_args
|
112
|
+
else
|
113
|
+
raise ArgumentError, "command or block required"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/hub/commands.rb
ADDED
@@ -0,0 +1,977 @@
|
|
1
|
+
module Hub
|
2
|
+
# The Commands module houses the git commands that hub
|
3
|
+
# lovingly wraps. If a method exists here, it is expected to have a
|
4
|
+
# corresponding git command which either gets run before or after
|
5
|
+
# the method executes.
|
6
|
+
#
|
7
|
+
# The typical flow is as follows:
|
8
|
+
#
|
9
|
+
# 1. hub is invoked from the command line:
|
10
|
+
# $ hub clone rtomayko/tilt
|
11
|
+
#
|
12
|
+
# 2. The Hub class is initialized:
|
13
|
+
# >> hub = Hub.new('clone', 'rtomayko/tilt')
|
14
|
+
#
|
15
|
+
# 3. The method representing the git subcommand is executed with the
|
16
|
+
# full args:
|
17
|
+
# >> Commands.clone('clone', 'rtomayko/tilt')
|
18
|
+
#
|
19
|
+
# 4. That method rewrites the args as it sees fit:
|
20
|
+
# >> args[1] = "git://github.com/" + args[1] + ".git"
|
21
|
+
# => "git://github.com/rtomayko/tilt.git"
|
22
|
+
#
|
23
|
+
# 5. The new args are used to run `git`:
|
24
|
+
# >> exec "git", "clone", "git://github.com/rtomayko/tilt.git"
|
25
|
+
#
|
26
|
+
# An optional `after` callback can be set. If so, it is run after
|
27
|
+
# step 5 (which then performs a `system` call rather than an
|
28
|
+
# `exec`). See `Hub::Args` for more information on the `after` callback.
|
29
|
+
module Commands
|
30
|
+
# We are a blank slate.
|
31
|
+
instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ }
|
32
|
+
extend self
|
33
|
+
|
34
|
+
# provides git interrogation methods
|
35
|
+
extend Context
|
36
|
+
|
37
|
+
API_REPO = 'http://github.com/api/v2/yaml/repos/show/%s/%s'
|
38
|
+
API_FORK = 'https://github.com/api/v2/yaml/repos/fork/%s/%s'
|
39
|
+
API_CREATE = 'https://github.com/api/v2/yaml/repos/create'
|
40
|
+
API_PULL = 'http://github.com/api/v2/json/pulls/%s'
|
41
|
+
API_PULLREQUEST = 'https://github.com/api/v2/yaml/pulls/%s/%s'
|
42
|
+
|
43
|
+
NAME_RE = /[\w.-]+/
|
44
|
+
OWNER_RE = /[a-zA-Z0-9-]+/
|
45
|
+
NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/
|
46
|
+
|
47
|
+
def run(args)
|
48
|
+
slurp_global_flags(args)
|
49
|
+
|
50
|
+
# Hack to emulate git-style
|
51
|
+
args.unshift 'help' if args.empty?
|
52
|
+
|
53
|
+
cmd = args[0]
|
54
|
+
expanded_args = expand_alias(cmd)
|
55
|
+
cmd = expanded_args[0] if expanded_args
|
56
|
+
|
57
|
+
# git commands can have dashes
|
58
|
+
cmd = cmd.sub(/(\w)-/, '\1_')
|
59
|
+
if method_defined?(cmd) and cmd != 'run'
|
60
|
+
args[0, 1] = expanded_args if expanded_args
|
61
|
+
send(cmd, args)
|
62
|
+
end
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
if $!.message.include? "No such file or directory - git"
|
65
|
+
abort "Error: `git` command not found"
|
66
|
+
else
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# $ hub pull-request
|
72
|
+
# $ hub pull-request "My humble contribution"
|
73
|
+
# $ hub pull-request -i 92
|
74
|
+
# $ hub pull-request https://github.com/rtomayko/tilt/issues/92
|
75
|
+
def pull_request(args)
|
76
|
+
args.shift
|
77
|
+
options = { }
|
78
|
+
force = explicit_owner = false
|
79
|
+
base_project = local_repo.main_project
|
80
|
+
head_project = local_repo.current_project
|
81
|
+
|
82
|
+
from_github_ref = lambda do |ref, context_project|
|
83
|
+
if ref.index(':')
|
84
|
+
owner, ref = ref.split(':', 2)
|
85
|
+
project = github_project(context_project.name, owner)
|
86
|
+
end
|
87
|
+
[project || context_project, ref]
|
88
|
+
end
|
89
|
+
|
90
|
+
while arg = args.shift
|
91
|
+
case arg
|
92
|
+
when '-f'
|
93
|
+
force = true
|
94
|
+
when '-b'
|
95
|
+
base_project, options[:base] = from_github_ref.call(args.shift, base_project)
|
96
|
+
when '-h'
|
97
|
+
head = args.shift
|
98
|
+
explicit_owner = !!head.index(':')
|
99
|
+
head_project, options[:head] = from_github_ref.call(head, head_project)
|
100
|
+
when '-i'
|
101
|
+
options[:issue] = args.shift
|
102
|
+
when %r{^https?://github.com/([^/]+/[^/]+)/issues/(\d+)}
|
103
|
+
options[:issue] = $2
|
104
|
+
base_project = github_project($1)
|
105
|
+
else
|
106
|
+
if !options[:title] then options[:title] = arg
|
107
|
+
else
|
108
|
+
abort "invalid argument: #{arg}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
options[:project] = base_project
|
114
|
+
options[:base] ||= master_branch.short_name
|
115
|
+
|
116
|
+
if tracked_branch = options[:head].nil? && current_branch.upstream
|
117
|
+
if base_project == head_project and tracked_branch.short_name == options[:base]
|
118
|
+
$stderr.puts "Aborted: head branch is the same as base (#{options[:base].inspect})"
|
119
|
+
warn "(use `-h <branch>` to specify an explicit pull request head)"
|
120
|
+
abort
|
121
|
+
end
|
122
|
+
end
|
123
|
+
options[:head] ||= (tracked_branch || current_branch).short_name
|
124
|
+
|
125
|
+
if head_project.owner != github_user and !tracked_branch and !explicit_owner
|
126
|
+
head_project = github_project(head_project.name, github_user)
|
127
|
+
end
|
128
|
+
|
129
|
+
remote_branch = "#{head_project.remote}/#{options[:head]}"
|
130
|
+
options[:head] = "#{head_project.owner}:#{options[:head]}"
|
131
|
+
|
132
|
+
if !force and tracked_branch and local_commits = git_command("rev-list --cherry #{remote_branch}...")
|
133
|
+
$stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{remote_branch}"
|
134
|
+
warn "(use `-f` to force submit a pull request anyway)"
|
135
|
+
abort
|
136
|
+
end
|
137
|
+
|
138
|
+
if args.noop?
|
139
|
+
puts "Would reqest a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
|
140
|
+
exit
|
141
|
+
end
|
142
|
+
|
143
|
+
unless options[:title] or options[:issue]
|
144
|
+
base_branch = "#{base_project.remote}/#{options[:base]}"
|
145
|
+
changes = git_command "log --no-color --pretty=medium --cherry %s...%s" %
|
146
|
+
[base_branch, remote_branch]
|
147
|
+
|
148
|
+
options[:title], options[:body] = pullrequest_editmsg(changes) { |msg|
|
149
|
+
msg.puts "# Requesting a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
|
150
|
+
msg.puts "#"
|
151
|
+
msg.puts "# Write a message for this pull request. The first block"
|
152
|
+
msg.puts "# of text is the title and the rest is description."
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
pull = create_pullrequest(options)
|
157
|
+
|
158
|
+
args.executable = 'echo'
|
159
|
+
args.replace [pull['html_url']]
|
160
|
+
rescue HTTPExceptions
|
161
|
+
display_http_exception("creating pull request", $!.response)
|
162
|
+
exit 1
|
163
|
+
end
|
164
|
+
|
165
|
+
# $ hub clone rtomayko/tilt
|
166
|
+
# > git clone git://github.com/rtomayko/tilt.
|
167
|
+
#
|
168
|
+
# $ hub clone -p kneath/hemingway
|
169
|
+
# > git clone git@github.com:kneath/hemingway.git
|
170
|
+
#
|
171
|
+
# $ hub clone tilt
|
172
|
+
# > git clone git://github.com/YOUR_LOGIN/tilt.
|
173
|
+
#
|
174
|
+
# $ hub clone -p github
|
175
|
+
# > git clone git@github.com:YOUR_LOGIN/hemingway.git
|
176
|
+
def clone(args)
|
177
|
+
ssh = args.delete('-p')
|
178
|
+
has_values = /^(--(upload-pack|template|depth|origin|branch|reference)|-[ubo])$/
|
179
|
+
|
180
|
+
idx = 1
|
181
|
+
while idx < args.length
|
182
|
+
arg = args[idx]
|
183
|
+
if arg.index('-') == 0
|
184
|
+
idx += 1 if arg =~ has_values
|
185
|
+
else
|
186
|
+
# $ hub clone rtomayko/tilt
|
187
|
+
# $ hub clone tilt
|
188
|
+
if arg =~ NAME_WITH_OWNER_RE
|
189
|
+
project = github_project(arg)
|
190
|
+
ssh ||= args[0] != 'submodule' && project.owner == github_user(false)
|
191
|
+
args[idx] = project.git_url(:private => ssh, :https => https_protocol?)
|
192
|
+
end
|
193
|
+
break
|
194
|
+
end
|
195
|
+
idx += 1
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# $ hub submodule add wycats/bundler vendor/bundler
|
200
|
+
# > git submodule add git://github.com/wycats/bundler.git vendor/bundler
|
201
|
+
#
|
202
|
+
# $ hub submodule add -p wycats/bundler vendor/bundler
|
203
|
+
# > git submodule add git@github.com:wycats/bundler.git vendor/bundler
|
204
|
+
#
|
205
|
+
# $ hub submodule add -b ryppl ryppl/pip vendor/bundler
|
206
|
+
# > git submodule add -b ryppl git://github.com/ryppl/pip.git vendor/pip
|
207
|
+
def submodule(args)
|
208
|
+
return unless index = args.index('add')
|
209
|
+
args.delete_at index
|
210
|
+
|
211
|
+
branch = args.index('-b') || args.index('--branch')
|
212
|
+
if branch
|
213
|
+
args.delete_at branch
|
214
|
+
branch_name = args.delete_at branch
|
215
|
+
end
|
216
|
+
|
217
|
+
clone(args)
|
218
|
+
|
219
|
+
if branch_name
|
220
|
+
args.insert branch, '-b', branch_name
|
221
|
+
end
|
222
|
+
args.insert index, 'add'
|
223
|
+
end
|
224
|
+
|
225
|
+
# $ hub remote add pjhyett
|
226
|
+
# > git remote add pjhyett git://github.com/pjhyett/THIS_REPO.git
|
227
|
+
#
|
228
|
+
# $ hub remote add -p mojombo
|
229
|
+
# > git remote add mojombo git@github.com:mojombo/THIS_REPO.git
|
230
|
+
#
|
231
|
+
# $ hub remote add origin
|
232
|
+
# > git remote add origin git://github.com/YOUR_LOGIN/THIS_REPO.git
|
233
|
+
def remote(args)
|
234
|
+
if %w[add set-url].include?(args[1])
|
235
|
+
name = args.last
|
236
|
+
if name =~ /^(#{OWNER_RE})$/ || name =~ /^(#{OWNER_RE})\/(#{NAME_RE})$/
|
237
|
+
user, repo = $1, $2 || repo_name
|
238
|
+
end
|
239
|
+
end
|
240
|
+
return unless user # do not touch arguments
|
241
|
+
|
242
|
+
ssh = args.delete('-p')
|
243
|
+
|
244
|
+
if args.words[2] == 'origin' && args.words[3].nil?
|
245
|
+
# Origin special case triggers default user/repo
|
246
|
+
user, repo = github_user, repo_name
|
247
|
+
elsif args.words[-2] == args.words[1]
|
248
|
+
# rtomayko/tilt => rtomayko
|
249
|
+
# Make sure you dance around flags.
|
250
|
+
idx = args.index( args.words[-1] )
|
251
|
+
args[idx] = user
|
252
|
+
else
|
253
|
+
# They're specifying the remote name manually (e.g.
|
254
|
+
# git remote add blah rtomayko/tilt), so just drop the last
|
255
|
+
# argument.
|
256
|
+
args.pop
|
257
|
+
end
|
258
|
+
|
259
|
+
args << git_url(user, repo, :private => ssh)
|
260
|
+
end
|
261
|
+
|
262
|
+
# $ hub fetch mislav
|
263
|
+
# > git remote add mislav git://github.com/mislav/REPO.git
|
264
|
+
# > git fetch mislav
|
265
|
+
#
|
266
|
+
# $ hub fetch --multiple mislav xoebus
|
267
|
+
# > git remote add mislav ...
|
268
|
+
# > git remote add xoebus ...
|
269
|
+
# > git fetch --multiple mislav xoebus
|
270
|
+
def fetch(args)
|
271
|
+
# $ hub fetch --multiple <name1>, <name2>, ...
|
272
|
+
if args.include?('--multiple')
|
273
|
+
names = args.words[1..-1]
|
274
|
+
# $ hub fetch <name>
|
275
|
+
elsif remote_name = args.words[1]
|
276
|
+
# $ hub fetch <name1>,<name2>,...
|
277
|
+
if remote_name =~ /^\w+(,\w+)+$/
|
278
|
+
index = args.index(remote_name)
|
279
|
+
args.delete(remote_name)
|
280
|
+
names = remote_name.split(',')
|
281
|
+
args.insert(index, *names)
|
282
|
+
args.insert(index, '--multiple')
|
283
|
+
else
|
284
|
+
names = [remote_name]
|
285
|
+
end
|
286
|
+
else
|
287
|
+
names = []
|
288
|
+
end
|
289
|
+
|
290
|
+
names.reject! { |name|
|
291
|
+
name =~ /\W/ or remotes.include?(name) or
|
292
|
+
remotes_group(name) or not repo_exists?(name)
|
293
|
+
}
|
294
|
+
|
295
|
+
if names.any?
|
296
|
+
names.each do |name|
|
297
|
+
args.before ['remote', 'add', name, git_url(name)]
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# $ git checkout https://github.com/defunkt/hub/pull/73
|
303
|
+
# > git remote add -f -t feature git://github:com/mislav/hub.git
|
304
|
+
# > git checkout -b mislav-feature mislav/feature
|
305
|
+
def checkout(args)
|
306
|
+
if (2..3) === args.length and args[1] =~ %r{https?://github.com/(.+?)/(.+?)/pull/(\d+)}
|
307
|
+
owner, repo, pull_id = $1, $2, $3
|
308
|
+
|
309
|
+
load_net_http
|
310
|
+
pull_body = Net::HTTP.get URI(API_PULL % File.join(owner, repo, pull_id))
|
311
|
+
|
312
|
+
user, branch = pull_body.match(/"label":\s*"(.+?)"/)[1].split(':', 2)
|
313
|
+
new_branch_name = args[2] || "#{user}-#{branch}"
|
314
|
+
|
315
|
+
if remotes.include? user
|
316
|
+
args.before ['remote', 'set-branches', '--add', user, branch]
|
317
|
+
args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"]
|
318
|
+
else
|
319
|
+
args.before ['remote', 'add', '-f', '-t', branch, user, github_project(repo, user).git_url]
|
320
|
+
end
|
321
|
+
args[1..-1] = ['-b', new_branch_name, "#{user}/#{branch}"]
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# $ git cherry-pick http://github.com/mislav/hub/commit/a319d88#comments
|
326
|
+
# > git remote add -f mislav git://github.com/mislav/hub.git
|
327
|
+
# > git cherry-pick a319d88
|
328
|
+
#
|
329
|
+
# $ git cherry-pick mislav@a319d88
|
330
|
+
# > git remote add -f mislav git://github.com/mislav/hub.git
|
331
|
+
# > git cherry-pick a319d88
|
332
|
+
#
|
333
|
+
# $ git cherry-pick mislav@SHA
|
334
|
+
# > git fetch mislav
|
335
|
+
# > git cherry-pick SHA
|
336
|
+
def cherry_pick(args)
|
337
|
+
unless args.include?('-m') or args.include?('--mainline')
|
338
|
+
case ref = args.words.last
|
339
|
+
when %r{^(?:https?:)//github.com/(.+?)/(.+?)/commit/([a-f0-9]{7,40})}
|
340
|
+
user, repo, sha = $1, $2, $3
|
341
|
+
args[args.index(ref)] = sha
|
342
|
+
when /^(\w+)@([a-f0-9]{7,40})$/
|
343
|
+
user, repo, sha = $1, nil, $2
|
344
|
+
args[args.index(ref)] = sha
|
345
|
+
else
|
346
|
+
user = nil
|
347
|
+
end
|
348
|
+
|
349
|
+
if user
|
350
|
+
if user == repo_owner
|
351
|
+
# fetch from origin if the repo belongs to the user
|
352
|
+
args.before ['fetch', origin_remote]
|
353
|
+
elsif remotes.include?(user)
|
354
|
+
args.before ['fetch', user]
|
355
|
+
else
|
356
|
+
args.before ['remote', 'add', '-f', user, git_url(user, repo)]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# $ hub am https://github.com/defunkt/hub/pull/55
|
363
|
+
# > curl https://github.com/defunkt/hub/pull/55.patch -o /tmp/55.patch
|
364
|
+
# > git am /tmp/55.patch
|
365
|
+
def am(args)
|
366
|
+
if url = args.find { |a| a =~ %r{^https?://(gist\.)?github\.com/} }
|
367
|
+
idx = args.index(url)
|
368
|
+
gist = $1 == 'gist.'
|
369
|
+
# strip extra path from "pull/42/files", "pull/42/commits"
|
370
|
+
url = url.sub(%r{(/pull/\d+)/\w*$}, '\1') unless gist
|
371
|
+
ext = gist ? '.txt' : '.patch'
|
372
|
+
url += ext unless File.extname(url) == ext
|
373
|
+
patch_file = File.join(ENV['TMPDIR'] || '/tmp', "#{gist ? 'gist-' : ''}#{File.basename(url)}")
|
374
|
+
args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file]
|
375
|
+
args[idx] = patch_file
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# $ hub apply https://github.com/defunkt/hub/pull/55
|
380
|
+
# > curl https://github.com/defunkt/hub/pull/55.patch -o /tmp/55.patch
|
381
|
+
# > git apply /tmp/55.patch
|
382
|
+
alias_method :apply, :am
|
383
|
+
|
384
|
+
# $ hub init -g
|
385
|
+
# > git init
|
386
|
+
# > git remote add origin git@github.com:USER/REPO.git
|
387
|
+
def init(args)
|
388
|
+
if args.delete('-g')
|
389
|
+
url = git_url(github_user, repo_name, :private => true)
|
390
|
+
args.after ['remote', 'add', 'origin', url]
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# $ hub fork
|
395
|
+
# ... hardcore forking action ...
|
396
|
+
# > git remote add -f YOUR_USER git@github.com:YOUR_USER/CURRENT_REPO.git
|
397
|
+
def fork(args)
|
398
|
+
# can't do anything without token and original owner name
|
399
|
+
if github_user && github_token && repo_owner
|
400
|
+
if repo_exists?(github_user)
|
401
|
+
warn "#{github_user}/#{repo_name} already exists on GitHub"
|
402
|
+
else
|
403
|
+
fork_repo unless args.noop?
|
404
|
+
end
|
405
|
+
|
406
|
+
if args.include?('--no-remote')
|
407
|
+
exit
|
408
|
+
else
|
409
|
+
url = git_url(github_user, repo_name, :private => true)
|
410
|
+
args.replace %W"remote add -f #{github_user} #{url}"
|
411
|
+
args.after 'echo', ['new remote:', github_user]
|
412
|
+
end
|
413
|
+
end
|
414
|
+
rescue HTTPExceptions
|
415
|
+
display_http_exception("creating fork", $!.response)
|
416
|
+
exit 1
|
417
|
+
end
|
418
|
+
|
419
|
+
# $ hub create
|
420
|
+
# ... create repo on github ...
|
421
|
+
# > git remote add -f origin git@github.com:YOUR_USER/CURRENT_REPO.git
|
422
|
+
def create(args)
|
423
|
+
if !is_repo?
|
424
|
+
abort "'create' must be run from inside a git repository"
|
425
|
+
elsif owner = github_user and github_token
|
426
|
+
args.shift
|
427
|
+
options = {}
|
428
|
+
options[:private] = true if args.delete('-p')
|
429
|
+
new_repo_name = nil
|
430
|
+
|
431
|
+
until args.empty?
|
432
|
+
case arg = args.shift
|
433
|
+
when '-d'
|
434
|
+
options[:description] = args.shift
|
435
|
+
when '-h'
|
436
|
+
options[:homepage] = args.shift
|
437
|
+
else
|
438
|
+
if arg =~ /^[^-]/ and new_repo_name.nil?
|
439
|
+
new_repo_name = arg
|
440
|
+
owner, new_repo_name = new_repo_name.split('/', 2) if new_repo_name.index('/')
|
441
|
+
else
|
442
|
+
abort "invalid argument: #{arg}"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
new_repo_name ||= repo_name
|
447
|
+
repo_with_owner = "#{owner}/#{new_repo_name}"
|
448
|
+
|
449
|
+
if repo_exists?(owner, new_repo_name)
|
450
|
+
warn "#{repo_with_owner} already exists on GitHub"
|
451
|
+
action = "set remote origin"
|
452
|
+
else
|
453
|
+
action = "created repository"
|
454
|
+
create_repo(repo_with_owner, options) unless args.noop?
|
455
|
+
end
|
456
|
+
|
457
|
+
url = git_url(owner, new_repo_name, :private => true)
|
458
|
+
|
459
|
+
if remotes.first != 'origin'
|
460
|
+
args.replace %W"remote add -f origin #{url}"
|
461
|
+
else
|
462
|
+
args.replace %W"remote -v"
|
463
|
+
end
|
464
|
+
|
465
|
+
args.after 'echo', ["#{action}:", repo_with_owner]
|
466
|
+
end
|
467
|
+
rescue HTTPExceptions
|
468
|
+
display_http_exception("creating repository", $!.response)
|
469
|
+
exit 1
|
470
|
+
end
|
471
|
+
|
472
|
+
# $ hub push origin,staging cool-feature
|
473
|
+
# > git push origin cool-feature
|
474
|
+
# > git push staging cool-feature
|
475
|
+
def push(args)
|
476
|
+
return if args[1].nil? || !args[1].index(',')
|
477
|
+
|
478
|
+
branch = (args[2] ||= current_branch.short_name)
|
479
|
+
remotes = args[1].split(',')
|
480
|
+
args[1] = remotes.shift
|
481
|
+
|
482
|
+
remotes.each do |name|
|
483
|
+
args.after ['push', name, branch]
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# $ hub browse
|
488
|
+
# > open https://github.com/CURRENT_REPO
|
489
|
+
#
|
490
|
+
# $ hub browse -- issues
|
491
|
+
# > open https://github.com/CURRENT_REPO/issues
|
492
|
+
#
|
493
|
+
# $ hub browse pjhyett/github-services
|
494
|
+
# > open https://github.com/pjhyett/github-services
|
495
|
+
#
|
496
|
+
# $ hub browse github-services
|
497
|
+
# > open https://github.com/YOUR_LOGIN/github-services
|
498
|
+
#
|
499
|
+
# $ hub browse github-services wiki
|
500
|
+
# > open https://github.com/YOUR_LOGIN/github-services/wiki
|
501
|
+
def browse(args)
|
502
|
+
args.shift
|
503
|
+
browse_command(args) do
|
504
|
+
dest = args.shift
|
505
|
+
dest = nil if dest == '--'
|
506
|
+
|
507
|
+
if dest
|
508
|
+
# $ hub browse pjhyett/github-services
|
509
|
+
# $ hub browse github-services
|
510
|
+
project = github_project dest
|
511
|
+
else
|
512
|
+
# $ hub browse
|
513
|
+
project = current_project
|
514
|
+
end
|
515
|
+
|
516
|
+
abort "Usage: hub browse [<USER>/]<REPOSITORY>" unless project
|
517
|
+
|
518
|
+
# $ hub browse -- wiki
|
519
|
+
path = case subpage = args.shift
|
520
|
+
when 'commits'
|
521
|
+
branch = (!dest && current_branch.upstream) || master_branch
|
522
|
+
"/commits/#{branch.short_name}"
|
523
|
+
when 'tree', NilClass
|
524
|
+
branch = !dest && current_branch.upstream
|
525
|
+
"/tree/#{branch.short_name}" if branch and !branch.master?
|
526
|
+
else
|
527
|
+
"/#{subpage}"
|
528
|
+
end
|
529
|
+
|
530
|
+
project.web_url(path)
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# $ hub compare 1.0..fix
|
535
|
+
# > open https://github.com/CURRENT_REPO/compare/1.0...fix
|
536
|
+
# $ hub compare refactor
|
537
|
+
# > open https://github.com/CURRENT_REPO/compare/refactor
|
538
|
+
# $ hub compare myfork feature
|
539
|
+
# > open https://github.com/myfork/REPO/compare/feature
|
540
|
+
# $ hub compare -u 1.0...2.0
|
541
|
+
# "https://github.com/CURRENT_REPO/compare/1.0...2.0"
|
542
|
+
def compare(args)
|
543
|
+
args.shift
|
544
|
+
browse_command(args) do
|
545
|
+
if args.empty?
|
546
|
+
branch = current_branch.upstream
|
547
|
+
if branch and not branch.master?
|
548
|
+
range = branch.short_name
|
549
|
+
project = current_project
|
550
|
+
else
|
551
|
+
abort "Usage: hub compare [USER] [<START>...]<END>"
|
552
|
+
end
|
553
|
+
else
|
554
|
+
sha_or_tag = /(\w{1,2}|\w[\w.-]+\w)/
|
555
|
+
# replaces two dots with three: "sha1...sha2"
|
556
|
+
range = args.pop.sub(/^#{sha_or_tag}\.\.#{sha_or_tag}$/, '\1...\2')
|
557
|
+
project = if owner = args.pop then github_project(nil, owner)
|
558
|
+
else current_project
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
project.web_url "/compare/#{range}"
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
# $ hub hub standalone
|
567
|
+
# Prints the "standalone" version of hub for an easy, memorable
|
568
|
+
# installation sequence:
|
569
|
+
#
|
570
|
+
# $ gem install hub
|
571
|
+
# $ hub hub standalone > ~/bin/hub && chmod 755 ~/bin/hub
|
572
|
+
# $ gem uninstall hub
|
573
|
+
def hub(args)
|
574
|
+
return help(args) unless args[1] == 'standalone'
|
575
|
+
require 'hub/standalone'
|
576
|
+
$stdout.puts Hub::Standalone.build
|
577
|
+
exit
|
578
|
+
rescue LoadError
|
579
|
+
abort "hub is running in standalone mode."
|
580
|
+
end
|
581
|
+
|
582
|
+
def alias(args)
|
583
|
+
shells = {
|
584
|
+
'sh' => 'alias git=hub',
|
585
|
+
'bash' => 'alias git=hub',
|
586
|
+
'zsh' => 'function git(){hub "$@"}',
|
587
|
+
'csh' => 'alias git hub',
|
588
|
+
'fish' => 'alias git hub'
|
589
|
+
}
|
590
|
+
|
591
|
+
silent = args.delete('-s')
|
592
|
+
|
593
|
+
if shell = args[1]
|
594
|
+
if silent.nil?
|
595
|
+
puts "Run this in your shell to start using `hub` as `git`:"
|
596
|
+
print " "
|
597
|
+
end
|
598
|
+
else
|
599
|
+
puts "usage: hub alias [-s] SHELL", ""
|
600
|
+
puts "You already have hub installed and available in your PATH,"
|
601
|
+
puts "but to get the full experience you'll want to alias it to"
|
602
|
+
puts "`git`.", ""
|
603
|
+
puts "To see how to accomplish this for your shell, run the alias"
|
604
|
+
puts "command again with the name of your shell.", ""
|
605
|
+
puts "Known shells:"
|
606
|
+
shells.map { |key, _| key }.sort.each do |key|
|
607
|
+
puts " " + key
|
608
|
+
end
|
609
|
+
puts "", "Options:"
|
610
|
+
puts " -s Silent. Useful when using the output with eval, e.g."
|
611
|
+
puts " $ eval `hub alias -s bash`"
|
612
|
+
|
613
|
+
exit
|
614
|
+
end
|
615
|
+
|
616
|
+
if shells[shell]
|
617
|
+
puts shells[shell]
|
618
|
+
else
|
619
|
+
abort "fatal: never heard of `#{shell}'"
|
620
|
+
end
|
621
|
+
|
622
|
+
exit
|
623
|
+
end
|
624
|
+
|
625
|
+
# $ hub version
|
626
|
+
# > git version
|
627
|
+
# (print hub version)
|
628
|
+
def version(args)
|
629
|
+
args.after 'echo', ['hub version', Version]
|
630
|
+
end
|
631
|
+
alias_method "--version", :version
|
632
|
+
|
633
|
+
# $ hub help
|
634
|
+
# (print improved help text)
|
635
|
+
def help(args)
|
636
|
+
command = args.words[1]
|
637
|
+
|
638
|
+
if command == 'hub'
|
639
|
+
puts hub_manpage
|
640
|
+
exit
|
641
|
+
elsif command.nil? && !args.has_flag?('-a', '--all')
|
642
|
+
ENV['GIT_PAGER'] = '' unless args.has_flag?('-p', '--paginate') # Use `cat`.
|
643
|
+
puts improved_help_text
|
644
|
+
exit
|
645
|
+
end
|
646
|
+
end
|
647
|
+
alias_method "--help", :help
|
648
|
+
|
649
|
+
private
|
650
|
+
#
|
651
|
+
# Helper methods are private so they cannot be invoked
|
652
|
+
# from the command line.
|
653
|
+
#
|
654
|
+
|
655
|
+
# The text print when `hub help` is run, kept in its own method
|
656
|
+
# for the convenience of the author.
|
657
|
+
def improved_help_text
|
658
|
+
<<-help
|
659
|
+
usage: git [--version] [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
|
660
|
+
[-p|--paginate|--no-pager] [--no-replace-objects] [--bare]
|
661
|
+
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
|
662
|
+
[-c name=value] [--help]
|
663
|
+
<command> [<args>]
|
664
|
+
|
665
|
+
Basic Commands:
|
666
|
+
init Create an empty git repository or reinitialize an existing one
|
667
|
+
add Add new or modified files to the staging area
|
668
|
+
rm Remove files from the working directory and staging area
|
669
|
+
mv Move or rename a file, a directory, or a symlink
|
670
|
+
status Show the status of the working directory and staging area
|
671
|
+
commit Record changes to the repository
|
672
|
+
|
673
|
+
History Commands:
|
674
|
+
log Show the commit history log
|
675
|
+
diff Show changes between commits, commit and working tree, etc
|
676
|
+
show Show information about commits, tags or files
|
677
|
+
|
678
|
+
Branching Commands:
|
679
|
+
branch List, create, or delete branches
|
680
|
+
checkout Switch the active branch to another branch
|
681
|
+
merge Join two or more development histories (branches) together
|
682
|
+
tag Create, list, delete, sign or verify a tag object
|
683
|
+
|
684
|
+
Remote Commands:
|
685
|
+
clone Clone a remote repository into a new directory
|
686
|
+
fetch Download data, tags and branches from a remote repository
|
687
|
+
pull Fetch from and merge with another repository or a local branch
|
688
|
+
push Upload data, tags and branches to a remote repository
|
689
|
+
remote View and manage a set of remote repositories
|
690
|
+
|
691
|
+
Advanced commands:
|
692
|
+
reset Reset your staging area or working directory to another point
|
693
|
+
rebase Re-apply a series of patches in one branch onto another
|
694
|
+
bisect Find by binary search the change that introduced a bug
|
695
|
+
grep Print files with lines matching a pattern in your codebase
|
696
|
+
|
697
|
+
See 'git help <command>' for more information on a specific command.
|
698
|
+
help
|
699
|
+
end
|
700
|
+
|
701
|
+
# Extract global flags from the front of the arguments list.
|
702
|
+
# Makes sure important ones are supplied for calls to subcommands.
|
703
|
+
#
|
704
|
+
# Known flags are:
|
705
|
+
# --version --exec-path=<path> --html-path
|
706
|
+
# -p|--paginate|--no-pager --no-replace-objects
|
707
|
+
# --bare --git-dir=<path> --work-tree=<path>
|
708
|
+
# -c name=value --help
|
709
|
+
#
|
710
|
+
# Special: `--version`, `--help` are replaced with "version" and "help".
|
711
|
+
# Ignored: `--exec-path`, `--html-path` are kept in args list untouched.
|
712
|
+
def slurp_global_flags(args)
|
713
|
+
flags = %w[ --noop -c -p --paginate --no-pager --no-replace-objects --bare --version --help ]
|
714
|
+
flags2 = %w[ --exec-path= --git-dir= --work-tree= ]
|
715
|
+
|
716
|
+
# flags that should be present in subcommands, too
|
717
|
+
globals = []
|
718
|
+
# flags that apply only to main command
|
719
|
+
locals = []
|
720
|
+
|
721
|
+
while args[0] && (flags.include?(args[0]) || flags2.any? {|f| args[0].index(f) == 0 })
|
722
|
+
flag = args.shift
|
723
|
+
case flag
|
724
|
+
when '--noop'
|
725
|
+
args.noop!
|
726
|
+
when '--version', '--help'
|
727
|
+
args.unshift flag.sub('--', '')
|
728
|
+
when '-c'
|
729
|
+
# slurp one additional argument
|
730
|
+
config_pair = args.shift
|
731
|
+
# add configuration to our local cache
|
732
|
+
key, value = config_pair.split('=', 2)
|
733
|
+
git_reader.stub_config_value(key, value)
|
734
|
+
|
735
|
+
globals << flag << config_pair
|
736
|
+
when '-p', '--paginate', '--no-pager'
|
737
|
+
locals << flag
|
738
|
+
else
|
739
|
+
globals << flag
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
git_reader.add_exec_flags(globals)
|
744
|
+
args.add_exec_flags(globals)
|
745
|
+
args.add_exec_flags(locals)
|
746
|
+
end
|
747
|
+
|
748
|
+
# Handles common functionality of browser commands like `browse`
|
749
|
+
# and `compare`. Yields a block that returns params for `github_url`.
|
750
|
+
def browse_command(args)
|
751
|
+
url_only = args.delete('-u')
|
752
|
+
warn "Warning: the `-p` flag has no effect anymore" if args.delete('-p')
|
753
|
+
url = yield
|
754
|
+
|
755
|
+
args.executable = url_only ? 'echo' : browser_launcher
|
756
|
+
args.push url
|
757
|
+
end
|
758
|
+
|
759
|
+
# Returns the terminal-formatted manpage, ready to be printed to
|
760
|
+
# the screen.
|
761
|
+
def hub_manpage
|
762
|
+
abort "** Can't find groff(1)" unless command?('groff')
|
763
|
+
|
764
|
+
require 'open3'
|
765
|
+
out = nil
|
766
|
+
Open3.popen3(groff_command) do |stdin, stdout, _|
|
767
|
+
stdin.puts hub_raw_manpage
|
768
|
+
stdin.close
|
769
|
+
out = stdout.read.strip
|
770
|
+
end
|
771
|
+
out
|
772
|
+
end
|
773
|
+
|
774
|
+
# The groff command complete with crazy arguments we need to run
|
775
|
+
# in order to turn our raw roff (manpage markup) into something
|
776
|
+
# readable on the terminal.
|
777
|
+
def groff_command
|
778
|
+
"groff -Wall -mtty-char -mandoc -Tascii"
|
779
|
+
end
|
780
|
+
|
781
|
+
# Returns the raw hub manpage. If we're not running in standalone
|
782
|
+
# mode, it's a file sitting at the root under the `man`
|
783
|
+
# directory.
|
784
|
+
#
|
785
|
+
# If we are running in standalone mode the manpage will be
|
786
|
+
# included after the __END__ of the file so we can grab it using
|
787
|
+
# DATA.
|
788
|
+
def hub_raw_manpage
|
789
|
+
if File.exists? file = File.dirname(__FILE__) + '/../../man/hub.1'
|
790
|
+
File.read(file)
|
791
|
+
else
|
792
|
+
DATA.read
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
# All calls to `puts` in after hooks or commands are paged,
|
797
|
+
# git-style.
|
798
|
+
def puts(*args)
|
799
|
+
page_stdout
|
800
|
+
super
|
801
|
+
end
|
802
|
+
|
803
|
+
# http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
|
804
|
+
def page_stdout
|
805
|
+
return if not $stdout.tty? or windows?
|
806
|
+
|
807
|
+
read, write = IO.pipe
|
808
|
+
|
809
|
+
if Kernel.fork
|
810
|
+
# Parent process, become pager
|
811
|
+
$stdin.reopen(read)
|
812
|
+
read.close
|
813
|
+
write.close
|
814
|
+
|
815
|
+
# Don't page if the input is short enough
|
816
|
+
ENV['LESS'] = 'FSRX'
|
817
|
+
|
818
|
+
# Wait until we have input before we start the pager
|
819
|
+
Kernel.select [STDIN]
|
820
|
+
|
821
|
+
pager = ENV['GIT_PAGER'] ||
|
822
|
+
`git config --get-all core.pager`.split.first || ENV['PAGER'] ||
|
823
|
+
'less -isr'
|
824
|
+
|
825
|
+
pager = 'cat' if pager.empty?
|
826
|
+
|
827
|
+
exec pager rescue exec "/bin/sh", "-c", pager
|
828
|
+
else
|
829
|
+
# Child process
|
830
|
+
$stdout.reopen(write)
|
831
|
+
$stderr.reopen(write) if $stderr.tty?
|
832
|
+
read.close
|
833
|
+
write.close
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
837
|
+
# Determines whether a user has a fork of the current repo on GitHub.
|
838
|
+
def repo_exists?(user, repo = repo_name)
|
839
|
+
load_net_http
|
840
|
+
url = API_REPO % [user, repo]
|
841
|
+
Net::HTTPSuccess === Net::HTTP.get_response(URI(url))
|
842
|
+
end
|
843
|
+
|
844
|
+
# Forks the current repo using the GitHub API.
|
845
|
+
#
|
846
|
+
# Returns nothing.
|
847
|
+
def fork_repo
|
848
|
+
load_net_http
|
849
|
+
response = http_post API_FORK % [repo_owner, repo_name]
|
850
|
+
response.error! unless Net::HTTPSuccess === response
|
851
|
+
end
|
852
|
+
|
853
|
+
# Creates a new repo using the GitHub API.
|
854
|
+
#
|
855
|
+
# Returns nothing.
|
856
|
+
def create_repo(name, options = {})
|
857
|
+
params = {'name' => name.sub(/^#{github_user}\//, '')}
|
858
|
+
params['public'] = '0' if options[:private]
|
859
|
+
params['description'] = options[:description] if options[:description]
|
860
|
+
params['homepage'] = options[:homepage] if options[:homepage]
|
861
|
+
|
862
|
+
load_net_http
|
863
|
+
response = http_post(API_CREATE, params)
|
864
|
+
response.error! unless Net::HTTPSuccess === response
|
865
|
+
end
|
866
|
+
|
867
|
+
# Returns parsed data from the new pull request.
|
868
|
+
def create_pullrequest(options)
|
869
|
+
project = options.fetch(:project)
|
870
|
+
params = {
|
871
|
+
'pull[base]' => options.fetch(:base),
|
872
|
+
'pull[head]' => options.fetch(:head)
|
873
|
+
}
|
874
|
+
params['pull[issue]'] = options[:issue] if options[:issue]
|
875
|
+
params['pull[title]'] = options[:title] if options[:title]
|
876
|
+
params['pull[body]'] = options[:body] if options[:body]
|
877
|
+
|
878
|
+
load_net_http
|
879
|
+
response = http_post(API_PULLREQUEST % [project.owner, project.name], params)
|
880
|
+
response.error! unless Net::HTTPSuccess === response
|
881
|
+
# GitHub bug: although we request YAML, it returns JSON
|
882
|
+
if response['Content-type'].to_s.include? 'application/json'
|
883
|
+
{ "html_url" => response.body.match(/"html_url":\s*"(.+?)"/)[1] }
|
884
|
+
else
|
885
|
+
require 'yaml'
|
886
|
+
YAML.load(response.body)['pull']
|
887
|
+
end
|
888
|
+
end
|
889
|
+
|
890
|
+
def pullrequest_editmsg(changes)
|
891
|
+
message_file = File.join(git_dir, 'PULLREQ_EDITMSG')
|
892
|
+
File.open(message_file, 'w') { |msg|
|
893
|
+
msg.puts
|
894
|
+
yield msg
|
895
|
+
if changes
|
896
|
+
msg.puts "#\n# Changes:\n#"
|
897
|
+
msg.puts changes.gsub(/^/, '# ').gsub(/ +$/, '')
|
898
|
+
end
|
899
|
+
}
|
900
|
+
edit_cmd = Array(git_editor).dup << message_file
|
901
|
+
system(*edit_cmd)
|
902
|
+
abort "can't open text editor for pull request message" unless $?.success?
|
903
|
+
title, body = read_editmsg(message_file)
|
904
|
+
abort "Aborting due to empty pull request title" unless title
|
905
|
+
[title, body]
|
906
|
+
end
|
907
|
+
|
908
|
+
def read_editmsg(file)
|
909
|
+
title, body = '', ''
|
910
|
+
File.open(file, 'r') { |msg|
|
911
|
+
msg.each_line do |line|
|
912
|
+
next if line.index('#') == 0
|
913
|
+
((body.empty? and line =~ /\S/) ? title : body) << line
|
914
|
+
end
|
915
|
+
}
|
916
|
+
title.tr!("\n", ' ')
|
917
|
+
title.strip!
|
918
|
+
body.strip!
|
919
|
+
|
920
|
+
[title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil]
|
921
|
+
end
|
922
|
+
|
923
|
+
def expand_alias(cmd)
|
924
|
+
if expanded = git_alias_for(cmd)
|
925
|
+
if expanded.index('!') != 0
|
926
|
+
require 'shellwords' unless defined?(::Shellwords)
|
927
|
+
Shellwords.shellwords(expanded)
|
928
|
+
end
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
def http_post(url, params = nil)
|
933
|
+
url = URI(url)
|
934
|
+
post = Net::HTTP::Post.new(url.request_uri)
|
935
|
+
post.basic_auth "#{github_user}/token", github_token
|
936
|
+
post.set_form_data params if params
|
937
|
+
|
938
|
+
port = url.port
|
939
|
+
if use_ssl = 'https' == url.scheme and not use_ssl?
|
940
|
+
# ruby compiled without openssl
|
941
|
+
use_ssl = false
|
942
|
+
port = 80
|
943
|
+
end
|
944
|
+
|
945
|
+
http = Net::HTTP.new(url.host, port)
|
946
|
+
if http.use_ssl = use_ssl
|
947
|
+
# TODO: SSL peer verification
|
948
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
949
|
+
end
|
950
|
+
http.start { http.request(post) }
|
951
|
+
end
|
952
|
+
|
953
|
+
def load_net_http
|
954
|
+
require 'net/https'
|
955
|
+
rescue LoadError
|
956
|
+
require 'net/http'
|
957
|
+
end
|
958
|
+
|
959
|
+
def use_ssl?
|
960
|
+
defined? ::OpenSSL
|
961
|
+
end
|
962
|
+
|
963
|
+
# Fake exception type for net/http exception handling.
|
964
|
+
# Necessary because net/http may or may not be loaded at the time.
|
965
|
+
module HTTPExceptions
|
966
|
+
def self.===(exception)
|
967
|
+
exception.class.ancestors.map {|a| a.to_s }.include? 'Net::HTTPExceptions'
|
968
|
+
end
|
969
|
+
end
|
970
|
+
|
971
|
+
def display_http_exception(action, response)
|
972
|
+
$stderr.puts "Error #{action}: #{response.message} (HTTP #{response.code})"
|
973
|
+
warn "Check your token configuration (`git config github.token`)" if response.code.to_i == 401
|
974
|
+
end
|
975
|
+
|
976
|
+
end
|
977
|
+
end
|