geet 0.1.12 → 0.2.0

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +32 -6
  3. data/.rubocop_todo.yml +24 -1
  4. data/.travis.yml +7 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +18 -0
  7. data/README.md +45 -11
  8. data/Rakefile +5 -0
  9. data/bin/geet +6 -9
  10. data/extra/issue_editing.png +0 -0
  11. data/extra/pr_editing.png +0 -0
  12. data/geet.gemspec +2 -2
  13. data/lib/geet/commandline/commands.rb +3 -1
  14. data/lib/geet/commandline/configuration.rb +37 -31
  15. data/lib/geet/commandline/editor.rb +66 -0
  16. data/lib/geet/git/repository.rb +5 -1
  17. data/lib/geet/github/abstract_issue.rb +7 -3
  18. data/lib/geet/github/api_interface.rb +9 -5
  19. data/lib/geet/github/branch.rb +15 -0
  20. data/lib/geet/github/gist.rb +1 -1
  21. data/lib/geet/github/issue.rb +1 -13
  22. data/lib/geet/github/label.rb +1 -1
  23. data/lib/geet/gitlab/api_interface.rb +9 -5
  24. data/lib/geet/helpers/os_helper.rb +8 -1
  25. data/lib/geet/resources/edit_summary_template.md +7 -0
  26. data/lib/geet/services/create_gist.rb +1 -1
  27. data/lib/geet/services/create_issue.rb +1 -1
  28. data/lib/geet/services/create_pr.rb +5 -2
  29. data/lib/geet/services/merge_pr.rb +8 -1
  30. data/lib/geet/version.rb +1 -1
  31. data/spec/integration/create_gist_spec.rb +5 -3
  32. data/spec/integration/create_issue_spec.rb +6 -3
  33. data/spec/integration/create_label_spec.rb +6 -4
  34. data/spec/integration/create_pr_spec.rb +4 -2
  35. data/spec/integration/list_issues_spec.rb +5 -3
  36. data/spec/integration/list_labels_spec.rb +4 -2
  37. data/spec/integration/list_milestones_spec.rb +3 -1
  38. data/spec/integration/list_prs_spec.rb +4 -2
  39. data/spec/integration/merge_pr_spec.rb +26 -1
  40. data/spec/spec_helper.rb +4 -2
  41. data/spec/vcr_cassettes/merge_pr_with_branch_deletion.yml +222 -0
  42. 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
@@ -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 ! klass.respond_to?(meth)
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.new(number, api_interface, title, link)
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 and :put are accepted, but only :patch/:put are meaningful,
39
- # since the others are automatically inferred by :data.
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
@@ -6,7 +6,7 @@ module Geet
6
6
  module Github
7
7
  class Gist
8
8
  def self.create(filename, content, api_interface, description: nil, publik: false)
9
- api_path = "/gists"
9
+ api_path = '/gists'
10
10
 
11
11
  request_data = prepare_request_data(filename, content, description, publik)
12
12
 
@@ -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
- api_path = 'issues'
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
@@ -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 and :put are accepted, but only :patch/:put are meaningful,
43
- # since the others are automatically inferred by :data.
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 os_open(file_or_url)
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.
@@ -24,7 +24,7 @@ module Geet
24
24
  if no_browse
25
25
  output.puts "Gist address: #{gist.link}"
26
26
  else
27
- os_open(gist.link)
27
+ open_file_with_default_application(gist.link)
28
28
  end
29
29
  end
30
30
  end
@@ -46,7 +46,7 @@ module Geet
46
46
  if no_open_issue
47
47
  output.puts "Issue address: #{issue.link}"
48
48
  else
49
- os_open(issue.link)
49
+ open_file_with_default_application(issue.link)
50
50
  end
51
51
 
52
52
  issue
@@ -12,7 +12,10 @@ module Geet
12
12
  # :reviewer_patterns
13
13
  # :no_open_pr
14
14
  #
15
- def execute(repository, title, description, label_patterns: nil, milestone_pattern: nil, reviewer_patterns: nil, no_open_pr: nil, output: $stdout, **)
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
- os_open(pr.link)
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geet
4
- VERSION = '0.1.12'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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 << "testcontent" } }
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("create_gist_public") do
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("create_gist_private") do
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("create_issue") do
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("create_issue_upstream") do
57
- described_class.new.execute(upstream_repository, 'Title', 'Description', no_open_issue: true, output: actual_output)
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("create_label") do
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 #%s
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("create_label_with_random_color") do
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 % actual_created_label.color
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