loca 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/loca +5 -0
- data/lib/loca.rb +14 -0
- data/lib/loca/cli.rb +47 -0
- data/lib/loca/error.rb +21 -0
- data/lib/loca/git.rb +117 -0
- data/lib/loca/url.rb +37 -0
- data/lib/loca/version.rb +3 -0
- data/spec/e2e/github_spec.rb +43 -0
- data/spec/loca/cli_spec.rb +69 -0
- data/spec/loca/git_spec.rb +152 -0
- data/spec/loca/url_spec.rb +47 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/support/features/github_helper.rb +55 -0
- metadata +224 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1d72e91059bc88a4090bd9eb2c6c8f1e45c13704
|
4
|
+
data.tar.gz: e8fda9734eaf1fc2446fdb85490e792db395d6ee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 71eb60e85d247ce2ee9e14905c685283632262618815a1dccf80af8039a449302f0945af2b40f144358e84ecad73129d72f1adac0183bbd48f6bd4439da611b3
|
7
|
+
data.tar.gz: 129bf8eb437ae3a064b5f9a77e284738eb961f8f6a55ba4c9390b6eba4a91623a73b8664fe200394dd414c083ca6b1e0d16ae348dba6686656889555455258ec
|
data/bin/loca
ADDED
data/lib/loca.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Require all runtime dependencies first
|
2
|
+
require 'thor'
|
3
|
+
require 'colorize'
|
4
|
+
require 'mixlib/shellout'
|
5
|
+
require 'addressable/uri'
|
6
|
+
|
7
|
+
# Start by requiring files with no dependencies on other files, then files with
|
8
|
+
# only external gem runtime dependencies (specified above), then files with
|
9
|
+
# dependencies on other files within the loca project itself
|
10
|
+
require 'loca/version'
|
11
|
+
require 'loca/error'
|
12
|
+
require 'loca/url'
|
13
|
+
require 'loca/git'
|
14
|
+
require 'loca/cli'
|
data/lib/loca/cli.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Loca
|
2
|
+
class CLI < Thor
|
3
|
+
include Thor::Actions # https://github.com/erikhuda/thor/wiki/Actions
|
4
|
+
|
5
|
+
map %w(--version -v) => :__print_version
|
6
|
+
|
7
|
+
desc '--version, -v', 'print the version'
|
8
|
+
def __print_version
|
9
|
+
puts Loca::VERSION
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'c URL', 'Check out a pull request locally'
|
13
|
+
method_option :delete, aliases: '-d', desc: 'Delete the branch instead of creating it'
|
14
|
+
def c(pasted_url)
|
15
|
+
return d(pasted_url) if options[:delete]
|
16
|
+
|
17
|
+
git = Loca::Git.new(pasted_url)
|
18
|
+
branch_name = git.branch_name
|
19
|
+
|
20
|
+
if git.first_time_creating? || yes?("WARN: Branch '#{branch_name}' "\
|
21
|
+
' already exists. Overwrite? (n)', :yellow)
|
22
|
+
git.fetch
|
23
|
+
git.checkout
|
24
|
+
say "Checked out #{branch_name}!", :green
|
25
|
+
else
|
26
|
+
fail Loca::Error::GitAborted, 'Git checkout aborted!'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'd URL', 'Delete the local branch for that URL'
|
31
|
+
def d(pasted_url)
|
32
|
+
git = Loca::Git.new(pasted_url)
|
33
|
+
branch_name = git.branch_name
|
34
|
+
|
35
|
+
git.delete
|
36
|
+
say "Deleted #{branch_name}!", :green
|
37
|
+
end
|
38
|
+
|
39
|
+
private # rubocop:disable Lint/UselessAccessModifier
|
40
|
+
|
41
|
+
no_commands do # Thor primitive(s) that we want to stub in RSpec
|
42
|
+
def yes?(*args)
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/loca/error.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Using http://ablogaboutcode.com/2011/01/03/using-custom-error-messages-for-cleaner-code/
|
2
|
+
# and http://www.simonewebdesign.it/how-to-set-default-message-exception/
|
3
|
+
module Loca
|
4
|
+
module Error
|
5
|
+
class Base < StandardError
|
6
|
+
def initialize(message)
|
7
|
+
# To get the message in red
|
8
|
+
$stderr.puts message.red
|
9
|
+
@message = message
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
InvalidURL = Class.new(Base)
|
14
|
+
|
15
|
+
GitStdErrDetected = Class.new(Base)
|
16
|
+
UnstashedFilesFound = Class.new(Base)
|
17
|
+
OnlyOneBranch = Class.new(Base)
|
18
|
+
RemoteNotSet = Class.new(Base)
|
19
|
+
GitAborted = Class.new(Base)
|
20
|
+
end
|
21
|
+
end
|
data/lib/loca/git.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
module Loca
|
2
|
+
class Git
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
attr_reader :branch_name
|
6
|
+
|
7
|
+
def initialize(url, remote = nil)
|
8
|
+
@url = Loca::URL.new(url)
|
9
|
+
@branch_name = @url.branch_name
|
10
|
+
|
11
|
+
ensure_git_repo
|
12
|
+
ensure_no_unstashed_files
|
13
|
+
@remote_name = remote || extract_remote_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete
|
17
|
+
# Cannot delete a branch you are currently on:
|
18
|
+
checkout_another_branch if @branch_name == current_branch
|
19
|
+
git "branch -D #{@branch_name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def checkout
|
23
|
+
git "checkout #{@branch_name}", false # prints to stderr for some reason
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch
|
27
|
+
# To avoid fatal error: Refusing to fetch into current branch
|
28
|
+
delete unless first_time_creating?
|
29
|
+
# Performs `git fetch upstream pull/PR_NUMBER/head:BRANCH_NAME`
|
30
|
+
git "fetch #{@remote_name} pull/#{@url.pr_num}/head:#{@branch_name}", false # shellout has stderr for some reason
|
31
|
+
end
|
32
|
+
|
33
|
+
def first_time_creating? # Keep this a public method so we can prompt the user for overwrite
|
34
|
+
branches.include?(@branch_name) ? false : true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Example
|
38
|
+
#
|
39
|
+
# git_match_http?("https://github.com/smoll/loca.git", "https://github.com/smoll/loca/pull/1")
|
40
|
+
# => true
|
41
|
+
def git_match_http?(git, http)
|
42
|
+
format = lambda do |uri| # Strip off uri scheme & trailing '.git'
|
43
|
+
uri.sub('https://', '')
|
44
|
+
.sub('http://', '')
|
45
|
+
.sub('git://', '')
|
46
|
+
.sub(/.git$/, '')
|
47
|
+
end
|
48
|
+
format.call(http).start_with?(format.call(git))
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def git(cmd, fail_on_stderr = true)
|
54
|
+
shellout = Mixlib::ShellOut.new "git #{cmd}"
|
55
|
+
shellout.run_command
|
56
|
+
return shellout.stdout if shellout.stderr.empty?
|
57
|
+
if fail_on_stderr
|
58
|
+
fail Loca::Error::GitStdErrDetected, "#{shellout.stderr.strip}"
|
59
|
+
else
|
60
|
+
$stderr.puts shellout.stderr.strip.yellow
|
61
|
+
end
|
62
|
+
shellout.stdout.strip
|
63
|
+
end
|
64
|
+
|
65
|
+
def branches
|
66
|
+
git("for-each-ref refs/heads/ --format='%(refname:short)'").split("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
def current_branch
|
70
|
+
git('rev-parse --abbrev-ref HEAD').strip
|
71
|
+
end
|
72
|
+
|
73
|
+
def ensure_git_repo
|
74
|
+
git 'rev-parse'
|
75
|
+
end
|
76
|
+
|
77
|
+
def ensure_no_unstashed_files
|
78
|
+
val = git 'status --porcelain'
|
79
|
+
fail Loca::Error::UnstashedFilesFound, 'Commit or stash your files before continuing!' unless val.empty?
|
80
|
+
end
|
81
|
+
|
82
|
+
def checkout_another_branch
|
83
|
+
another = branches.find { |branch| branch != current_branch }
|
84
|
+
fail Loca::Error::OnlyOneBranch, 'No other branch to checkout!' unless another
|
85
|
+
git "checkout #{another}", false # prints to stderr for some reason
|
86
|
+
end
|
87
|
+
|
88
|
+
def remote_mapping
|
89
|
+
names = git('remote show -n').split("\n")
|
90
|
+
mapping = {}
|
91
|
+
names.each do |name|
|
92
|
+
mapping[name] = git("config --get remote.#{name}.url").strip
|
93
|
+
end
|
94
|
+
mapping
|
95
|
+
end
|
96
|
+
|
97
|
+
def extract_remote_name
|
98
|
+
match = remote_mapping.find { |_name, url| git_match_http?(url, @url.to_s) }
|
99
|
+
unless match
|
100
|
+
if yes?("Remote #{(@url)} not set. Would you like to set it as 'auto_loca_remote'?")
|
101
|
+
set_remote
|
102
|
+
else
|
103
|
+
fail Loca::Error::RemoteNotSet, "You must set the repo (#{@url}) as a remote "\
|
104
|
+
"(see `git remote -v'). All remotes: #{remote_mapping}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
match.first
|
108
|
+
end
|
109
|
+
|
110
|
+
def set_remote
|
111
|
+
uri = Addressable::URI.parse(@url)
|
112
|
+
uri.path = uri.path.split('/')[0..2].join('/')
|
113
|
+
remote_url = "#{uri}.git"
|
114
|
+
git "remote add upstream #{remote_url}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/loca/url.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Loca
|
2
|
+
class URL
|
3
|
+
attr_reader :branch_name
|
4
|
+
attr_reader :pr_num
|
5
|
+
|
6
|
+
def initialize(url)
|
7
|
+
@url = url
|
8
|
+
ensure_well_formed
|
9
|
+
|
10
|
+
@branch_name = extract_branch_name
|
11
|
+
@pr_num = extract_pr_num
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@url
|
16
|
+
end
|
17
|
+
|
18
|
+
def ensure_well_formed
|
19
|
+
# TODO: add more checks via Addressable::URI
|
20
|
+
segments = URI(@url).path.split('/')
|
21
|
+
int = Integer(segments[-1]) rescue false # replace with coercible gem?
|
22
|
+
pull = segments[-2] == 'pull'
|
23
|
+
|
24
|
+
fail Loca::Error::InvalidURL, "Doesn't appear to be a well-formed URL: #{@url}" unless int && pull
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def extract_branch_name
|
30
|
+
"PULL_#{extract_pr_num}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_pr_num
|
34
|
+
URI(@url).path.split('/').last
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/loca/version.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
describe 'Checking out a GitHub PR locally' do
|
2
|
+
let(:pr_url) { 'https://github.com/octocat/Spoon-Knife/pull/4865' }
|
3
|
+
let(:expected_branch_name) { 'PULL_4865' }
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
clone_test_repo
|
7
|
+
cd_to_cloned_dir
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) { teardown }
|
11
|
+
|
12
|
+
context "the repo exists as the 'upstream' remote" do
|
13
|
+
before(:each) { set_upstream }
|
14
|
+
|
15
|
+
it 'checks out then deletes' do
|
16
|
+
shellout! "loca c #{pr_url}"
|
17
|
+
expect(current_branch.strip).to eq expected_branch_name
|
18
|
+
shellout! "loca c #{pr_url} -d"
|
19
|
+
expect(current_branch.strip).to_not eq expected_branch_name
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'checks out then prompts to overwrite' do
|
23
|
+
shellout! "loca c #{pr_url}"
|
24
|
+
expect(current_branch.strip).to eq expected_branch_name
|
25
|
+
shellout! "loca c #{pr_url}", input: 'yes'
|
26
|
+
expect(current_branch.strip).to eq expected_branch_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'the repo does not exist as a remote' do
|
31
|
+
it 'fails to checkout' do
|
32
|
+
expect { shellout! "loca c #{pr_url}" }.to raise_error
|
33
|
+
expect(current_branch.strip).to_not eq expected_branch_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'the wrong URL is supplied' do
|
38
|
+
it 'fails to checkout' do
|
39
|
+
expect { shellout! 'loca c https://github.com/octocat/Spoon-Knife/wrong/4865' }.to raise_error
|
40
|
+
expect(current_branch.strip).to_not eq expected_branch_name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
describe Loca::CLI do
|
2
|
+
let(:invalid_url) { 'https://github.com/fakez/fakezz/pull/1' }
|
3
|
+
let(:url) { 'https://github.com/smoll/loca/pull/1' }
|
4
|
+
subject { described_class.new }
|
5
|
+
|
6
|
+
before do # ensure we don't shell out to git
|
7
|
+
allow_any_instance_of(Loca::Git).to receive(:fetch)
|
8
|
+
allow_any_instance_of(Loca::Git).to receive(:checkout)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'c' do
|
12
|
+
it 'fetches and checks out the branch locally' do
|
13
|
+
expect_any_instance_of(Loca::Git).to receive(:branch_name).and_return 'PULL_1'
|
14
|
+
expect_any_instance_of(Loca::Git).to receive(:first_time_creating?).and_return true
|
15
|
+
|
16
|
+
output = capture(:stdout) { described_class.start(%W(c #{url})) }.strip
|
17
|
+
expect(output).to end_with 'Checked out PULL_1!'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'fetches and overwrites existing branch if the user says yes to overwrite' do
|
21
|
+
expect_any_instance_of(Loca::Git).to receive(:branch_name).and_return 'PULL_1'
|
22
|
+
expect_any_instance_of(Loca::Git).to receive(:first_time_creating?).and_return false
|
23
|
+
silence(:stdout) { allow_any_instance_of(described_class).to receive(:yes?).and_return true }
|
24
|
+
|
25
|
+
output = capture(:stdout) { described_class.start(%W(c #{url})) }.strip
|
26
|
+
expect(output).to end_with 'Checked out PULL_1!'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises an error when the branch exists and the user says no to overwrite' do
|
30
|
+
expect_any_instance_of(Loca::Git).to receive(:branch_name).and_return 'PULL_1'
|
31
|
+
expect_any_instance_of(Loca::Git).to receive(:first_time_creating?).and_return false
|
32
|
+
silence(:stdout) { allow_any_instance_of(described_class).to receive(:yes?).and_return false }
|
33
|
+
|
34
|
+
expect { silence(:stderr) { described_class.start(%W(c #{url})) } }.to raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'raises an error when an invalid repo is supplied' do
|
38
|
+
expect { silence(:stderr) { described_class.start(%W(c #{invalid_url})) } }.to raise_error
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'deletes when the delete flag is appended' do
|
42
|
+
expect_any_instance_of(Loca::Git).to receive(:branch_name).and_return 'PULL_1'
|
43
|
+
expect_any_instance_of(Loca::Git).to receive(:delete)
|
44
|
+
|
45
|
+
output = capture(:stdout) { described_class.start(%W(c #{url} -d)) }.strip
|
46
|
+
expect(output).to eq 'Deleted PULL_1!'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe 'd' do
|
51
|
+
it 'deletes' do
|
52
|
+
expect_any_instance_of(Loca::Git).to receive(:branch_name).and_return 'PULL_1'
|
53
|
+
expect_any_instance_of(Loca::Git).to receive(:delete)
|
54
|
+
|
55
|
+
output = capture(:stdout) { described_class.start(%W(d #{url})) }.strip
|
56
|
+
expect(output).to eq 'Deleted PULL_1!'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'raises an error when an invalid repo is supplied' do
|
60
|
+
expect { silence(:stderr) { described_class.start(%W(d #{invalid_url})) } }.to raise_error
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '--version' do
|
65
|
+
it 'returns the correct version' do
|
66
|
+
expect(capture(:stdout) { described_class.start(%w(-v)) }.strip).to eq(Loca::VERSION)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
describe Loca::Git do
|
2
|
+
let(:pid) { 1 } # Pull request ID
|
3
|
+
let(:url) { "https://github.com/smoll/loca/pull/#{pid}" }
|
4
|
+
let(:remote_name) { 'loca-fake-remote' }
|
5
|
+
let(:remote_url) { 'https://github.com/smoll/loca.git' }
|
6
|
+
subject { described_class.new(url, remote_name) }
|
7
|
+
|
8
|
+
let(:expected_branch_name) { "PULL_#{pid}" }
|
9
|
+
let(:branches_without_expected) { ['master'] }
|
10
|
+
let(:branches_with_expected) { branches_without_expected << expected_branch_name }
|
11
|
+
|
12
|
+
describe '#git' do # private method that shells out to git
|
13
|
+
it 'raises an error when an invalid git command is supplied' do
|
14
|
+
expect { capture(:stderr) { subject.send(:git, 'not-a-git-command') } }.to raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'prints to stderr when an invalid git command is supplied, but we do not want to fail on stderr' do
|
18
|
+
output = capture(:stderr) { subject.send(:git, 'not-a-git-command', false) }
|
19
|
+
expect(output).to include "git: 'not-a-git-command' is not a git command"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#branches' do # private method
|
24
|
+
it 'returns all local branches' do
|
25
|
+
cmd = "for-each-ref refs/heads/ --format='%(refname:short)'"
|
26
|
+
expect(subject).to receive(:git).with(cmd).and_return 'a_branch'
|
27
|
+
expect(subject.send(:branches)).to eq ['a_branch']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#remote_mapping' do # private method
|
32
|
+
it "returns all of the repo's remotes" do
|
33
|
+
expect(subject).to receive(:git).with('remote show -n').and_return remote_name
|
34
|
+
expect(subject).to receive(:git).with("config --get remote.#{remote_name}.url").and_return remote_url
|
35
|
+
expect(subject.send(:remote_mapping)).to eq(remote_name => remote_url)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#extract_remote_name' do # private method
|
40
|
+
it 'returns the local repo remote name corresponding to the Pull Request URL' do
|
41
|
+
expect(subject).to receive(:remote_mapping).and_return(remote_name => remote_url) # investigate this
|
42
|
+
|
43
|
+
expect(subject.send(:extract_remote_name)).to eq remote_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#current_branch' do # private method
|
48
|
+
it 'returns the current branch' do
|
49
|
+
expect(subject).to receive(:git).with('rev-parse --abbrev-ref HEAD').and_return "branch_name\n"
|
50
|
+
expect(subject.send(:current_branch)).to eq 'branch_name'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#checkout' do # private method
|
55
|
+
it 'checks out the expected branch' do
|
56
|
+
expect(subject).to receive(:git).with("checkout #{expected_branch_name}", false).once
|
57
|
+
subject.send(:checkout)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#checkout_another_branch' do # private method
|
62
|
+
it 'checks out a branch that is not the current one' do
|
63
|
+
allow(subject).to receive(:branches).and_return %w(branch1 branch2)
|
64
|
+
allow(subject).to receive(:current_branch).and_return 'branch1'
|
65
|
+
expect(subject).to receive(:git).with('checkout branch2', false).once
|
66
|
+
silence(:stdout) { subject.send(:checkout_another_branch) }
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'raises an error when there is no other branch to check out' do
|
70
|
+
allow(subject).to receive(:branches).and_return ['branch1']
|
71
|
+
allow(subject).to receive(:current_branch).and_return 'branch1'
|
72
|
+
|
73
|
+
expect { silence(:stderr) { subject.send(:checkout_another_branch) } }.to raise_error
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#fetch' do
|
78
|
+
before do
|
79
|
+
allow(subject).to receive(:git) # ensure we don't actually shell out to `git`
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'deletes before fetching when the branch already exists' do
|
83
|
+
allow(subject).to receive(:branches).and_return branches_with_expected
|
84
|
+
expect(subject).to receive(:delete).once
|
85
|
+
|
86
|
+
subject.fetch
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'fetches the remote pull request' do
|
90
|
+
allow(subject).to receive(:branches).and_return branches_without_expected
|
91
|
+
|
92
|
+
# Investigate why this git command prints to $stderr, i.e. we must pass false as the 2nd arg:
|
93
|
+
expect(subject).to receive(:git).with("fetch #{remote_name} pull/#{pid}/head:#{expected_branch_name}", false)
|
94
|
+
|
95
|
+
subject.fetch
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#delete' do # should attempt to delete a branch with name 'PULL_1'
|
100
|
+
it 'raises an error when the branch does not exist' do
|
101
|
+
allow(subject).to receive(:branches).and_return branches_without_expected
|
102
|
+
expect { silence(:stderr) { subject.delete } }.to raise_error
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'deletes the branch when it exists' do
|
106
|
+
allow(subject).to receive(:branches).and_return branches_with_expected
|
107
|
+
allow(subject).to receive(:current_branch).and_return('master')
|
108
|
+
expect(subject).to receive(:git).with("branch -D #{expected_branch_name}").once
|
109
|
+
|
110
|
+
subject.delete
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'checks out another branch if attempting to delete current branch' do
|
114
|
+
allow(subject).to receive(:branches).and_return branches_with_expected
|
115
|
+
allow(subject).to receive(:current_branch).and_return expected_branch_name
|
116
|
+
|
117
|
+
expect(subject).to receive(:checkout_another_branch).once
|
118
|
+
expect(subject).to receive(:git).with("branch -D #{expected_branch_name}").once
|
119
|
+
|
120
|
+
subject.delete
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#first_time_creating?' do
|
125
|
+
it 'returns false when the branch already exists' do
|
126
|
+
allow(subject).to receive(:branches).and_return branches_with_expected
|
127
|
+
expect(subject.first_time_creating?).to eq false
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'returns true when the branch does not exist' do
|
131
|
+
allow(subject).to receive(:branches).and_return branches_without_expected
|
132
|
+
expect(subject.first_time_creating?).to eq true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#git_match_http?' do
|
137
|
+
it 'returns true when matching urls are supplied' do
|
138
|
+
git_url = 'https://github.com/smoll/loca.git'
|
139
|
+
expect(subject.git_match_http?(git_url, url)).to eq true
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'returns true when different URI schemes but matching urls are supplied' do
|
143
|
+
git_url = 'git://github.com/smoll/loca.git'
|
144
|
+
expect(subject.git_match_http?(git_url, url)).to eq true
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'returns false when non-matching urls are supplied' do
|
148
|
+
git_url = 'git://github.com/someoneelse/loca.git'
|
149
|
+
expect(subject.git_match_http?(git_url, url)).to eq false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
describe Loca::URL do
|
2
|
+
let(:pid) { 1 } # Pull request ID
|
3
|
+
let(:url) { "https://github.com/smoll/loca/pull/#{pid}" }
|
4
|
+
subject { described_class.new(url) }
|
5
|
+
|
6
|
+
let(:expected_branch_name) { "PULL_#{pid}" }
|
7
|
+
let(:expected_pr_num) { pid.to_s }
|
8
|
+
|
9
|
+
describe '#initialize' do
|
10
|
+
it 'does not raise an error for a valid URL' do
|
11
|
+
expect { Loca::URL.new(url) }.to_not raise_error
|
12
|
+
end
|
13
|
+
# it 'does not raise an error for a GitHub PR URL with additional segments' do
|
14
|
+
# expect { Loca::URL.new("#{url}/something/something") }.to_not raise_error # TODO: technical improvement
|
15
|
+
# end
|
16
|
+
it 'raises an error for a non-URL' do
|
17
|
+
expect { silence(:stderr) { Loca::URL.new('not a URL') } }.to raise_error
|
18
|
+
end
|
19
|
+
it 'raises an error for a non-GitHub URL' do
|
20
|
+
expect { silence(:stderr) { Loca::URL.new('http://bad-url-here.com') } }.to raise_error
|
21
|
+
end
|
22
|
+
it 'raises an error for a GitHub non-PR URL' do
|
23
|
+
expect { silence(:stderr) { Loca::URL.new('https://github.com/smoll/loca/something/something') } }.to raise_error
|
24
|
+
end
|
25
|
+
it 'raises an error for a GitHub URL without an integer in the PID location' do
|
26
|
+
expect { silence(:stderr) { Loca::URL.new('https://github.com/smoll/loca/pull/NaN') } }.to raise_error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#to_s' do
|
31
|
+
it 'returns the url passed to the constructor' do
|
32
|
+
expect(subject.to_s).to eq url
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#extract_branch_name' do # private method
|
37
|
+
it 'returns the expected branch name' do
|
38
|
+
expect(subject.send(:extract_branch_name)).to eq expected_branch_name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#extract_pr_num' do # private method
|
43
|
+
it 'returns the expected branch name' do
|
44
|
+
expect(subject.send(:extract_pr_num)).to eq "#{pid}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Using https://raw.githubusercontent.com/bbatsov/rubocop/master/spec/spec_helper.rb as a guide
|
2
|
+
require 'simplecov'
|
3
|
+
require 'coveralls'
|
4
|
+
require 'codeclimate-test-reporter'
|
5
|
+
|
6
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
7
|
+
SimpleCov::Formatter::HTMLFormatter,
|
8
|
+
Coveralls::SimpleCov::Formatter,
|
9
|
+
CodeClimate::TestReporter::Formatter
|
10
|
+
]
|
11
|
+
SimpleCov.start
|
12
|
+
|
13
|
+
require 'bundler/setup'
|
14
|
+
Bundler.setup # From http://stackoverflow.com/a/4402193
|
15
|
+
|
16
|
+
require 'loca'
|
17
|
+
|
18
|
+
# Requires supporting files with custom matchers and macros, etc,
|
19
|
+
# in ./support/ and its subdirectories.
|
20
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
21
|
+
|
22
|
+
RSpec.configure do |config|
|
23
|
+
config.include Features::GitHubHelper
|
24
|
+
|
25
|
+
config.order = :random
|
26
|
+
|
27
|
+
config.expect_with :rspec do |expectations|
|
28
|
+
expectations.syntax = :expect # Disable `should`
|
29
|
+
end
|
30
|
+
|
31
|
+
config.mock_with :rspec do |mocks|
|
32
|
+
mocks.syntax = :expect # Disable `should_receive` and `stub`
|
33
|
+
mocks.verify_partial_doubles = true # Avoid La La Land,
|
34
|
+
# read http://wegowise.github.io/blog/2014/09/03/rspec-verifying-doubles/
|
35
|
+
end
|
36
|
+
|
37
|
+
config.before do
|
38
|
+
ARGV.replace []
|
39
|
+
end
|
40
|
+
|
41
|
+
# From https://raw.githubusercontent.com/erikhuda/thor/master/spec/helper.rb
|
42
|
+
# Captures the output for analysis later
|
43
|
+
#
|
44
|
+
# @example Capture `$stderr`
|
45
|
+
#
|
46
|
+
# output = capture(:stderr) { $stderr.puts "this is captured" }
|
47
|
+
#
|
48
|
+
# @param [Symbol] stream `:stdout` or `:stderr`
|
49
|
+
# @yield The block to capture stdout/stderr for.
|
50
|
+
# @return [String] The contents of $stdout or $stderr
|
51
|
+
# rubocop:disable Lint/Eval
|
52
|
+
def capture(stream)
|
53
|
+
begin
|
54
|
+
stream = stream.to_s
|
55
|
+
eval "$#{stream} = StringIO.new"
|
56
|
+
yield
|
57
|
+
result = eval("$#{stream}").string
|
58
|
+
ensure
|
59
|
+
eval("$#{stream} = #{stream.upcase}")
|
60
|
+
end
|
61
|
+
|
62
|
+
result
|
63
|
+
end
|
64
|
+
# rubocop:enable Lint/Eval
|
65
|
+
|
66
|
+
# This code was adapted from Ruby on Rails, available under MIT-LICENSE
|
67
|
+
# Copyright (c) 2004-2013 David Heinemeier Hansson
|
68
|
+
def silence_warnings
|
69
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
70
|
+
yield
|
71
|
+
ensure
|
72
|
+
$VERBOSE = old_verbose
|
73
|
+
end
|
74
|
+
|
75
|
+
# Silences the output stream
|
76
|
+
#
|
77
|
+
# @example Silence `$stdout`
|
78
|
+
#
|
79
|
+
# silence(:stdout) { $stdout.puts "hi" }
|
80
|
+
#
|
81
|
+
# @param [IO] stream The stream to use such as $stderr or $stdout
|
82
|
+
# @return [nil]
|
83
|
+
alias :silence :capture # rubocop:disable Style/Alias
|
84
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'mixlib/shellout'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Features
|
5
|
+
module GitHubHelper
|
6
|
+
def teardown
|
7
|
+
return_to_original_wd
|
8
|
+
FileUtils.rm_rf absolute_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def cd_to_cloned_dir
|
12
|
+
@original_wd = Dir.pwd
|
13
|
+
Dir.chdir(absolute_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def return_to_original_wd
|
17
|
+
Dir.chdir(@original_wd)
|
18
|
+
end
|
19
|
+
|
20
|
+
def clone_test_repo
|
21
|
+
shellout! "git clone https://github.com/smoll/Spoon-Knife ./#{rel_path}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_upstream
|
25
|
+
shellout! 'git remote add upstream https://github.com/octocat/Spoon-Knife.git'
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_branch
|
29
|
+
shellout! 'git rev-parse --abbrev-ref HEAD'
|
30
|
+
end
|
31
|
+
|
32
|
+
def shellout!(cmd, opts = {})
|
33
|
+
opts = {
|
34
|
+
input: nil
|
35
|
+
}.merge(opts)
|
36
|
+
sh = opts[:input].nil? ? Mixlib::ShellOut.new(cmd.to_s) : Mixlib::ShellOut.new(cmd.to_s, input: opts[:input].to_s)
|
37
|
+
sh.run_command
|
38
|
+
|
39
|
+
sh.error! if opts[:input].nil? # NOTE: CLI exits non-zero if waiting on user input
|
40
|
+
sh.stdout
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def rel_path
|
46
|
+
absolute_path.split(Dir.pwd.to_s)[1].sub('/', '')
|
47
|
+
end
|
48
|
+
|
49
|
+
def absolute_path
|
50
|
+
path = File.expand_path('../../../tmp/cloned', __FILE__)
|
51
|
+
FileUtils.mkdir_p path
|
52
|
+
path
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: loca
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- smoll
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.19.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.19.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colorize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.7.5
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.7.5
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mixlib-shellout
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.0.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.0.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: addressable
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.3.7
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.3.7
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.4'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 10.4.2
|
93
|
+
type: :development
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '10.4'
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 10.4.2
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rubocop
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.29.0
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 0.29.0
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rspec
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 3.2.0
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 3.2.0
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: coveralls
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 0.7.9
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: 0.7.9
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: codeclimate-test-reporter
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: 0.4.6
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 0.4.6
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: simplecov
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: 0.9.1
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - "~>"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: 0.9.1
|
173
|
+
description:
|
174
|
+
email:
|
175
|
+
- mollah@gmail.com
|
176
|
+
executables:
|
177
|
+
- loca
|
178
|
+
extensions: []
|
179
|
+
extra_rdoc_files: []
|
180
|
+
files:
|
181
|
+
- bin/loca
|
182
|
+
- lib/loca.rb
|
183
|
+
- lib/loca/cli.rb
|
184
|
+
- lib/loca/error.rb
|
185
|
+
- lib/loca/git.rb
|
186
|
+
- lib/loca/url.rb
|
187
|
+
- lib/loca/version.rb
|
188
|
+
- spec/e2e/github_spec.rb
|
189
|
+
- spec/loca/cli_spec.rb
|
190
|
+
- spec/loca/git_spec.rb
|
191
|
+
- spec/loca/url_spec.rb
|
192
|
+
- spec/spec_helper.rb
|
193
|
+
- spec/support/features/github_helper.rb
|
194
|
+
homepage: https://github.com/smoll/loca
|
195
|
+
licenses:
|
196
|
+
- MIT
|
197
|
+
metadata: {}
|
198
|
+
post_install_message:
|
199
|
+
rdoc_options: []
|
200
|
+
require_paths:
|
201
|
+
- lib
|
202
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - ">="
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '0'
|
212
|
+
requirements: []
|
213
|
+
rubyforge_project:
|
214
|
+
rubygems_version: 2.4.6
|
215
|
+
signing_key:
|
216
|
+
specification_version: 4
|
217
|
+
summary: CLI for checking out GitHub Pull Requests locally
|
218
|
+
test_files:
|
219
|
+
- spec/e2e/github_spec.rb
|
220
|
+
- spec/loca/cli_spec.rb
|
221
|
+
- spec/loca/git_spec.rb
|
222
|
+
- spec/loca/url_spec.rb
|
223
|
+
- spec/spec_helper.rb
|
224
|
+
- spec/support/features/github_helper.rb
|