octo-merge 0.7.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +129 -0
- data/Rakefile +6 -0
- data/TODO.md +11 -0
- data/bin/console +14 -0
- data/bin/octo-merge +9 -0
- data/bin/setup +8 -0
- data/exe/octo-merge +8 -0
- data/lib/git.rb +61 -0
- data/lib/octo_merge.rb +44 -0
- data/lib/octo_merge/cli.rb +53 -0
- data/lib/octo_merge/cli/parser.rb +102 -0
- data/lib/octo_merge/configuration.rb +5 -0
- data/lib/octo_merge/context.rb +21 -0
- data/lib/octo_merge/execute.rb +21 -0
- data/lib/octo_merge/interactive_pull_requests.rb +47 -0
- data/lib/octo_merge/list_pull_requests.rb +20 -0
- data/lib/octo_merge/options.rb +108 -0
- data/lib/octo_merge/pull_request.rb +54 -0
- data/lib/octo_merge/setup.rb +74 -0
- data/lib/octo_merge/strategy.rb +11 -0
- data/lib/octo_merge/strategy/base.rb +53 -0
- data/lib/octo_merge/strategy/merge_with_rebase.rb +29 -0
- data/lib/octo_merge/strategy/merge_with_rebase_and_message.rb +90 -0
- data/lib/octo_merge/strategy/merge_without_rebase.rb +24 -0
- data/lib/octo_merge/strategy/rebase.rb +21 -0
- data/lib/octo_merge/version.rb +3 -0
- data/octo_merge.gemspec +34 -0
- metadata +205 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module OctoMerge
|
4
|
+
class CLI
|
5
|
+
class Parser
|
6
|
+
def self.parse(args)
|
7
|
+
new(args).parse!
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(args)
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse!
|
15
|
+
prepare
|
16
|
+
opts.parse!(args)
|
17
|
+
options
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :args
|
23
|
+
|
24
|
+
def prepare
|
25
|
+
prepare_banner
|
26
|
+
|
27
|
+
prepare_application
|
28
|
+
|
29
|
+
opts.separator ""
|
30
|
+
opts.separator "Common options:"
|
31
|
+
|
32
|
+
prepare_help
|
33
|
+
prepare_version
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepare_banner
|
37
|
+
opts.banner = "Usage: octo-merge [options]"
|
38
|
+
opts.separator ""
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepare_application
|
42
|
+
opts.on("--repo=REPO", "Repository (e.g.: 'rails/rails')") do |repo|
|
43
|
+
options[:repo] = repo
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("--dir=DIR", "Working directory (e.g.: '~/Dev/Rails/rails')") do |dir|
|
47
|
+
options[:dir] = dir
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("--pull_requests=PULL_REQUESTS", "Pull requests (e.g.: '23,42,66')") do |pull_requests|
|
51
|
+
options[:pull_requests] = pull_requests
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("--login=login", "Login (Your GitHub username)") do |login|
|
55
|
+
options[:login] = login
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("--password=password", "Password (Your GitHub API-Token)") do |password|
|
59
|
+
options[:password] = password
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on("--strategy=STRATEGY", "Merge strategy (e.g.: 'MergeWithoutRebase')") do |strategy|
|
63
|
+
options[:strategy] = strategy
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on("--query=QUERY", "Query to use in interactive mode (e.g.: 'label:ready-to-merge')") do |query|
|
67
|
+
options[:query] = query
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('--interactive', 'Select PullRequests within an interactive session') do |interactive|
|
71
|
+
options[:interactive] = interactive
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on('--setup', 'Setup') do |setup|
|
75
|
+
options[:setup] = setup
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def prepare_help
|
80
|
+
opts.on_tail('-h', '--help', 'Display this screen') do
|
81
|
+
puts opts
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def prepare_version
|
87
|
+
opts.on_tail('-v', '--version', 'Display the version') do
|
88
|
+
puts OctoMerge::VERSION
|
89
|
+
exit
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def opts
|
94
|
+
@opts ||= OptionParser.new
|
95
|
+
end
|
96
|
+
|
97
|
+
def options
|
98
|
+
@options ||= {}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module OctoMerge
|
2
|
+
class Context
|
3
|
+
attr_reader :working_directory, :repo
|
4
|
+
|
5
|
+
def initialize(working_directory:, repo:, pull_request_numbers:)
|
6
|
+
@working_directory = working_directory
|
7
|
+
@repo = repo
|
8
|
+
@pull_request_numbers = pull_request_numbers
|
9
|
+
end
|
10
|
+
|
11
|
+
def pull_requests
|
12
|
+
@pull_requests ||= pull_request_numbers.map do |number|
|
13
|
+
PullRequest.new(repo: repo, number: number.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :pull_request_numbers
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module OctoMerge
|
2
|
+
class Execute
|
3
|
+
attr_reader :context, :strategy
|
4
|
+
|
5
|
+
def initialize(context:, strategy:)
|
6
|
+
@context = context
|
7
|
+
@strategy = strategy
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
env.run
|
12
|
+
end
|
13
|
+
|
14
|
+
def env
|
15
|
+
@env ||= strategy.new(
|
16
|
+
working_directory: context.working_directory,
|
17
|
+
pull_requests: context.pull_requests
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'inquirer'
|
2
|
+
|
3
|
+
module OctoMerge
|
4
|
+
class InteractivePullRequests
|
5
|
+
def initialize(repo:, query:)
|
6
|
+
@repo = repo
|
7
|
+
@query = query
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.get(options = {})
|
11
|
+
new(repo: options[:repo], query: options[:query]).to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
system("clear")
|
16
|
+
|
17
|
+
idx = Ask.checkbox(
|
18
|
+
"Select the pull requests you want to merge",
|
19
|
+
formatted_pull_requests
|
20
|
+
)
|
21
|
+
|
22
|
+
idx.zip(pull_requests).select { |e| e[0] }.map { |e| e[1].number }.join(",")
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :repo, :query
|
28
|
+
|
29
|
+
def formatted_pull_requests
|
30
|
+
pull_requests.map do |pull_request|
|
31
|
+
format_pull_request(pull_request)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def pull_requests
|
36
|
+
list.all
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_pull_request(pull_request)
|
40
|
+
"#{pull_request.number}: \"#{pull_request.title}\" by @#{pull_request.user.login}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def list
|
44
|
+
@list ||= OctoMerge::ListPullRequests.new(repo: repo, query: query)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module OctoMerge
|
2
|
+
class ListPullRequests
|
3
|
+
attr_reader :repo, :query
|
4
|
+
|
5
|
+
def initialize(repo:, query:)
|
6
|
+
@repo = repo
|
7
|
+
@query = query
|
8
|
+
end
|
9
|
+
|
10
|
+
def all
|
11
|
+
@all ||= github_client.search_issues("is:open is:pr repo:#{repo} #{query}")[:items]
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def github_client
|
17
|
+
OctoMerge.github_client
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
module OctoMerge
|
5
|
+
class Options
|
6
|
+
def self.option(key)
|
7
|
+
define_method(key) { self[key] }
|
8
|
+
end
|
9
|
+
|
10
|
+
option :login
|
11
|
+
option :password
|
12
|
+
|
13
|
+
option :dir
|
14
|
+
option :pull_requests
|
15
|
+
option :query
|
16
|
+
option :repo
|
17
|
+
option :setup
|
18
|
+
option :strategy
|
19
|
+
|
20
|
+
def [](key)
|
21
|
+
data[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def cli_options=(options)
|
25
|
+
reset_cache
|
26
|
+
@cli_options = options
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.user_config_path
|
30
|
+
USER_CONFIG_PATH
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.pathname
|
34
|
+
Pathname.new(CONFIG_FILE)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
CONFIG_FILE = ".octo-merge.yml"
|
40
|
+
DEFAULT_OPTIONS = {
|
41
|
+
dir: ".",
|
42
|
+
strategy: "MergeWithoutRebase"
|
43
|
+
}
|
44
|
+
USER_CONFIG_PATH = File.expand_path("~/#{CONFIG_FILE}")
|
45
|
+
|
46
|
+
def data
|
47
|
+
@data ||= begin
|
48
|
+
options = default_options
|
49
|
+
.merge(user_options)
|
50
|
+
.merge(project_options)
|
51
|
+
.merge(cli_options)
|
52
|
+
|
53
|
+
|
54
|
+
# Sanitize input
|
55
|
+
options[:dir] = File.expand_path(options[:dir])
|
56
|
+
options[:strategy] = Object.const_get("OctoMerge::Strategy::#{options[:strategy]}")
|
57
|
+
options[:pull_requests] = get_interactive_pull_requests(options) if options[:interactive]
|
58
|
+
options[:pull_requests] = options[:pull_requests].to_s.split(",")
|
59
|
+
|
60
|
+
options
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# This hotfix will configure the API credentials before doing the API call.
|
65
|
+
def get_interactive_pull_requests(options)
|
66
|
+
OctoMerge.configure do |config|
|
67
|
+
config.login = options[:login]
|
68
|
+
config.password = options[:password]
|
69
|
+
end
|
70
|
+
|
71
|
+
OctoMerge::InteractivePullRequests.get(options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def reset_cache
|
75
|
+
@data = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def default_options
|
79
|
+
DEFAULT_OPTIONS
|
80
|
+
end
|
81
|
+
|
82
|
+
def user_options
|
83
|
+
if File.exist?(USER_CONFIG_PATH)
|
84
|
+
body = File.read(USER_CONFIG_PATH)
|
85
|
+
symbolize_keys YAML.load(body)
|
86
|
+
else
|
87
|
+
{}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def project_options
|
92
|
+
if File.exist?(CONFIG_FILE)
|
93
|
+
body = File.read(CONFIG_FILE)
|
94
|
+
symbolize_keys YAML.load(body)
|
95
|
+
else
|
96
|
+
{}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def cli_options
|
101
|
+
@cli_options ||= {}
|
102
|
+
end
|
103
|
+
|
104
|
+
def symbolize_keys(hash)
|
105
|
+
hash.inject({}){ |memo, (k, v)| memo[k.to_sym] = v; memo }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "octokit"
|
2
|
+
|
3
|
+
module OctoMerge
|
4
|
+
class PullRequest
|
5
|
+
attr_reader :repo, :number
|
6
|
+
|
7
|
+
def initialize(repo:, number:)
|
8
|
+
@repo = repo
|
9
|
+
@number = number.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def url
|
13
|
+
github_api_result.html_url
|
14
|
+
end
|
15
|
+
|
16
|
+
def remote
|
17
|
+
github_api_result.user.login
|
18
|
+
end
|
19
|
+
|
20
|
+
def remote_url
|
21
|
+
github_api_result.head.repo.ssh_url
|
22
|
+
end
|
23
|
+
|
24
|
+
def remote_branch
|
25
|
+
github_api_result.head.ref
|
26
|
+
end
|
27
|
+
|
28
|
+
def number_branch
|
29
|
+
"pull/#{number}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def title
|
33
|
+
github_api_result.title
|
34
|
+
end
|
35
|
+
|
36
|
+
def body
|
37
|
+
github_api_result.body
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other_pull_request)
|
41
|
+
repo == other_pull_request.repo && number == other_pull_request.number
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def github_api_result
|
47
|
+
@github_api_result ||= github_client.pull_request(repo, number)
|
48
|
+
end
|
49
|
+
|
50
|
+
def github_client
|
51
|
+
OctoMerge.github_client
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module OctoMerge
|
2
|
+
class Setup
|
3
|
+
def initialize(options)
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.run(*args)
|
8
|
+
new(*args).tap { |setup| setup.run }
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
setup_user_config_file
|
13
|
+
setup_project_config_file
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
HINTS = {
|
19
|
+
login: "login is your GitHub username",
|
20
|
+
password: "You can manage your GitHub API tokens at: https://github.com/settings/tokens"
|
21
|
+
}
|
22
|
+
private_constant :HINTS
|
23
|
+
|
24
|
+
attr_reader :options
|
25
|
+
|
26
|
+
def setup_user_config_file
|
27
|
+
setup(
|
28
|
+
name: "user",
|
29
|
+
path: Options.user_config_path,
|
30
|
+
attributes: [:login, :password],
|
31
|
+
default: true
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_project_config_file
|
36
|
+
setup(
|
37
|
+
name: "project",
|
38
|
+
path: Options.pathname.realpath.to_s,
|
39
|
+
attributes: [:repo, :strategy, :query],
|
40
|
+
default: false
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup(name:, path:, attributes:, default:)
|
45
|
+
return unless Ask.confirm "Create #{name} config file? (#{path})", default: default
|
46
|
+
|
47
|
+
create(path: path, config: config_for(attributes))
|
48
|
+
end
|
49
|
+
|
50
|
+
def config_for(attributes)
|
51
|
+
attributes.inject({}) { |hash, key|
|
52
|
+
hash[key] = ask_for(key)
|
53
|
+
hash
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def ask_for(key)
|
58
|
+
puts "[INFO] #{HINTS[key]}" if HINTS[key]
|
59
|
+
|
60
|
+
Ask.input "#{key}", default: options.send(key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def create(path:, config:)
|
64
|
+
File.write(path.to_s, content_for(config))
|
65
|
+
end
|
66
|
+
|
67
|
+
def content_for(config)
|
68
|
+
config
|
69
|
+
.select { |key, value| !value.nil? && value != "" }
|
70
|
+
.map { |key, value| "#{key}: \"#{value}\"" }
|
71
|
+
.join("\n")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|