octo-merge 0.7.0.rc1
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/.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
|