loca 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/loca +31 -2
- data/lib/loca.rb +5 -9
- data/lib/loca/cli.rb +59 -33
- data/lib/loca/error.rb +7 -14
- data/lib/loca/git/branch_creator.rb +55 -0
- data/lib/loca/git/branch_deleter.rb +32 -0
- data/lib/loca/git/common.rb +42 -0
- data/lib/loca/url/parser.rb +61 -0
- data/lib/loca/url/validator.rb +45 -0
- data/lib/loca/utils.rb +12 -0
- data/lib/loca/version.rb +1 -1
- data/spec/loca/cli_spec.rb +33 -55
- data/spec/loca/git/branch_creator_spec.rb +18 -0
- data/spec/loca/git/common_spec.rb +46 -0
- data/spec/loca/url/parser_spec.rb +88 -0
- data/spec/loca/url/validator_spec.rb +26 -0
- data/spec/loca/utils_spec.rb +40 -0
- data/spec/spec_helper.rb +13 -59
- data/spec/support/matchers/terminate.rb +37 -0
- metadata +63 -41
- data/lib/loca/git.rb +0 -117
- data/lib/loca/url.rb +0 -37
- data/spec/e2e/github_spec.rb +0 -43
- data/spec/loca/git_spec.rb +0 -152
- data/spec/loca/url_spec.rb +0 -47
- data/spec/support/features/github_helper.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8d346e967391656b7e5270109caa97349ebd2e7
|
4
|
+
data.tar.gz: 8d24248adeaba1b73089eac6abb8d4a0deb1ba65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a14f15ea2617100bc2a952d8b5e7a2348fa1b03c55e4071023f096ee12cf358ff374fd2f5024d6956955eca84e7b7ba6defbad2d9a544ee3409169ea016776f5
|
7
|
+
data.tar.gz: 33e0b60efc31955531b15c68d33b2184fbaa98e048828bfae87139f2349751ca6d59eacaccb2baaab9577fbeb6ca5d420d42cdde167de7a33bcc2536e6425927
|
data/bin/loca
CHANGED
@@ -1,5 +1,34 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
# Based on https://github.com/colszowka/simplecov/issues/234
|
4
|
+
# and http://stackoverflow.com/a/20505441/3456726
|
5
|
+
if ENV["COVERAGE"]
|
6
|
+
require "simplecov"
|
4
7
|
|
5
|
-
|
8
|
+
# As described in the issue, every process must have an unique name:
|
9
|
+
SimpleCov.command_name "loca #{ARGV.join(' ')} (Aruba)"
|
10
|
+
|
11
|
+
# When running with aruba simplecov was using /tmp/aruba as the root folder.
|
12
|
+
# This is to force using the project folder
|
13
|
+
SimpleCov.root(File.join(File.expand_path(File.dirname(__FILE__)), ".."))
|
14
|
+
|
15
|
+
SimpleCov.start do
|
16
|
+
filters.clear
|
17
|
+
|
18
|
+
# Because simplecov filters everything outside of the SimpleCov.root
|
19
|
+
# This should be added, cf.
|
20
|
+
# https://github.com/colszowka/simplecov#default-root-filter-and-coverage-for-things-outside-of-it
|
21
|
+
add_filter do |src|
|
22
|
+
!(src.filename =~ /^#{SimpleCov.root}/) unless src.filename =~ /loca/
|
23
|
+
end
|
24
|
+
|
25
|
+
# Ignoring test folders and tmp for Aruba
|
26
|
+
add_filter "/spec/"
|
27
|
+
add_filter "/test/"
|
28
|
+
add_filter "/features/"
|
29
|
+
add_filter "/tmp/"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require "loca"
|
34
|
+
Loca::CLI.new(ARGV.dup).execute!
|
data/lib/loca.rb
CHANGED
@@ -1,14 +1,10 @@
|
|
1
1
|
# Require all runtime dependencies first
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
2
|
+
require "addressable/uri"
|
3
|
+
require "colorize"
|
4
|
+
require "mixlib/shellout"
|
5
|
+
require "require_all"
|
6
6
|
|
7
7
|
# Start by requiring files with no dependencies on other files, then files with
|
8
8
|
# only external gem runtime dependencies (specified above), then files with
|
9
9
|
# dependencies on other files within the loca project itself
|
10
|
-
|
11
|
-
require 'loca/error'
|
12
|
-
require 'loca/url'
|
13
|
-
require 'loca/git'
|
14
|
-
require 'loca/cli'
|
10
|
+
require_rel "./loca"
|
data/lib/loca/cli.rb
CHANGED
@@ -1,47 +1,73 @@
|
|
1
|
-
|
2
|
-
class CLI < Thor
|
3
|
-
include Thor::Actions # https://github.com/erikhuda/thor/wiki/Actions
|
4
|
-
|
5
|
-
map %w(--version -v) => :__print_version
|
1
|
+
require "optparse"
|
6
2
|
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
module Loca
|
4
|
+
class CLI
|
5
|
+
def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
|
6
|
+
@argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
|
10
7
|
end
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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!'
|
9
|
+
def execute!
|
10
|
+
begin
|
11
|
+
parse_opts
|
12
|
+
parse_cmd
|
13
|
+
rescue Loca::Error::Standard, OptionParser::InvalidOption => e # colorize if so
|
14
|
+
@stderr.puts e.message.red
|
15
|
+
@stderr.puts e.backtrace if @options[:backtrace]
|
16
|
+
@stdout.puts parser # add if-user-error validation here
|
17
|
+
@kernel.exit(1)
|
27
18
|
end
|
19
|
+
@kernel.exit(0)
|
28
20
|
end
|
29
21
|
|
30
|
-
|
31
|
-
def d(pasted_url)
|
32
|
-
git = Loca::Git.new(pasted_url)
|
33
|
-
branch_name = git.branch_name
|
22
|
+
private
|
34
23
|
|
35
|
-
|
36
|
-
|
24
|
+
def parse_opts
|
25
|
+
parser.parse! @argv
|
37
26
|
end
|
38
27
|
|
39
|
-
|
28
|
+
def parse_cmd
|
29
|
+
fail Loca::Error::InvalidURL, "Need to pass in a single URL! Args: #{@argv}" unless @argv.count == 1
|
30
|
+
@parsed_url = Loca::URL::Parser.new(@argv[0]).parse
|
31
|
+
|
32
|
+
return delete if @options[:delete]
|
33
|
+
create
|
34
|
+
end
|
40
35
|
|
41
|
-
|
42
|
-
|
43
|
-
|
36
|
+
def parser # rubocop:disable Metrics/MethodLength
|
37
|
+
return @parser if @parser
|
38
|
+
@options = { delete: false, backtrace: false }
|
39
|
+
@parser = OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: loca <url> [options]"
|
41
|
+
opts.on("-b", "--backtrace", "Display full stacktraces on error") do
|
42
|
+
@options[:backtrace] = true
|
43
|
+
end
|
44
|
+
opts.on("-d", "--delete", "Deletes the checked out branch") do
|
45
|
+
@options[:delete] = true
|
46
|
+
end
|
47
|
+
opts.on_tail("-h", "--help", "Displays Help") do
|
48
|
+
@stdout.puts opts
|
49
|
+
@kernel.exit(0)
|
50
|
+
end
|
51
|
+
opts.on_tail("-v", "--version", "Display Version") do
|
52
|
+
@stdout.puts Loca::VERSION
|
53
|
+
@kernel.exit(0)
|
54
|
+
end
|
44
55
|
end
|
45
56
|
end
|
57
|
+
|
58
|
+
def delete
|
59
|
+
Loca::Git::BranchDeleter.new(@parsed_url[:branch_name]).delete
|
60
|
+
@stdout.puts "Deleted branch #{@parsed_url[:branch_name]}".green
|
61
|
+
end
|
62
|
+
|
63
|
+
def create
|
64
|
+
Loca::Git::BranchCreator.new(
|
65
|
+
@parsed_url[:pull][:num],
|
66
|
+
@parsed_url[:branch_name],
|
67
|
+
@parsed_url[:remote][:name],
|
68
|
+
@parsed_url[:remote][:url]
|
69
|
+
).create
|
70
|
+
@stdout.puts "Created and checked out branch #{@parsed_url[:branch_name]}".green
|
71
|
+
end
|
46
72
|
end
|
47
73
|
end
|
data/lib/loca/error.rb
CHANGED
@@ -2,20 +2,13 @@
|
|
2
2
|
# and http://www.simonewebdesign.it/how-to-set-default-message-exception/
|
3
3
|
module Loca
|
4
4
|
module Error
|
5
|
-
class
|
6
|
-
def initialize(message)
|
7
|
-
# To get the message in red
|
8
|
-
$stderr.puts message.red
|
9
|
-
@message = message
|
10
|
-
end
|
11
|
-
end
|
5
|
+
class Standard < StandardError; end
|
12
6
|
|
13
|
-
InvalidURL = Class.new(
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
GitAborted = Class.new(Base)
|
7
|
+
InvalidURL = Class.new(Standard)
|
8
|
+
GitStdErrDetected = Class.new(Standard)
|
9
|
+
UnstashedFilesFound = Class.new(Standard)
|
10
|
+
OnlyOneBranch = Class.new(Standard)
|
11
|
+
RemoteNotSet = Class.new(Standard)
|
12
|
+
GitAborted = Class.new(Standard)
|
20
13
|
end
|
21
14
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Loca
|
2
|
+
module Git
|
3
|
+
class BranchCreator
|
4
|
+
include Common
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
def initialize(pull_num, branch_name, remote_name, remote_url)
|
8
|
+
@pull_num, @branch_name, @remote_name, @remote_url = pull_num, branch_name, remote_name, remote_url
|
9
|
+
end
|
10
|
+
|
11
|
+
def create
|
12
|
+
run_git_checkers
|
13
|
+
add_remote
|
14
|
+
# To avoid fatal error: Refusing to fetch into current branch
|
15
|
+
delete unless first_time_creating?
|
16
|
+
fetch
|
17
|
+
checkout
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch
|
21
|
+
# Performs `git fetch upstream pull/PR_NUMBER/head:BRANCH_NAME`
|
22
|
+
git "fetch #{@remote_name} pull/#{@pull_num}/head:#{@branch_name}" # shellout has stderr for some reason
|
23
|
+
end
|
24
|
+
|
25
|
+
def checkout
|
26
|
+
git "checkout #{@branch_name}" # prints to stderr for some reason
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def delete
|
32
|
+
BranchDeleter.new(@branch_name).delete
|
33
|
+
end
|
34
|
+
|
35
|
+
def first_time_creating?
|
36
|
+
branches.include?(@branch_name) ? false : true
|
37
|
+
end
|
38
|
+
|
39
|
+
def remote_mapping
|
40
|
+
names = git("remote show -n").split("\n")
|
41
|
+
mapping = {}
|
42
|
+
names.each do |name|
|
43
|
+
mapping[name] = git("config --get remote.#{name}.url").strip
|
44
|
+
end
|
45
|
+
mapping
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_remote
|
49
|
+
match = remote_mapping.find { |_name, url| git_urls_match?(url, @remote_url) }
|
50
|
+
return @remote_name = match.first if match # avoid creating duplicate remotes
|
51
|
+
git "remote add #{@remote_name} #{@remote_url}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Loca
|
2
|
+
module Git
|
3
|
+
class BranchDeleter
|
4
|
+
include Common
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
def initialize(branch)
|
8
|
+
@branch = branch
|
9
|
+
end
|
10
|
+
|
11
|
+
def delete
|
12
|
+
run_git_checkers
|
13
|
+
# Cannot delete a branch you are currently on:
|
14
|
+
checkout_another_branch if @branch == current_branch
|
15
|
+
git "branch -D #{@branch}"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def checkout_another_branch
|
21
|
+
another = "master" if branches.include?("master") # prefer to checkout master branch if it exists
|
22
|
+
another = branches.find { |branch| branch != current_branch } unless another
|
23
|
+
fail Loca::Error::OnlyOneBranch, "No other branch to checkout!" unless another
|
24
|
+
git "checkout #{another}" # prints to stderr for some reason
|
25
|
+
end
|
26
|
+
|
27
|
+
def current_branch
|
28
|
+
git("rev-parse --abbrev-ref HEAD").strip
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Loca
|
2
|
+
module Git
|
3
|
+
module Common
|
4
|
+
def run_git_checkers
|
5
|
+
ensure_git_repo
|
6
|
+
ensure_no_unstashed_files
|
7
|
+
end
|
8
|
+
|
9
|
+
def git(cmd)
|
10
|
+
shellout = Mixlib::ShellOut.new "git #{cmd}"
|
11
|
+
shellout.run_command
|
12
|
+
shellout.error!
|
13
|
+
shellout.stdout
|
14
|
+
end
|
15
|
+
|
16
|
+
def branches
|
17
|
+
git("for-each-ref refs/heads/ --format='%(refname:short)'").split("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Example
|
21
|
+
#
|
22
|
+
# git_urls_match?("https://github.com/smoll/loca.git", "https://github.com/smoll/loca/") # note trailing slash
|
23
|
+
# => true
|
24
|
+
def git_urls_match?(git, http)
|
25
|
+
git_uri = Addressable::URI.parse git
|
26
|
+
http_uri = Addressable::URI.parse http
|
27
|
+
http_uri.host == git_uri.host && http_uri.path.chomp("/").chomp(".git") == git_uri.path.chomp("/").chomp(".git")
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def ensure_git_repo
|
33
|
+
git "rev-parse"
|
34
|
+
end
|
35
|
+
|
36
|
+
def ensure_no_unstashed_files
|
37
|
+
val = git "status --porcelain"
|
38
|
+
fail Loca::Error::UnstashedFilesFound, "Commit or stash your files before continuing!" if non_empty_string?(val)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Loca
|
2
|
+
module URL
|
3
|
+
class Parser
|
4
|
+
def initialize(url)
|
5
|
+
@url = url
|
6
|
+
@uri = Addressable::URI.parse(@url)
|
7
|
+
@segments = @uri.path.split("/").reject(&:empty?)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
@url
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse
|
15
|
+
validate
|
16
|
+
parse_attrs
|
17
|
+
set_other_attrs
|
18
|
+
all_attrs
|
19
|
+
end
|
20
|
+
|
21
|
+
def all_attrs
|
22
|
+
{
|
23
|
+
branch_name: @branch_name,
|
24
|
+
pull: {
|
25
|
+
num: @pull_num,
|
26
|
+
url: @pull_url
|
27
|
+
},
|
28
|
+
remote: {
|
29
|
+
name: @remote_name,
|
30
|
+
url: @remote_url
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def validate
|
38
|
+
Validator.new(@url).validate
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_attrs
|
42
|
+
@username = @segments[0]
|
43
|
+
@repo_name = @segments[1]
|
44
|
+
@pull_num = @segments[3]
|
45
|
+
|
46
|
+
pull_uri = @uri
|
47
|
+
pull_uri.path = @segments[0..3].join("/")
|
48
|
+
@pull_url = pull_uri.to_s
|
49
|
+
|
50
|
+
remote_uri = @uri
|
51
|
+
remote_uri.path = @segments[0..1].join("/") + ".git"
|
52
|
+
@remote_url = remote_uri.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_other_attrs
|
56
|
+
@remote_name = "loca_r_#{@username}" # i.e. loca_r_USERNAME
|
57
|
+
@branch_name = "PULL_#{@pull_num}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Loca
|
2
|
+
module URL
|
3
|
+
class Validator
|
4
|
+
include Utils
|
5
|
+
|
6
|
+
def initialize(url)
|
7
|
+
@url = url
|
8
|
+
@uri = Addressable::URI.parse(@url)
|
9
|
+
@segments = @uri.path.split("/").reject(&:empty?)
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate
|
13
|
+
return if valid_host? && valid_user_and_repo_segments? && valid_pull_segment? && valid_pull_number?
|
14
|
+
fail Loca::Error::InvalidURL, <<-MSG
|
15
|
+
Not a GitHub PR URL: #{@url}
|
16
|
+
Examples of valid URLs:
|
17
|
+
- https://github.com/smoll/loca/pull/1337
|
18
|
+
- https://github.com/smoll/loca/pull/1337/commits
|
19
|
+
- https://github.com/smoll/loca/pull/1337/files
|
20
|
+
MSG
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def valid_host?
|
26
|
+
@uri.host == "github.com"
|
27
|
+
end
|
28
|
+
|
29
|
+
def valid_user_and_repo_segments?
|
30
|
+
non_empty_string?(@segments[0]) && non_empty_string?(@segments[1])
|
31
|
+
end
|
32
|
+
|
33
|
+
def valid_pull_segment?
|
34
|
+
@segments[2] == "pull"
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_pull_number?
|
38
|
+
Integer(@segments[3])
|
39
|
+
true
|
40
|
+
rescue ArgumentError
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|