geet 0.3.17 → 0.4.2

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -9
  3. data/bin/geet +4 -0
  4. data/geet.gemspec +1 -1
  5. data/lib/geet/commandline/commands.rb +2 -0
  6. data/lib/geet/commandline/configuration.rb +16 -1
  7. data/lib/geet/git/repository.rb +8 -2
  8. data/lib/geet/github/abstract_issue.rb +3 -0
  9. data/lib/geet/github/api_interface.rb +2 -0
  10. data/lib/geet/github/issue.rb +1 -1
  11. data/lib/geet/github/pr.rb +28 -9
  12. data/lib/geet/github/remote_repository.rb +37 -0
  13. data/lib/geet/gitlab/api_interface.rb +2 -0
  14. data/lib/geet/helpers/os_helper.rb +4 -3
  15. data/lib/geet/helpers/services_workflow_helper.rb +9 -7
  16. data/lib/geet/services/add_upstream_repo.rb +37 -0
  17. data/lib/geet/services/comment_pr.rb +1 -2
  18. data/lib/geet/services/create_pr.rb +13 -11
  19. data/lib/geet/services/merge_pr.rb +7 -8
  20. data/lib/geet/services/open_pr.rb +2 -3
  21. data/lib/geet/services/open_repo.rb +50 -0
  22. data/lib/geet/utils/attributes_selection_manager.rb +7 -0
  23. data/lib/geet/utils/git_client.rb +74 -33
  24. data/lib/geet/version.rb +1 -1
  25. data/spec/integration/comment_pr_spec.rb +1 -1
  26. data/spec/integration/create_issue_spec.rb +3 -3
  27. data/spec/integration/create_label_spec.rb +5 -5
  28. data/spec/integration/create_milestone_spec.rb +1 -1
  29. data/spec/integration/create_pr_spec.rb +20 -15
  30. data/spec/integration/list_issues_spec.rb +6 -6
  31. data/spec/integration/list_labels_spec.rb +4 -4
  32. data/spec/integration/list_milestones_spec.rb +4 -4
  33. data/spec/integration/list_prs_spec.rb +3 -3
  34. data/spec/integration/merge_pr_spec.rb +13 -9
  35. data/spec/integration/open_pr_spec.rb +1 -1
  36. data/spec/integration/open_repo_spec.rb +46 -0
  37. data/spec/vcr_cassettes/create_issue.yml +1 -1
  38. data/spec/vcr_cassettes/create_issue_upstream.yml +1 -1
  39. data/spec/vcr_cassettes/github_com/create_pr.yml +1 -1
  40. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_create_upstream.yml +1 -1
  41. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_with_push.yml +1 -1
  42. data/spec/vcr_cassettes/github_com/create_pr_upstream.yml +1 -1
  43. data/spec/vcr_cassettes/github_com/create_pr_upstream_without_write_permissions.yml +1 -1
  44. metadata +7 -4
  45. data/lib/geet/shared/branches.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a475adafa9bb0791266b171ea0e8ba05caf076227f1d3b68efe75697479c2cca
4
- data.tar.gz: e63c7d8d46c3f44b0962ee1c3445e3aaf076f03b41f218bfde957779decfd445
3
+ metadata.gz: c13aed62c61e4cdaf20931f657c101daee30925b36a9b87fcce3b86d009d3c69
4
+ data.tar.gz: 5f13669499214cb7b13d96f05a25547a19a472e9d855e6d58967f5ce3ba3d138
5
5
  SHA512:
6
- metadata.gz: 1d19fde0bb7c9733ee7aaea4c249378077d242c56f92055f29c1f3eb27e200de06890f2d5e97c76a58ffc604c46674ceddd8fc349b17907cee89fdfabbd029c0
7
- data.tar.gz: dbdad4b310621f063af44aabfe71fb485232a836085c5729a057d350911408db3e3dd18e7a40459ceb80d4cae000f279ceaf2798a1eaf9f6caeab7b704da3fa8
6
+ metadata.gz: 8cb5a717299f16140d7ce1506d0bba549b87d97e4405f1d32549ea519ba58ccb2e918ce0561ecf714fcc2d3660f1172890c6fee99fc53270216b6656c730e7b9
7
+ data.tar.gz: 7ad452d8d32f8e96b853925aa745ad7664a4dd14cd97754ea6ab47627718938bb8af03e52eb4db28e425d96605bff83de74332a196d79845fd78baa870cbeef9
data/README.md CHANGED
@@ -18,17 +18,12 @@ The functionalities currently supported are:
18
18
 
19
19
  - Github/Gitlab:
20
20
  - create label
21
- - list issues
22
- - list labels
23
- - list milestones
24
- - list PRs
25
- - merge PR
21
+ - list issues, labels, milestones, MR/PRs
22
+ - merge MR/PR
23
+ - open repository
26
24
  - Github:
27
25
  - comment PR
28
- - create gist
29
- - create issue
30
- - create milestone
31
- - create PR
26
+ - create gist, issue, milestone, PR
32
27
 
33
28
  ## Samples
34
29
 
data/bin/geet CHANGED
@@ -76,6 +76,10 @@ class GeetLauncher
76
76
  Services::MergePr.new(repository).execute(**options)
77
77
  when PR_OPEN_COMMAND
78
78
  Services::OpenPr.new(repository).execute(**options)
79
+ when REPO_ADD_UPSTREAM_COMMAND
80
+ Services::AddUpstreamRepo.new(repository).execute
81
+ when REPO_OPEN_COMMAND
82
+ Services::OpenRepo.new(repository).execute(**options)
79
83
  else
80
84
  raise "Internal error - Unrecognized command #{command.inspect}"
81
85
  end
data/geet.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.platform = Gem::Platform::RUBY
11
11
  s.required_ruby_version = '>= 2.3.0'
12
12
  s.authors = ['Saverio Miroddi']
13
- s.date = '2021-01-18'
13
+ s.date = '2021-07-26'
14
14
  s.email = ['saverio.pub2@gmail.com']
15
15
  s.homepage = 'https://github.com/saveriomiroddi/geet'
16
16
  s.summary = 'Commandline interface for performing SCM host operations, eg. create a PR on GitHub'
@@ -16,6 +16,8 @@ module Geet
16
16
  PR_LIST_COMMAND = 'pr.list'
17
17
  PR_MERGE_COMMAND = 'pr.merge'
18
18
  PR_OPEN_COMMAND = 'pr.open'
19
+ REPO_ADD_UPSTREAM_COMMAND = 'repo.add_upstream'
20
+ REPO_OPEN_COMMAND = 'repo.open'
19
21
  end
20
22
  end
21
23
  end
@@ -67,7 +67,8 @@ module Geet
67
67
  PR_CREATE_OPTIONS = [
68
68
  ['-A', '--automated-mode', "Automate the branch operations (see long help)"],
69
69
  ['-n', '--no-open-pr', "Don't open the PR link in the browser after creation"],
70
- ['-b', '--base develop', "Specify the base branch; defaults to `master`"],
70
+ ['-b', '--base develop', "Specify the base branch; defaults to the main branch"],
71
+ ['-d', '--draft', "Create as draft"],
71
72
  ['-l', '--labels "legacy,code review"', 'Labels'],
72
73
  ['-m', '--milestone 1.5.0', 'Milestone title pattern'],
73
74
  ['-r', '--reviewers john,tom,adrian,kevin', 'Reviewer logins'],
@@ -95,9 +96,19 @@ module Geet
95
96
  ]
96
97
 
97
98
  PR_OPEN_OPTIONS = [
99
+ ['-u', '--upstream', 'List on the upstream repository'],
98
100
  long_help: 'Open in the browser the PR for the current branch'
99
101
  ]
100
102
 
103
+ REPO_ADD_UPSTREAM_OPTIONS = [
104
+ long_help: 'Add the upstream repository to the current repository (configuration).'
105
+ ]
106
+
107
+ REPO_OPEN_OPTIONS = [
108
+ ['-u', '--upstream', 'Open the upstream repository'],
109
+ long_help: 'Open the current repository in the browser'
110
+ ]
111
+
101
112
  # Commands decoding table
102
113
 
103
114
  COMMANDS_DECODING_TABLE = {
@@ -124,6 +135,10 @@ module Geet
124
135
  'merge' => PR_MERGE_OPTIONS,
125
136
  'open' => PR_OPEN_OPTIONS,
126
137
  },
138
+ 'repo' => {
139
+ 'add_upstream' => REPO_ADD_UPSTREAM_OPTIONS,
140
+ 'open' => REPO_OPEN_OPTIONS,
141
+ },
127
142
  }
128
143
 
129
144
  # Public interface
@@ -74,17 +74,23 @@ module Geet
74
74
  attempt_provider_call(:Milestone, :close, number, api_interface)
75
75
  end
76
76
 
77
- def create_pr(title, description, head, base: nil)
77
+ def create_pr(title, description, head, base, draft)
78
78
  confirm(LOCAL_ACTION_ON_UPSTREAM_REPOSITORY_MESSAGE) if local_action_on_upstream_repository? && @warnings
79
79
  confirm(ACTION_ON_PROTECTED_REPOSITORY_MESSAGE) if action_on_protected_repository? && @warnings
80
80
 
81
- attempt_provider_call(:PR, :create, title, description, head, api_interface, base: base)
81
+ attempt_provider_call(:PR, :create, title, description, head, api_interface, base, draft: draft)
82
82
  end
83
83
 
84
84
  def prs(owner: nil, head: nil, milestone: nil)
85
85
  attempt_provider_call(:PR, :list, api_interface, owner: owner, head: head, milestone: milestone)
86
86
  end
87
87
 
88
+ # Returns the RemoteRepository instance.
89
+ #
90
+ def remote
91
+ attempt_provider_call(:RemoteRepository, :find, api_interface)
92
+ end
93
+
88
94
  # REMOTE FUNCTIONALITIES (ACCOUNT)
89
95
 
90
96
  def authenticated_user
@@ -22,6 +22,9 @@ module Geet
22
22
 
23
23
  # See https://developer.github.com/v3/issues/#list-issues-for-a-repository
24
24
  #
25
+ # This works both for Issues and PRs, however, when the `/pulls` API (path) is used, additional
26
+ # information is provided (e.g. `head`).
27
+ #
25
28
  def self.list(api_interface, milestone: nil, assignee: nil, &type_filter)
26
29
  api_path = 'issues'
27
30
 
@@ -11,6 +11,8 @@ module Geet
11
11
  API_AUTH_USER = '' # We don't need the login, as the API key uniquely identifies the user
12
12
  API_BASE_URL = 'https://api.github.com'
13
13
 
14
+ attr_reader :repository_path
15
+
14
16
  # repo_path: optional for operations that don't require a repository, eg. gist creation.
15
17
  # upstream: boolean; makes sense only when :repo_path is set.
16
18
  #
@@ -8,7 +8,7 @@ module Geet
8
8
  class Issue < Geet::Github::AbstractIssue
9
9
  def self.create(title, description, api_interface, **)
10
10
  api_path = 'issues'
11
- request_data = { title: title, body: description, base: 'master' }
11
+ request_data = { title: title, body: description }
12
12
 
13
13
  response = api_interface.send_request(api_path, data: request_data)
14
14
 
@@ -8,16 +8,15 @@ module Geet
8
8
  class PR < AbstractIssue
9
9
  # See https://developer.github.com/v3/pulls/#create-a-pull-request
10
10
  #
11
- def self.create(title, description, head, api_interface, base: nil)
11
+ def self.create(title, description, head, api_interface, base, draft: false)
12
12
  api_path = 'pulls'
13
- base ||= 'master'
14
13
 
15
14
  if api_interface.upstream?
16
15
  authenticated_user = Geet::Github::User.authenticated(api_interface).username
17
16
  head = "#{authenticated_user}:#{head}"
18
17
  end
19
18
 
20
- request_data = { title: title, body: description, head: head, base: base }
19
+ request_data = { title: title, body: description, head: head, base: base, draft: draft }
21
20
 
22
21
  response = api_interface.send_request(api_path, data: request_data)
23
22
 
@@ -33,14 +32,34 @@ module Geet
33
32
 
34
33
  if head
35
34
  api_path = 'pulls'
36
- request_params = { head: "#{owner}:#{head}" }
37
35
 
38
- response = api_interface.send_request(api_path, params: request_params, multipage: true)
36
+ # Technically, the upstream approach could be used for both, but it's actually good to have
37
+ # both of them as reference.
38
+ #
39
+ # For upstream pulls, the owner is the authenticated user, otherwise, the repository owner.
40
+ #
41
+ response = if api_interface.upstream?
42
+ unfiltered_response = api_interface.send_request(api_path, multipage: true)
43
+
44
+ # VERY weird. From the docs, it's not clear if the user/org is required in the `head` parameter,
45
+ # but:
46
+ #
47
+ # - if it isn't included (eg. `anything`), the parameter is ignored
48
+ # - if it's included (eg. `saveriomiroddi:local_branch_name`), an empty resultset is returned.
49
+ #
50
+ # For this reason, we can't use that param, and have to filter manually.
51
+ #
52
+ unfiltered_response.select { |pr_data| pr_data.fetch('head').fetch('label') == "#{owner}:#{head}" }
53
+ else
54
+ request_params = { head: "#{owner}:#{head}" }
55
+
56
+ api_interface.send_request(api_path, params: request_params, multipage: true)
57
+ end
39
58
 
40
- response.map do |issue_data|
41
- number = issue_data.fetch('number')
42
- title = issue_data.fetch('title')
43
- link = issue_data.fetch('html_url')
59
+ response.map do |pr_data|
60
+ number = pr_data.fetch('number')
61
+ title = pr_data.fetch('title')
62
+ link = pr_data.fetch('html_url')
44
63
 
45
64
  new(number, api_interface, title, link)
46
65
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module Github
5
+ # A remote repository. Currently only provides the parent path.
6
+ #
7
+ # It's a difficult choice whether to independently use the repository path, or relying on the one
8
+ # stored in the ApiInterface.
9
+ # The former design is conceptually cleaner, but it practically (as of the current design) introduces
10
+ # duplication. All in all, for simplicity, the latter design is chosen, but is subject to redesign.
11
+ #
12
+ class RemoteRepository
13
+ # Nil if the repository is not a fork.
14
+ #
15
+ attr_reader :parent_path
16
+
17
+ def initialize(api_interface, parent_path: nil)
18
+ @api_interface = api_interface
19
+ @parent_path = parent_path
20
+ end
21
+
22
+ # Get the repository parent path.
23
+ #
24
+ # https://docs.github.com/en/rest/reference/repos#get-a-repository
25
+ #
26
+ def self.find(api_interface)
27
+ api_path = "/repos/#{api_interface.repository_path}"
28
+
29
+ response = api_interface.send_request(api_path)
30
+
31
+ parent_path = response['parent']&.fetch("full_name")
32
+
33
+ self.new(api_interface, parent_path: parent_path)
34
+ end
35
+ end # module RemoteRepository
36
+ end # module GitHub
37
+ end # module Geet
@@ -10,6 +10,8 @@ module Geet
10
10
  class ApiInterface
11
11
  API_BASE_URL = 'https://gitlab.com/api/v4'
12
12
 
13
+ attr_reader :repository_path
14
+
13
15
  # repo_path: "path/namespace"; required for the current GitLab operations.
14
16
  # upstream: boolean; required for the current GitLab operations.
15
17
  #
@@ -26,14 +26,15 @@ module Geet
26
26
  # interactive: set when required; in this case, a different API will be used (`system()`
27
27
  # instead of `popen3`).
28
28
  # silent_stderr: don't print the stderr output
29
+ # allow_error: don't raise error on failure
29
30
  #
30
- def execute_command(command, description: nil, interactive: false, silent_stderr: false)
31
+ def execute_command(command, description: nil, interactive: false, silent_stderr: false, allow_error: false)
31
32
  description_message = " on #{description}" if description
32
33
 
33
34
  if interactive
34
35
  system(command)
35
36
 
36
- if !$CHILD_STATUS.success?
37
+ if !$CHILD_STATUS.success? && !allow_error
37
38
  raise "Error#{description_message} (exit status: #{$CHILD_STATUS.exitstatus})"
38
39
  end
39
40
  else
@@ -43,7 +44,7 @@ module Geet
43
44
 
44
45
  puts stderr_content if stderr_content != '' && !silent_stderr
45
46
 
46
- if !wait_thread.value.success?
47
+ if !wait_thread.value.success? && !allow_error
47
48
  error_message = stderr_content.lines.first&.strip || "Error running command #{command.inspect}"
48
49
  raise "Error#{description_message}: #{error_message}"
49
50
  end
@@ -9,17 +9,19 @@ module Geet
9
9
  # Helper for services common workflow, for example, find the merge head.
10
10
  #
11
11
  module ServicesWorkflowHelper
12
- # Requires: @git_client
13
- #
14
- def find_merge_head
15
- [@git_client.owner, @git_client.current_branch]
16
- end
17
-
18
12
  # Expect to find only one.
19
13
  #
20
14
  # Requires: @out, @repository.
21
15
  #
22
- def checked_find_branch_pr(owner, head)
16
+ def checked_find_branch_pr
17
+ owner = if @repository.upstream?
18
+ @repository.authenticated_user.username
19
+ else
20
+ @git_client.owner
21
+ end
22
+
23
+ head = @git_client.current_branch
24
+
23
25
  @out.puts "Finding PR with head (#{owner}:#{head})..."
24
26
 
25
27
  prs = @repository.prs(owner: owner, head: head)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module Services
5
+ # Add the upstream repository to the current repository (configuration).
6
+ #
7
+ class AddUpstreamRepo
8
+ DEFAULT_GIT_CLIENT = Utils::GitClient.new
9
+
10
+ def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
11
+ @repository = repository
12
+ @out = out
13
+ @git_client = git_client
14
+ end
15
+
16
+ def execute
17
+ raise "Upstream remote already existing!" if @git_client.remote_defined?(Utils::GitClient::UPSTREAM_NAME)
18
+
19
+ parent_path = @repository.remote.parent_path
20
+
21
+ parent_url = compose_parent_url(parent_path)
22
+
23
+ @git_client.add_remote(Utils::GitClient::UPSTREAM_NAME, parent_url)
24
+ end
25
+
26
+ private
27
+
28
+ # Use the same protocol as the main repository.
29
+ #
30
+ def compose_parent_url(parent_path)
31
+ protocol, domain, separator, _, suffix = @git_client.remote_components
32
+
33
+ "#{protocol}#{domain}#{separator}#{parent_path}#{suffix}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -20,8 +20,7 @@ module Geet
20
20
  end
21
21
 
22
22
  def execute(comment, no_open_pr: nil)
23
- merge_owner, merge_head = find_merge_head
24
- pr = checked_find_branch_pr(merge_owner, merge_head)
23
+ pr = checked_find_branch_pr
25
24
  pr.comment(comment)
26
25
  open_file_with_default_application(pr.link) unless no_open_pr
27
26
  pr
@@ -25,7 +25,7 @@ module Geet
25
25
  #
26
26
  def execute(
27
27
  title, description, labels: nil, milestone: nil, reviewers: nil,
28
- base: nil, no_open_pr: nil, automated_mode: false, **
28
+ base: nil, draft: false, no_open_pr: nil, automated_mode: false, **
29
29
  )
30
30
  ensure_clean_tree if automated_mode
31
31
 
@@ -37,9 +37,9 @@ module Geet
37
37
  selected_labels, selected_milestone, selected_reviewers = find_and_select_attributes(labels, milestone, reviewers)
38
38
  end
39
39
 
40
- sync_with_upstream_branch if automated_mode
40
+ sync_with_remote_branch if automated_mode
41
41
 
42
- pr = create_pr(title, description, base)
42
+ pr = create_pr(title, description, base: base, draft: draft)
43
43
 
44
44
  if user_has_write_permissions
45
45
  edit_pr(pr, selected_labels, selected_milestone, selected_reviewers)
@@ -81,24 +81,26 @@ module Geet
81
81
  selection_manager.select_attributes
82
82
  end
83
83
 
84
- def sync_with_upstream_branch
85
- if @git_client.upstream_branch
86
- @out.puts "Pushing to upstream branch..."
84
+ def sync_with_remote_branch
85
+ if @git_client.remote_branch
86
+ @out.puts "Pushing to remote branch..."
87
87
 
88
88
  @git_client.push
89
89
  else
90
- upstream_branch = @git_client.current_branch
90
+ remote_branch = @git_client.current_branch
91
91
 
92
- @out.puts "Creating upstream branch #{upstream_branch.inspect}..."
92
+ @out.puts "Creating remote branch #{remote_branch.inspect}..."
93
93
 
94
- @git_client.push(upstream_branch: upstream_branch)
94
+ @git_client.push(remote_branch: remote_branch)
95
95
  end
96
96
  end
97
97
 
98
- def create_pr(title, description, base)
98
+ def create_pr(title, description, base:, draft:)
99
99
  @out.puts 'Creating PR...'
100
100
 
101
- @repository.create_pr(title, description, @git_client.current_branch, base: base)
101
+ base ||= @git_client.main_branch
102
+
103
+ @repository.create_pr(title, description, @git_client.current_branch, base, draft)
102
104
  end
103
105
 
104
106
  def edit_pr(pr, labels, milestone, reviewers)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helpers/services_workflow_helper'
4
- require_relative '../shared/branches'
5
4
 
6
5
  module Geet
7
6
  module Services
@@ -13,7 +12,7 @@ module Geet
13
12
  #
14
13
  class MergePr
15
14
  include Geet::Helpers::ServicesWorkflowHelper
16
- include Geet::Shared::Branches
15
+ include Geet::Shared
17
16
 
18
17
  DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
19
18
 
@@ -24,8 +23,7 @@ module Geet
24
23
  end
25
24
 
26
25
  def execute(delete_branch: false)
27
- merge_owner, merge_head = find_merge_head
28
- pr = checked_find_branch_pr(merge_owner, merge_head)
26
+ pr = checked_find_branch_pr
29
27
 
30
28
  merge_pr(pr)
31
29
 
@@ -37,13 +35,14 @@ module Geet
37
35
 
38
36
  fetch_repository
39
37
 
40
- if upstream_branch_gone?
38
+ if remote_branch_gone?
41
39
  pr_branch = @git_client.current_branch
40
+ main_branch = @git_client.main_branch
42
41
 
43
42
  # The rebase could also be placed after the branch deletion. There are pros/cons;
44
43
  # currently, it's not important.
45
44
  #
46
- checkout_branch(MAIN_BRANCH)
45
+ checkout_branch(main_branch)
47
46
  rebase
48
47
 
49
48
  delete_local_branch(pr_branch)
@@ -72,8 +71,8 @@ module Geet
72
71
  @git_client.fetch
73
72
  end
74
73
 
75
- def upstream_branch_gone?
76
- @git_client.upstream_branch_gone?
74
+ def remote_branch_gone?
75
+ @git_client.remote_branch_gone?
77
76
  end
78
77
 
79
78
  def checkout_branch(branch)