geet 0.1.12 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +32 -6
- data/.rubocop_todo.yml +24 -1
- data/.travis.yml +7 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +18 -0
- data/README.md +45 -11
- data/Rakefile +5 -0
- data/bin/geet +6 -9
- data/extra/issue_editing.png +0 -0
- data/extra/pr_editing.png +0 -0
- data/geet.gemspec +2 -2
- data/lib/geet/commandline/commands.rb +3 -1
- data/lib/geet/commandline/configuration.rb +37 -31
- data/lib/geet/commandline/editor.rb +66 -0
- data/lib/geet/git/repository.rb +5 -1
- data/lib/geet/github/abstract_issue.rb +7 -3
- data/lib/geet/github/api_interface.rb +9 -5
- data/lib/geet/github/branch.rb +15 -0
- data/lib/geet/github/gist.rb +1 -1
- data/lib/geet/github/issue.rb +1 -13
- data/lib/geet/github/label.rb +1 -1
- data/lib/geet/gitlab/api_interface.rb +9 -5
- data/lib/geet/helpers/os_helper.rb +8 -1
- data/lib/geet/resources/edit_summary_template.md +7 -0
- data/lib/geet/services/create_gist.rb +1 -1
- data/lib/geet/services/create_issue.rb +1 -1
- data/lib/geet/services/create_pr.rb +5 -2
- data/lib/geet/services/merge_pr.rb +8 -1
- data/lib/geet/version.rb +1 -1
- data/spec/integration/create_gist_spec.rb +5 -3
- data/spec/integration/create_issue_spec.rb +6 -3
- data/spec/integration/create_label_spec.rb +6 -4
- data/spec/integration/create_pr_spec.rb +4 -2
- data/spec/integration/list_issues_spec.rb +5 -3
- data/spec/integration/list_labels_spec.rb +4 -2
- data/spec/integration/list_milestones_spec.rb +3 -1
- data/spec/integration/list_prs_spec.rb +4 -2
- data/spec/integration/merge_pr_spec.rb +26 -1
- data/spec/spec_helper.rb +4 -2
- data/spec/vcr_cassettes/merge_pr_with_branch_deletion.yml +222 -0
- metadata +11 -3
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
require_relative '../helpers/os_helper.rb'
|
6
|
+
|
7
|
+
module Geet
|
8
|
+
module Commandline
|
9
|
+
class Editor
|
10
|
+
# Liberally ripp..., ahem, inspired from git.
|
11
|
+
SUMMARY_TEMPLATE = File.expand_path('../resources/edit_summary_template.md', __dir__)
|
12
|
+
SUMMARY_TEMPLATE_SEPARATOR = '------------------------ >8 ------------------------'
|
13
|
+
|
14
|
+
include Geet::Helpers::OsHelper
|
15
|
+
|
16
|
+
# Edits a summary in the default editor, providing the SUMMARY_TEMPLATE.
|
17
|
+
#
|
18
|
+
# A summary is a composition with a title and an optional description;
|
19
|
+
# if the description is not found, a blank string is returned.
|
20
|
+
#
|
21
|
+
def edit_summary
|
22
|
+
raw_summary = edit_content_in_default_editor(IO.read(SUMMARY_TEMPLATE))
|
23
|
+
|
24
|
+
split_raw_summary(raw_summary)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# MAIN STEPS #######################################################################
|
30
|
+
|
31
|
+
# The gem `tty-editor` does this, although it requires in turn other 7/8 gems.
|
32
|
+
# Interestingly, the API `TTY::Editor.open(content: 'text')` is not very useful,
|
33
|
+
# as it doesn't return the filename (!).
|
34
|
+
#
|
35
|
+
def edit_content_in_default_editor(content)
|
36
|
+
tempfile = Tempfile.open(['geet_editor', '.md']) { |file| file << content }.path
|
37
|
+
|
38
|
+
execute_command('editing', system_editor, tempfile)
|
39
|
+
|
40
|
+
content = IO.read(tempfile)
|
41
|
+
|
42
|
+
File.unlink(tempfile)
|
43
|
+
|
44
|
+
content
|
45
|
+
end
|
46
|
+
|
47
|
+
def split_raw_summary(raw_summary)
|
48
|
+
raw_summary, _ = raw_summary.strip.split(SUMMARY_TEMPLATE_SEPARATOR, 2)
|
49
|
+
|
50
|
+
raise "Missing title!" if raw_summary.empty?
|
51
|
+
|
52
|
+
title, description = raw_summary.split(/\r|\n/, 2)
|
53
|
+
|
54
|
+
# The title may have a residual newline char; the description may not be present,
|
55
|
+
# or have multiple blank lines.
|
56
|
+
[title.strip, description.to_s.strip]
|
57
|
+
end
|
58
|
+
|
59
|
+
# HELPERS ##########################################################################
|
60
|
+
|
61
|
+
def system_editor
|
62
|
+
ENV['EDITOR'] || ENV['VISUAL'] || 'vi'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/geet/git/repository.rb
CHANGED
@@ -47,6 +47,10 @@ module Geet
|
|
47
47
|
attempt_provider_call(:Label, :create, name, color, api_interface)
|
48
48
|
end
|
49
49
|
|
50
|
+
def delete_branch(name)
|
51
|
+
attempt_provider_call(:Branch, :delete, name, api_interface)
|
52
|
+
end
|
53
|
+
|
50
54
|
def abstract_issues(milestone: nil)
|
51
55
|
attempt_provider_call(:AbstractIssue, :list, api_interface, milestone: milestone)
|
52
56
|
end
|
@@ -139,7 +143,7 @@ module Geet
|
|
139
143
|
if Kernel.const_defined?(full_class_name)
|
140
144
|
klass = Kernel.const_get(full_class_name)
|
141
145
|
|
142
|
-
if !
|
146
|
+
if !klass.respond_to?(meth)
|
143
147
|
raise "The functionality invoked (#{class_name} #{meth}) is not currently supported!"
|
144
148
|
end
|
145
149
|
|
@@ -22,21 +22,25 @@ module Geet
|
|
22
22
|
|
23
23
|
# See https://developer.github.com/v3/issues/#list-issues-for-a-repository
|
24
24
|
#
|
25
|
-
def self.list(api_interface, milestone: nil)
|
25
|
+
def self.list(api_interface, only_issues: false, milestone: nil)
|
26
26
|
api_path = 'issues'
|
27
27
|
request_params = { milestone: milestone } if milestone
|
28
28
|
|
29
29
|
response = api_interface.send_request(api_path, params: request_params, multipage: true)
|
30
30
|
|
31
|
-
response.map do |issue_data|
|
31
|
+
abstract_issues_list = response.map do |issue_data|
|
32
32
|
number = issue_data.fetch('number')
|
33
33
|
title = issue_data.fetch('title')
|
34
34
|
link = issue_data.fetch('html_url')
|
35
35
|
|
36
36
|
klazz = issue_data.key?('pull_request') ? PR : Issue
|
37
37
|
|
38
|
-
klazz
|
38
|
+
if !only_issues || klazz == Issue
|
39
|
+
klazz.new(number, api_interface, title, link)
|
40
|
+
end
|
39
41
|
end
|
42
|
+
|
43
|
+
abstract_issues_list.compact
|
40
44
|
end
|
41
45
|
|
42
46
|
# params:
|
@@ -24,6 +24,7 @@ module Geet
|
|
24
24
|
# Send a request.
|
25
25
|
#
|
26
26
|
# Returns the parsed response, or an Array, in case of multipage.
|
27
|
+
# Where no body is present in the response, nil is returned.
|
27
28
|
#
|
28
29
|
# params:
|
29
30
|
# :api_path: api path, will be appended to the API URL.
|
@@ -35,8 +36,9 @@ module Geet
|
|
35
36
|
# :data: (Hash) if present, will generate a POST request, otherwise, a GET
|
36
37
|
# :multipage: set true for paged Github responses (eg. issues); it will make the method
|
37
38
|
# return an array, with the concatenated (parsed) responses
|
38
|
-
# :http_method: :get, :patch, :post
|
39
|
-
#
|
39
|
+
# :http_method: symbol format of the method (:get, :patch, :post, :put and :delete)
|
40
|
+
# :get and :post are automatically inferred by the present of :data; the other
|
41
|
+
# cases must be specified.
|
40
42
|
#
|
41
43
|
def send_request(api_path, params: nil, data: nil, multipage: false, http_method: nil)
|
42
44
|
address = api_url(api_path)
|
@@ -46,7 +48,7 @@ module Geet
|
|
46
48
|
loop do
|
47
49
|
response = send_http_request(address, params: params, data: data, http_method: http_method)
|
48
50
|
|
49
|
-
parsed_response = JSON.parse(response.body)
|
51
|
+
parsed_response = JSON.parse(response.body) if response.body
|
50
52
|
|
51
53
|
if error?(response)
|
52
54
|
formatted_error = decode_and_format_error(parsed_response)
|
@@ -133,12 +135,14 @@ module Geet
|
|
133
135
|
case http_method
|
134
136
|
when :get
|
135
137
|
Net::HTTP::Get
|
138
|
+
when :delete
|
139
|
+
Net::HTTP::Delete
|
136
140
|
when :patch
|
137
141
|
Net::HTTP::Patch
|
138
|
-
when :put
|
139
|
-
Net::HTTP::Put
|
140
142
|
when :post
|
141
143
|
Net::HTTP::Post
|
144
|
+
when :put
|
145
|
+
Net::HTTP::Put
|
142
146
|
else
|
143
147
|
raise "Unsupported HTTP method: #{http_method.inspect}"
|
144
148
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Geet
|
4
|
+
module Github
|
5
|
+
class Branch
|
6
|
+
# See https://developer.github.com/v3/git/refs/#delete-a-reference
|
7
|
+
#
|
8
|
+
def self.delete(name, api_interface)
|
9
|
+
api_path = "git/refs/heads/#{name}"
|
10
|
+
|
11
|
+
api_interface.send_request(api_path, http_method: :delete)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/geet/github/gist.rb
CHANGED
data/lib/geet/github/issue.rb
CHANGED
@@ -20,19 +20,7 @@ module Geet
|
|
20
20
|
# See https://developer.github.com/v3/issues/#list-issues-for-a-repository
|
21
21
|
#
|
22
22
|
def self.list(api_interface)
|
23
|
-
|
24
|
-
|
25
|
-
response = api_interface.send_request(api_path, multipage: true)
|
26
|
-
|
27
|
-
response.each_with_object([]) do |issue_data, result|
|
28
|
-
if !issue_data.key?('pull_request')
|
29
|
-
number = issue_data.fetch('number')
|
30
|
-
title = issue_data.fetch('title')
|
31
|
-
link = issue_data.fetch('html_url')
|
32
|
-
|
33
|
-
result << new(number, api_interface, title, link)
|
34
|
-
end
|
35
|
-
end
|
23
|
+
super(api_interface, only_issues: true)
|
36
24
|
end
|
37
25
|
end
|
38
26
|
end
|
data/lib/geet/github/label.rb
CHANGED
@@ -28,7 +28,7 @@ module Geet
|
|
28
28
|
# See https://developer.github.com/v3/issues/labels/#create-a-label
|
29
29
|
def self.create(name, color, api_interface)
|
30
30
|
api_path = 'labels'
|
31
|
-
request_data = {name: name, color: color }
|
31
|
+
request_data = { name: name, color: color }
|
32
32
|
|
33
33
|
api_interface.send_request(api_path, data: request_data)
|
34
34
|
|
@@ -28,6 +28,7 @@ module Geet
|
|
28
28
|
# Send a request.
|
29
29
|
#
|
30
30
|
# Returns the parsed response, or an Array, in case of multipage.
|
31
|
+
# Where no body is present in the response, nil is returned.
|
31
32
|
#
|
32
33
|
# params:
|
33
34
|
# :api_path: api path, will be appended to the API URL.
|
@@ -39,8 +40,9 @@ module Geet
|
|
39
40
|
# :data: (Hash) if present, will generate a POST request, otherwise, a GET
|
40
41
|
# :multipage: set true for paged Github responses (eg. issues); it will make the method
|
41
42
|
# return an array, with the concatenated (parsed) responses
|
42
|
-
# :http_method: :get, :patch, :post
|
43
|
-
#
|
43
|
+
# :http_method: symbol format of the method (:get, :patch, :post, :put and :delete)
|
44
|
+
# :get and :post are automatically inferred by the present of :data; the other
|
45
|
+
# cases must be specified.
|
44
46
|
#
|
45
47
|
def send_request(api_path, params: nil, data: nil, multipage: false, http_method: nil)
|
46
48
|
address = api_url(api_path)
|
@@ -50,7 +52,7 @@ module Geet
|
|
50
52
|
loop do
|
51
53
|
response = send_http_request(address, params: params, data: data, http_method: http_method)
|
52
54
|
|
53
|
-
parsed_response = JSON.parse(response.body)
|
55
|
+
parsed_response = JSON.parse(response.body) if response.body
|
54
56
|
|
55
57
|
if error?(response)
|
56
58
|
formatted_error = decode_and_format_error(parsed_response)
|
@@ -126,12 +128,14 @@ module Geet
|
|
126
128
|
case http_method
|
127
129
|
when :get
|
128
130
|
Net::HTTP::Get
|
131
|
+
when :delete
|
132
|
+
Net::HTTP::Delete
|
129
133
|
when :patch
|
130
134
|
Net::HTTP::Patch
|
131
|
-
when :put
|
132
|
-
Net::HTTP::Put
|
133
135
|
when :post
|
134
136
|
Net::HTTP::Post
|
137
|
+
when :put
|
138
|
+
Net::HTTP::Put
|
135
139
|
else
|
136
140
|
raise "Unsupported HTTP method: #{http_method.inspect}"
|
137
141
|
end
|
@@ -1,17 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'English'
|
3
4
|
require 'shellwords'
|
4
5
|
|
5
6
|
module Geet
|
6
7
|
module Helpers
|
7
8
|
module OsHelper
|
8
|
-
def
|
9
|
+
def open_file_with_default_application(file_or_url)
|
9
10
|
if `uname`.strip == 'Darwin'
|
10
11
|
exec "open #{file_or_url.shellescape}"
|
11
12
|
else
|
12
13
|
exec "xdg-open #{file_or_url.shellescape}"
|
13
14
|
end
|
14
15
|
end
|
16
|
+
|
17
|
+
def execute_command(description, *command_tokens)
|
18
|
+
system(*command_tokens.map(&:shellescape))
|
19
|
+
|
20
|
+
raise "Error during #{description} (exit status: #{$CHILD_STATUS.exitstatus})" if !$CHILD_STATUS.success?
|
21
|
+
end
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
------------------------ >8 ------------------------
|
4
|
+
Please enter above, the title and description for your changes.
|
5
|
+
The first line is the title, and it's mandatory; lines between
|
6
|
+
the second and the separator above, if present, are the description;
|
7
|
+
the separator and any line below are ignored.
|
@@ -12,7 +12,10 @@ module Geet
|
|
12
12
|
# :reviewer_patterns
|
13
13
|
# :no_open_pr
|
14
14
|
#
|
15
|
-
def execute(
|
15
|
+
def execute(
|
16
|
+
repository, title, description, label_patterns: nil, milestone_pattern: nil, reviewer_patterns: nil,
|
17
|
+
no_open_pr: nil, output: $stdout, **
|
18
|
+
)
|
16
19
|
labels_thread = select_labels(repository, label_patterns, output) if label_patterns
|
17
20
|
milestone_thread = find_milestone(repository, milestone_pattern, output) if milestone_pattern
|
18
21
|
reviewers_thread = select_reviewers(repository, reviewer_patterns, output) if reviewer_patterns
|
@@ -36,7 +39,7 @@ module Geet
|
|
36
39
|
if no_open_pr
|
37
40
|
output.puts "PR address: #{pr.link}"
|
38
41
|
else
|
39
|
-
|
42
|
+
open_file_with_default_application(pr.link)
|
40
43
|
end
|
41
44
|
|
42
45
|
pr
|
@@ -3,10 +3,11 @@
|
|
3
3
|
module Geet
|
4
4
|
module Services
|
5
5
|
class MergePr
|
6
|
-
def execute(repository, output: $stdout)
|
6
|
+
def execute(repository, delete_branch: false, output: $stdout)
|
7
7
|
merge_head = find_merge_head(repository)
|
8
8
|
pr = checked_find_branch_pr(repository, merge_head, output)
|
9
9
|
merge_pr(pr, output)
|
10
|
+
do_delete_branch(repository, output) if delete_branch
|
10
11
|
pr
|
11
12
|
end
|
12
13
|
|
@@ -32,6 +33,12 @@ module Geet
|
|
32
33
|
|
33
34
|
pr.merge
|
34
35
|
end
|
36
|
+
|
37
|
+
def do_delete_branch(repository, output)
|
38
|
+
output.puts "Deleting branch #{repository.current_branch}..."
|
39
|
+
|
40
|
+
repository.delete_branch(repository.current_branch)
|
41
|
+
end
|
35
42
|
end
|
36
43
|
end
|
37
44
|
end
|
data/lib/geet/version.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
require 'tempfile'
|
3
5
|
|
@@ -6,7 +8,7 @@ require_relative '../../lib/geet/services/create_gist'
|
|
6
8
|
|
7
9
|
describe Geet::Services::CreateGist do
|
8
10
|
let(:repository) { Geet::Git::Repository.new }
|
9
|
-
let(:tempfile) { Tempfile.open('geet_gist') { |file| file <<
|
11
|
+
let(:tempfile) { Tempfile.open('geet_gist') { |file| file << 'testcontent' } }
|
10
12
|
|
11
13
|
it 'should create a public gist' do
|
12
14
|
expected_output = <<~STR
|
@@ -16,7 +18,7 @@ describe Geet::Services::CreateGist do
|
|
16
18
|
|
17
19
|
actual_output = StringIO.new
|
18
20
|
|
19
|
-
VCR.use_cassette(
|
21
|
+
VCR.use_cassette('create_gist_public') do
|
20
22
|
described_class.new.execute(
|
21
23
|
repository, tempfile.path,
|
22
24
|
description: 'testdescription', publik: true, no_browse: true, output: actual_output
|
@@ -34,7 +36,7 @@ describe Geet::Services::CreateGist do
|
|
34
36
|
|
35
37
|
actual_output = StringIO.new
|
36
38
|
|
37
|
-
VCR.use_cassette(
|
39
|
+
VCR.use_cassette('create_gist_private') do
|
38
40
|
described_class.new.execute(
|
39
41
|
repository, tempfile.path,
|
40
42
|
description: 'testdescription', no_browse: true, output: actual_output
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
require_relative '../../lib/geet/git/repository'
|
@@ -24,7 +26,7 @@ describe Geet::Services::CreateIssue do
|
|
24
26
|
|
25
27
|
actual_output = StringIO.new
|
26
28
|
|
27
|
-
actual_created_issue = VCR.use_cassette(
|
29
|
+
actual_created_issue = VCR.use_cassette('create_issue') do
|
28
30
|
described_class.new.execute(
|
29
31
|
repository, 'Title', 'Description',
|
30
32
|
label_patterns: 'bug,invalid', milestone_pattern: '0.0.1', assignee_patterns: 'nald-ts,nald-fr',
|
@@ -53,8 +55,9 @@ describe Geet::Services::CreateIssue do
|
|
53
55
|
|
54
56
|
actual_output = StringIO.new
|
55
57
|
|
56
|
-
actual_created_issue = VCR.use_cassette(
|
57
|
-
|
58
|
+
actual_created_issue = VCR.use_cassette('create_issue_upstream') do
|
59
|
+
create_options = { no_open_issue: true, output: actual_output }
|
60
|
+
described_class.new.execute(upstream_repository, 'Title', 'Description', create_options)
|
58
61
|
end
|
59
62
|
|
60
63
|
expect(actual_output.string).to eql(expected_output)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
require_relative '../../lib/geet/git/repository'
|
@@ -17,7 +19,7 @@ describe Geet::Services::CreateLabel do
|
|
17
19
|
|
18
20
|
actual_output = StringIO.new
|
19
21
|
|
20
|
-
actual_created_label = VCR.use_cassette(
|
22
|
+
actual_created_label = VCR.use_cassette('create_label') do
|
21
23
|
described_class.new.execute(repository, 'my_label', color: 'c64c64', output: actual_output)
|
22
24
|
end
|
23
25
|
|
@@ -34,16 +36,16 @@ describe Geet::Services::CreateLabel do
|
|
34
36
|
|
35
37
|
expected_output_template = <<~STR
|
36
38
|
Creating label...
|
37
|
-
Created with color
|
39
|
+
Created with color #%<color>s
|
38
40
|
STR
|
39
41
|
|
40
42
|
actual_output = StringIO.new
|
41
43
|
|
42
|
-
actual_created_label = VCR.use_cassette(
|
44
|
+
actual_created_label = VCR.use_cassette('create_label_with_random_color') do
|
43
45
|
described_class.new.execute(repository, 'my_label', output: actual_output)
|
44
46
|
end
|
45
47
|
|
46
|
-
expected_output = expected_output_template
|
48
|
+
expected_output = format(expected_output_template, color: actual_created_label.color)
|
47
49
|
|
48
50
|
expect(actual_output.string).to eql(expected_output)
|
49
51
|
|