loca 0.0.1
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.
- 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
|