geet 0.3.17 → 0.4.2

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