octo-merge 0.7.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module OctoMerge
2
+ class Configuration
3
+ attr_accessor :login, :password
4
+ end
5
+ 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