geet 0.1.12 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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