diff --git a/README.md b/README.md
index 8601af39..1f736130 100644
--- a/README.md
+++ b/README.md
@@ -190,11 +190,12 @@ superpowers:
### git pull-request
# while on a topic branch called "feature":
- $ git pull-request "I've implemented feature X"
+ $ git pull-request
+ [ opens text editor to edit title & body for the request ]
[ opened pull request on GitHub for "YOUR_USER:feature" ]
- # explicit pull base & head:
- $ git pull-request -b defunkt:master -h mislav:feature
+ # explicit title, pull base & head:
+ $ git pull-request "I've implemented feature X" -b defunkt:master -h mislav:feature
$ git pull-request #123
[ attached pull request to issue #123 ]
diff --git a/lib/hub/commands.rb b/lib/hub/commands.rb
index 701cb063..84a1bb77 100644
--- a/lib/hub/commands.rb
+++ b/lib/hub/commands.rb
@@ -73,11 +73,13 @@ module Hub
def pull_request(args)
args.shift
options = { }
- explicit_owner = false
+ force = explicit_owner = false
base_repo = origin_repo
while arg = args.shift
case arg
+ when '-f'
+ force = true
when '-b'
options[:base] = Context::Ref.from_github_ref(args.shift, origin_repo)
when '-h'
@@ -105,6 +107,21 @@ module Hub
options[:head].repo = head_repo.from_owner(github_user)
end
+ if not force and not explicit_owner and local_commits = Context::GIT_CONFIG["rev-list --cherry #{options[:head].to_local_ref}..."]
+ $stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{options[:head].to_local_ref}"
+ warn "(use `-f` to force submit a pull request anyway)"
+ abort
+ end
+
+ unless options[:title] or options[:issue]
+ changes = Context::GIT_CONFIG["log --no-color --pretty=medium --cherry %s...%s" %
+ [options[:base].to_local_ref, options[:head].to_local_ref]]
+
+ options[:title], options[:body] = pullrequest_editmsg(changes) { |msg|
+ msg.puts "# You're requesting a pull to #{options[:base].to_github_ref} from #{options[:head].to_github_ref}"
+ }
+ end
+
pull = create_pullrequest(options)
args.executable = 'echo'
@@ -805,6 +822,39 @@ help
YAML.load(response.body)['pull']
end
+ def pullrequest_editmsg(changes)
+ message_file = File.join(git_dir, 'PULLREQ_EDITMSG')
+ File.open(message_file, 'w') { |msg|
+ msg.puts
+ yield msg
+ if changes
+ msg.puts "#\n# Changes:\n#"
+ msg.puts changes.gsub(/^/, '# ')
+ end
+ }
+ edit_cmd = Array(git_editor).dup << message_file
+ system(*edit_cmd)
+ abort "can't open text editor for pull request message" unless $?.success?
+ title, body = read_editmsg(message_file)
+ abort "Aborting due to empty pull request title" unless title
+ [title, body]
+ end
+
+ def read_editmsg(file)
+ title, body = '', ''
+ File.open(file, 'r') { |msg|
+ msg.each_line do |line|
+ next if line.index('#') == 0
+ ((body.empty? and line =~ /\S/) ? title : body) << line
+ end
+ }
+ title.tr!("\n", ' ')
+ title.strip!
+ body.strip!
+
+ [title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil]
+ end
+
def expand_alias(cmd)
if expanded = git_alias_for(cmd)
if expanded.index('!') != 0
diff --git a/lib/hub/context.rb b/lib/hub/context.rb
index c2f9300b..623980a5 100644
--- a/lib/hub/context.rb
+++ b/lib/hub/context.rb
@@ -279,6 +279,14 @@ module Hub
!!git_dir
end
+ def git_editor
+ # possible: ~/bin/vi, $SOME_ENVIRONMENT_VARIABLE, "C:\Program Files\Vim\gvim.exe" --nofork
+ editor = GIT_CONFIG['var GIT_EDITOR']
+ editor = ENV[$1] if editor =~ /^\$(\w+)$/
+ editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/
+ editor.shellsplit
+ end
+
# Cross-platform web browser command; respects the value set in $BROWSER.
#
# Returns an array, e.g.: ['open']
diff --git a/man/hub.1 b/man/hub.1
index e95bd1c4..b7584f30 100644
--- a/man/hub.1
+++ b/man/hub.1
@@ -55,7 +55,7 @@
\fBgit fork\fR [\fB\-\-no\-remote\fR]
.
.br
-\fBgit pull\-request\fR [\fITITLE\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR]
+\fBgit pull\-request\fR [\fB\-f\fR] [\fITITLE\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR]
.
.SH "DESCRIPTION"
\fBhub\fR enhances various \fBgit\fR commands with GitHub remote expansion\. The alias command displays information on configuring your environment:
@@ -124,10 +124,13 @@ Submodule repository "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" into \
\fBgit fork\fR [\fB\-\-no\-remote\fR]: Forks the original project (referenced by "origin" remote) on GitHub and adds a new remote for it under your username\. Requires \fBgithub\.token\fR to be set (see CONFIGURATION)\.
.
.IP "\(bu" 4
-\fBgit pull\-request\fR [\fITITLE\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR]:
+\fBgit pull\-request\fR [\fB\-f\fR] [\fITITLE\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR]:
.
.br
-Opens a pull request on GitHub for the project that the "origin" remote points to\. The default head of the pull request is the current branch\. Both base and head of the pull request can be explicitly given in one of the following formats: "branch", "owner:branch", "owner/repo:branch"\.
+Opens a pull request on GitHub for the project that the "origin" remote points to\. The default head of the pull request is the current branch\. Both base and head of the pull request can be explicitly given in one of the following formats: "branch", "owner:branch", "owner/repo:branch"\. This command will abort operation if it detects that the current topic branch has local commits that are not yet pushed to its upstream branch on the remote\. To skip this check, use \fB\-f\fR\.
+.
+.IP
+If \fITITLE\fR is omitted, a text editor will open in which title and body of the pull request can be entered in the same manner as git commit message\.
.
.IP
If instead of normal \fITITLE\fR a string in "#\fINUMBER\fR" format is given, the pull request will be attached to an existing GitHub issue whose ID corresponds to \fINUMBER\fR\.
@@ -292,11 +295,12 @@ $ git fork
.nf
# while on a topic branch called "feature":
-$ git pull\-request "I\'ve implemented feature X"
+$ git pull\-request
+[ opens text editor to edit title & body for the request ]
[ opened pull request on GitHub for "YOUR_USER:feature" ]
-# explicit pull base & head:
-$ git pull\-request \-b defunkt:master \-h mislav:feature
+# explicit title, pull base & head:
+$ git pull\-request "I\'ve implemented feature X" \-b defunkt:master \-h mislav:feature
$ git pull\-request #123
[ attached pull request to issue #123 ]
diff --git a/man/hub.1.html b/man/hub.1.html
index 14ebf8ec..c09e081c 100644
--- a/man/hub.1.html
+++ b/man/hub.1.html
@@ -93,7 +93,7 @@
git compare
[-u
] [USER] [START...]END
git submodule add
[-p
] OPTIONS [USER/]REPOSITORY DIRECTORY
git fork
[--no-remote
]
-git pull-request
[TITLE] [-b
BASE] [-h
HEAD]
git pull-request
[-f
] [TITLE] [-b
BASE] [-h
HEAD]
-p
, use private remote
Forks the original project (referenced by "origin" remote) on GitHub and
adds a new remote for it under your username. Requires github.token
to
be set (see CONFIGURATION).
-git pull-request
[TITLE] [-b
BASE] [-h
HEAD]:
+
git pull-request
[-f
] [TITLE] [-b
BASE] [-h
HEAD]:
Opens a pull request on GitHub for the project that the "origin" remote
points to. The default head of the pull request is the current branch.
Both base and head of the pull request can be explicitly given in one of
-the following formats: "branch", "owner:branch", "owner/repo:branch".
-f
.
+
+If TITLE is omitted, a text editor will open in which title and body of +the pull request can be entered in the same manner as git commit message.
If instead of normal TITLE a string in "#NUMBER" format is given, the pull request will be attached to an existing GitHub issue whose ID @@ -307,11 +313,12 @@ $ git apply https://gist.github.com/8da7fb575debd88c54cf
# while on a topic branch called "feature":
-$ git pull-request "I've implemented feature X"
+$ git pull-request
+[ opens text editor to edit title & body for the request ]
[ opened pull request on GitHub for "YOUR_USER:feature" ]
-# explicit pull base & head:
-$ git pull-request -b defunkt:master -h mislav:feature
+# explicit title, pull base & head:
+$ git pull-request "I've implemented feature X" -b defunkt:master -h mislav:feature
$ git pull-request #123
[ attached pull request to issue #123 ]
diff --git a/man/hub.1.ronn b/man/hub.1.ronn
index 37eb4379..846e939e 100644
--- a/man/hub.1.ronn
+++ b/man/hub.1.ronn
@@ -20,7 +20,7 @@ hub(1) -- git + hub = github
`git compare` [`-u`] [] [...]
`git submodule add` [`-p`] [/]
`git fork` [`--no-remote`]
-`git pull-request` [] [`-b` ] [`-h` ]
+`git pull-request` [`-f`] [] [`-b` ] [`-h` ]
## DESCRIPTION
@@ -116,11 +116,17 @@ alias command displays information on configuring your environment:
adds a new remote for it under your username. Requires `github.token` to
be set (see CONFIGURATION).
- * `git pull-request` [] [`-b` ] [`-h` ]:
+ * `git pull-request` [`-f`] [] [`-b` ] [`-h` ]:
Opens a pull request on GitHub for the project that the "origin" remote
points to. The default head of the pull request is the current branch.
Both base and head of the pull request can be explicitly given in one of
the following formats: "branch", "owner:branch", "owner/repo:branch".
+ This command will abort operation if it detects that the current topic
+ branch has local commits that are not yet pushed to its upstream branch
+ on the remote. To skip this check, use `-f`.
+
+ If is omitted, a text editor will open in which title and body of
+ the pull request can be entered in the same manner as git commit message.
If instead of normal a string in "#" format is given,
the pull request will be attached to an existing GitHub issue whose ID
diff --git a/test/hub_test.rb b/test/hub_test.rb
index 3c1ca5d5..d43e2f73 100644
--- a/test/hub_test.rb
+++ b/test/hub_test.rb
@@ -45,7 +45,7 @@ class HubTest < Test::Unit::TestCase
'config github.token' => 'abc123',
'config --get-all remote.origin.url' => 'git://github.com/defunkt/hub.git',
'config --get-all remote.mislav.url' => 'git://github.com/mislav/hub.git',
- "name-rev refs/heads/master@{upstream} --name-only --refs='refs/remotes/*' --no-undefined" => 'remotes/origin/master',
+ "name-rev master@{upstream} --name-only --refs='refs/remotes/*' --no-undefined" => 'remotes/origin/master',
'config --bool hub.http-clone' => 'false',
'rev-parse --git-dir' => '.git'
)
@@ -638,6 +638,14 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
+ assert_output expected, "pull-request hereyougo -f"
+ end
+
+ def test_pullrequest_with_checks
+ @git["rev-list --cherry origin/master..."] = "+abcd1234\n+bcde2345"
+
+ expected = "Aborted: 2 commits are not yet pushed to origin/master\n" <<
+ "(use `-f` to force submit a pull request anyway)\n"
assert_output expected, "pull-request hereyougo"
end
@@ -649,7 +657,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo"
+ assert_output expected, "pull-request hereyougo -f"
end
def test_pullrequest_from_tracking_branch
@@ -660,7 +668,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo"
+ assert_output expected, "pull-request hereyougo -f"
end
def test_pullrequest_explicit_head
@@ -669,7 +677,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo -h yay-feature"
+ assert_output expected, "pull-request hereyougo -h yay-feature -f"
end
def test_pullrequest_explicit_head_with_owner
@@ -678,7 +686,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo -h mojombo:feature"
+ assert_output expected, "pull-request hereyougo -h mojombo:feature -f"
end
def test_pullrequest_explicit_base
@@ -687,7 +695,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo -b feature"
+ assert_output expected, "pull-request hereyougo -b feature -f"
end
def test_pullrequest_explicit_base_with_owner
@@ -696,7 +704,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo -b mojombo:feature"
+ assert_output expected, "pull-request hereyougo -b mojombo:feature -f"
end
def test_pullrequest_explicit_base_with_repo
@@ -705,7 +713,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(1))
expected = "https://github.com/defunkt/hub/pull/1\n"
- assert_output expected, "pull-request hereyougo -b mojombo/hubbub:feature"
+ assert_output expected, "pull-request hereyougo -b mojombo/hubbub:feature -f"
end
def test_pullrequest_existing_issue
@@ -714,7 +722,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(92))
expected = "https://github.com/defunkt/hub/pull/92\n"
- assert_output expected, "pull-request #92"
+ assert_output expected, "pull-request #92 -f"
end
def test_pullrequest_existing_issue_url
@@ -723,7 +731,7 @@ class HubTest < Test::Unit::TestCase
to_return(:body => mock_pullreq_response(92, 'mojombo/hub'))
expected = "https://github.com/mojombo/hub/pull/92\n"
- assert_output expected, "pull-request https://github.com/mojombo/hub/issues/92#comment_4"
+ assert_output expected, "pull-request https://github.com/mojombo/hub/issues/92#comment_4 -f"
end
def test_version
@@ -1005,7 +1013,7 @@ config
def stub_tracking(from, remote_name, remote_branch)
value = remote_branch ? "remotes/#{remote_name}/#{remote_branch}" : nil
- @git["name-rev refs/heads/#{from}@{upstream} --name-only --refs='refs/remotes/*' --no-undefined"] = value
+ @git["name-rev #{from}@{upstream} --name-only --refs='refs/remotes/*' --no-undefined"] = value
end
def stub_tracking_nothing(from = 'master')