geet 0.3.15 → 0.4.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +7 -0
  4. data/README.md +4 -9
  5. data/bin/geet +19 -8
  6. data/geet.gemspec +1 -1
  7. data/lib/geet/commandline/commands.rb +3 -0
  8. data/lib/geet/commandline/configuration.rb +20 -1
  9. data/lib/geet/git/repository.rb +17 -3
  10. data/lib/geet/github/api_interface.rb +2 -0
  11. data/lib/geet/github/branch.rb +1 -1
  12. data/lib/geet/github/issue.rb +2 -2
  13. data/lib/geet/github/label.rb +2 -2
  14. data/lib/geet/github/milestone.rb +16 -3
  15. data/lib/geet/github/pr.rb +2 -3
  16. data/lib/geet/github/remote_repository.rb +37 -0
  17. data/lib/geet/github/user.rb +2 -2
  18. data/lib/geet/gitlab/api_interface.rb +2 -0
  19. data/lib/geet/gitlab/label.rb +2 -2
  20. data/lib/geet/gitlab/milestone.rb +1 -1
  21. data/lib/geet/gitlab/user.rb +1 -1
  22. data/lib/geet/helpers/os_helper.rb +4 -3
  23. data/lib/geet/services/add_upstream_repo.rb +37 -0
  24. data/lib/geet/services/close_milestones.rb +46 -0
  25. data/lib/geet/services/create_issue.rb +5 -3
  26. data/lib/geet/services/create_pr.rb +11 -7
  27. data/lib/geet/services/list_issues.rb +4 -1
  28. data/lib/geet/services/merge_pr.rb +55 -4
  29. data/lib/geet/services/open_repo.rb +50 -0
  30. data/lib/geet/shared/selection.rb +3 -0
  31. data/lib/geet/utils/attributes_selection_manager.rb +12 -3
  32. data/lib/geet/utils/git_client.rb +122 -29
  33. data/lib/geet/version.rb +1 -1
  34. data/spec/integration/comment_pr_spec.rb +1 -1
  35. data/spec/integration/create_issue_spec.rb +4 -4
  36. data/spec/integration/create_label_spec.rb +5 -5
  37. data/spec/integration/create_milestone_spec.rb +1 -1
  38. data/spec/integration/create_pr_spec.rb +13 -8
  39. data/spec/integration/list_issues_spec.rb +6 -6
  40. data/spec/integration/list_labels_spec.rb +4 -4
  41. data/spec/integration/list_milestones_spec.rb +4 -4
  42. data/spec/integration/list_prs_spec.rb +3 -3
  43. data/spec/integration/merge_pr_spec.rb +33 -4
  44. data/spec/integration/open_pr_spec.rb +1 -1
  45. data/spec/integration/open_repo_spec.rb +46 -0
  46. data/spec/vcr_cassettes/create_issue.yml +1 -1
  47. data/spec/vcr_cassettes/create_issue_upstream.yml +1 -1
  48. data/spec/vcr_cassettes/github_com/create_pr.yml +1 -1
  49. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_create_upstream.yml +1 -1
  50. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_with_push.yml +1 -1
  51. data/spec/vcr_cassettes/github_com/create_pr_upstream.yml +1 -1
  52. data/spec/vcr_cassettes/github_com/create_pr_upstream_without_write_permissions.yml +1 -1
  53. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24fb5de6b678ccceae3bf6366beded7c208a6d44f75db7303063176436365a5d
4
- data.tar.gz: 4de89e5bfd5414e12cdbf74badf7ed36254f657dc9b8c3fe54c85d8af617b025
3
+ metadata.gz: 277ed0b36c021a28cc688fd768955c5639d0658885f60ff45f2a70c179a1e507
4
+ data.tar.gz: f181e99e34416ebf216d4fb603fd213217a9d188060d38ac2e9770e02aa5befb
5
5
  SHA512:
6
- metadata.gz: f7631017c938ebf68f5c94e142fe00ab7a905cfa3c01383ed61296c0e1db29e76d743d7818c524732755cd7231d654316428bd2b23d2ae450dc83fac3856058e
7
- data.tar.gz: b6e366dd0e923d4b8a4e91781ba5820365a885b866b377907ad965d54f71ac2a9dd061900e8b011c1f54e5c7619bf87d58af7474039deb476d1f3322c745242b
6
+ metadata.gz: 1b5a73ec3c4ffb45daf21dba976f1c6330b0c4a4f2ee0ea3065674710026745834287586dc897b7ec3a1a645e1ed7a3d60f7ca580a46b5f47dc2ed25b6031ee7
7
+ data.tar.gz: 0a4b834f9fcba3502c6f6408511001ab35fc08c5a5c4987b9c51ad5a4a16708ff714a7f47d993cdcd1decf0cf57dad75f4147deab2ac65363cad8cb6f37ee605
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  /.ruby-version
2
2
  /.ruby-gemset
3
3
  /Gemfile.lock
4
+ /test_repos
data/.travis.yml CHANGED
@@ -5,6 +5,13 @@ rvm:
5
5
  - 2.4
6
6
  - 2.5
7
7
  - 2.6
8
+ - 2.7
9
+ - 3.0
10
+ - ruby-head
11
+ matrix:
12
+ fast_finish: true
13
+ allow_failures:
14
+ - rvm: ruby-head
8
15
  # API tokens are always required, but not used in testing, since no requests are actually made.
9
16
  env:
10
17
  - GITHUB_API_TOKEN=phony GITLAB_API_TOKEN=phony
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
@@ -28,24 +28,31 @@ class GeetLauncher
28
28
  filename = options.delete(:filename)
29
29
  options[:publik] = options.delete(:public) if options.key?(:public)
30
30
 
31
- Services::CreateGist.new.execute(filename, options)
31
+ Services::CreateGist.new.execute(filename, **options)
32
32
  when ISSUE_CREATE_COMMAND
33
33
  summary = options[:summary] || Commandline::Editor.new.edit_content(help: SUMMARY_TEMPLATE)
34
34
  title, description = split_summary(summary)
35
35
 
36
36
  options = default_to_manual_selection(options, :labels, :milestone, :assignees)
37
37
 
38
- Services::CreateIssue.new(repository).execute(title, description, options)
38
+ Services::CreateIssue.new(repository).execute(title, description, **options)
39
39
  when LABEL_CREATE_COMMAND
40
40
  name = options.delete(:name)
41
41
 
42
- Services::CreateLabel.new(repository).execute(name, options)
42
+ Services::CreateLabel.new(repository).execute(name, **options)
43
43
  when ISSUE_LIST_COMMAND
44
44
  options = default_to_manual_selection(options, :assignee)
45
45
 
46
- Services::ListIssues.new(repository).execute(options)
46
+ Services::ListIssues.new(repository).execute(**options)
47
47
  when LABEL_LIST_COMMAND
48
48
  Services::ListLabels.new(repository).execute
49
+ when MILESTONE_CLOSE_COMMAND
50
+ # Don't support user selection. This requires extra complexity, specifically, matching by number
51
+ # while displaying the titles (see AttributesSelectionManager).
52
+ #
53
+ options = {numbers: Shared::Selection::MANUAL_LIST_SELECTION_FLAG}
54
+
55
+ Services::CloseMilestones.new(repository).execute(**options)
49
56
  when MILESTONE_CREATE_COMMAND
50
57
  title = options.delete(:title)
51
58
 
@@ -55,20 +62,24 @@ class GeetLauncher
55
62
  when PR_COMMENT_COMMAND
56
63
  comment = options.delete(:comment)
57
64
 
58
- Services::CommentPr.new(repository).execute(comment, options)
65
+ Services::CommentPr.new(repository).execute(comment, **options)
59
66
  when PR_CREATE_COMMAND
60
67
  summary = options[:summary] || edit_pr_summary(base: options[:base])
61
68
  title, description = split_summary(summary)
62
69
 
63
70
  options = default_to_manual_selection(options, :labels, :milestone, :reviewers)
64
71
 
65
- Services::CreatePr.new(repository).execute(title, description, options)
72
+ Services::CreatePr.new(repository).execute(title, description, **options)
66
73
  when PR_LIST_COMMAND
67
74
  Services::ListPrs.new(repository).execute
68
75
  when PR_MERGE_COMMAND
69
- Services::MergePr.new(repository).execute(options)
76
+ Services::MergePr.new(repository).execute(**options)
70
77
  when PR_OPEN_COMMAND
71
- Services::OpenPr.new(repository).execute(options)
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)
72
83
  else
73
84
  raise "Internal error - Unrecognized command #{command.inspect}"
74
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 = '2019-09-03'
13
+ s.date = '2021-07-09'
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'
@@ -8,6 +8,7 @@ module Geet
8
8
  LABEL_CREATE_COMMAND = 'label.create'
9
9
  ISSUE_LIST_COMMAND = 'issue.list'
10
10
  LABEL_LIST_COMMAND = 'label.list'
11
+ MILESTONE_CLOSE_COMMAND = 'milestone.close'
11
12
  MILESTONE_CREATE_COMMAND = 'milestone.create'
12
13
  MILESTONE_LIST_COMMAND = 'milestone.list'
13
14
  PR_COMMENT_COMMAND = 'pr.comment'
@@ -15,6 +16,8 @@ module Geet
15
16
  PR_LIST_COMMAND = 'pr.list'
16
17
  PR_MERGE_COMMAND = 'pr.merge'
17
18
  PR_OPEN_COMMAND = 'pr.open'
19
+ REPO_ADD_UPSTREAM_COMMAND = 'repo.add_upstream'
20
+ REPO_OPEN_COMMAND = 'repo.open'
18
21
  end
19
22
  end
20
23
  end
@@ -45,6 +45,10 @@ module Geet
45
45
  ['-u', '--upstream', 'List on the upstream repository'],
46
46
  ].freeze
47
47
 
48
+ MILESTONE_CLOSE_OPTIONS = [
49
+ long_help: 'Close milestones.'
50
+ ]
51
+
48
52
  MILESTONE_CREATE_OPTIONS = [
49
53
  'title',
50
54
  long_help: 'Create a milestone.'
@@ -63,7 +67,8 @@ module Geet
63
67
  PR_CREATE_OPTIONS = [
64
68
  ['-A', '--automated-mode', "Automate the branch operations (see long help)"],
65
69
  ['-n', '--no-open-pr', "Don't open the PR link in the browser after creation"],
66
- ['-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"],
67
72
  ['-l', '--labels "legacy,code review"', 'Labels'],
68
73
  ['-m', '--milestone 1.5.0', 'Milestone title pattern'],
69
74
  ['-r', '--reviewers john,tom,adrian,kevin', 'Reviewer logins'],
@@ -94,6 +99,15 @@ module Geet
94
99
  long_help: 'Open in the browser the PR for the current branch'
95
100
  ]
96
101
 
102
+ REPO_ADD_UPSTREAM_OPTIONS = [
103
+ long_help: 'Add the upstream repository to the current repository (configuration).'
104
+ ]
105
+
106
+ REPO_OPEN_OPTIONS = [
107
+ ['-u', '--upstream', 'Open the upstream repository'],
108
+ long_help: 'Open the current repository in the browser'
109
+ ]
110
+
97
111
  # Commands decoding table
98
112
 
99
113
  COMMANDS_DECODING_TABLE = {
@@ -109,6 +123,7 @@ module Geet
109
123
  'list' => LABEL_LIST_OPTIONS,
110
124
  },
111
125
  'milestone' => {
126
+ 'close' => MILESTONE_CLOSE_OPTIONS,
112
127
  'create' => MILESTONE_CREATE_OPTIONS,
113
128
  'list' => MILESTONE_LIST_OPTIONS,
114
129
  },
@@ -119,6 +134,10 @@ module Geet
119
134
  'merge' => PR_MERGE_OPTIONS,
120
135
  'open' => PR_OPEN_OPTIONS,
121
136
  },
137
+ 'repo' => {
138
+ 'add_upstream' => REPO_ADD_UPSTREAM_OPTIONS,
139
+ 'open' => REPO_OPEN_OPTIONS,
140
+ },
122
141
  }
123
142
 
124
143
  # Public interface
@@ -70,17 +70,27 @@ module Geet
70
70
  attempt_provider_call(:Milestone, :list, api_interface)
71
71
  end
72
72
 
73
- def create_pr(title, description, head, base: nil)
73
+ def close_milestone(number)
74
+ attempt_provider_call(:Milestone, :close, number, api_interface)
75
+ end
76
+
77
+ def create_pr(title, description, head, base, draft)
74
78
  confirm(LOCAL_ACTION_ON_UPSTREAM_REPOSITORY_MESSAGE) if local_action_on_upstream_repository? && @warnings
75
79
  confirm(ACTION_ON_PROTECTED_REPOSITORY_MESSAGE) if action_on_protected_repository? && @warnings
76
80
 
77
- 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)
78
82
  end
79
83
 
80
84
  def prs(owner: nil, head: nil, milestone: nil)
81
85
  attempt_provider_call(:PR, :list, api_interface, owner: owner, head: head, milestone: milestone)
82
86
  end
83
87
 
88
+ # Returns the RemoteRepository instance.
89
+ #
90
+ def remote
91
+ attempt_provider_call(:RemoteRepository, :find, api_interface)
92
+ end
93
+
84
94
  # REMOTE FUNCTIONALITIES (ACCOUNT)
85
95
 
86
96
  def authenticated_user
@@ -119,7 +129,11 @@ module Geet
119
129
  raise "The functionality invoked (#{class_name}.#{meth}) is not currently supported!"
120
130
  end
121
131
 
122
- klass.send(meth, *args)
132
+ # Can't use ruby2_keywords, because the method definitions use named keyword arguments.
133
+ #
134
+ kwargs = args.last.is_a?(Hash) ? args.pop : {}
135
+
136
+ klass.send(meth, *args, **kwargs)
123
137
  else
124
138
  raise "The class referenced (#{full_class_name}) is not currently supported!"
125
139
  end
@@ -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
  #
@@ -5,7 +5,7 @@ module Geet
5
5
  class Branch
6
6
  # See https://developer.github.com/v3/git/refs/#delete-a-reference
7
7
  #
8
- def self.delete(name, api_interface)
8
+ def self.delete(name, api_interface, **)
9
9
  api_path = "git/refs/heads/#{name}"
10
10
 
11
11
  api_interface.send_request(api_path, http_method: :delete)
@@ -6,9 +6,9 @@ module Geet
6
6
  autoload :AbstractIssue, File.expand_path('abstract_issue', __dir__)
7
7
 
8
8
  class Issue < Geet::Github::AbstractIssue
9
- def self.create(title, description, api_interface)
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
 
@@ -11,7 +11,7 @@ module Geet
11
11
  end
12
12
 
13
13
  # Returns a flat list of names in string form.
14
- def self.list(api_interface)
14
+ def self.list(api_interface, **)
15
15
  api_path = 'labels'
16
16
  response = api_interface.send_request(api_path, multipage: true)
17
17
 
@@ -24,7 +24,7 @@ module Geet
24
24
  end
25
25
 
26
26
  # See https://developer.github.com/v3/issues/labels/#create-a-label
27
- def self.create(name, color, api_interface)
27
+ def self.create(name, color, api_interface, **)
28
28
  api_path = 'labels'
29
29
  request_data = { name: name, color: color }
30
30
 
@@ -7,6 +7,8 @@ module Geet
7
7
  class Milestone
8
8
  attr_reader :number, :title, :due_on
9
9
 
10
+ STATE_CLOSED = 'closed'
11
+
10
12
  class << self
11
13
  private
12
14
 
@@ -22,7 +24,7 @@ module Geet
22
24
  end
23
25
 
24
26
  # See https://developer.github.com/v3/issues/milestones/#create-a-milestone
25
- def self.create(title, api_interface)
27
+ def self.create(title, api_interface, **)
26
28
  api_path = 'milestones'
27
29
  request_data = { title: title }
28
30
 
@@ -37,7 +39,7 @@ module Geet
37
39
 
38
40
  # See https://developer.github.com/v3/issues/milestones/#get-a-single-milestone
39
41
  #
40
- def self.find(number, api_interface)
42
+ def self.find(number, api_interface, **)
41
43
  api_path = "milestones/#{number}"
42
44
 
43
45
  response = api_interface.send_request(api_path)
@@ -51,7 +53,7 @@ module Geet
51
53
 
52
54
  # See https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository
53
55
  #
54
- def self.list(api_interface)
56
+ def self.list(api_interface, **)
55
57
  api_path = 'milestones'
56
58
 
57
59
  response = api_interface.send_request(api_path, multipage: true)
@@ -64,6 +66,17 @@ module Geet
64
66
  new(number, title, due_on, api_interface)
65
67
  end
66
68
  end
69
+
70
+ # See https://docs.github.com/en/free-pro-team@latest/rest/reference/issues#update-a-milestone
71
+ #
72
+ # This is a convenience method; the underlying operation is a generic update.
73
+ #
74
+ def self.close(number, api_interface, **)
75
+ api_path = "milestones/#{number}"
76
+ request_data = { state: STATE_CLOSED }
77
+
78
+ api_interface.send_request(api_path, data: request_data)
79
+ end
67
80
  end
68
81
  end
69
82
  end
@@ -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
 
@@ -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
@@ -45,7 +45,7 @@ module Geet
45
45
 
46
46
  # See https://developer.github.com/v3/users/#get-the-authenticated-user
47
47
  #
48
- def self.authenticated(api_interface)
48
+ def self.authenticated(api_interface, **_)
49
49
  api_path = '/user'
50
50
 
51
51
  response = api_interface.send_request(api_path)
@@ -55,7 +55,7 @@ module Geet
55
55
 
56
56
  # Returns an array of User instances
57
57
  #
58
- def self.list_collaborators(api_interface)
58
+ def self.list_collaborators(api_interface, **)
59
59
  api_path = 'collaborators'
60
60
  response = api_interface.send_request(api_path, multipage: true)
61
61
 
@@ -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
  #
@@ -11,7 +11,7 @@ module Geet
11
11
  end
12
12
 
13
13
  # Returns a flat list of names in string form.
14
- def self.list(api_interface)
14
+ def self.list(api_interface, **)
15
15
  api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/labels"
16
16
  response = api_interface.send_request(api_path, multipage: true)
17
17
 
@@ -24,7 +24,7 @@ module Geet
24
24
  end
25
25
 
26
26
  # See https://docs.gitlab.com/ee/api/labels.html#create-a-new-label
27
- def self.create(name, color, api_interface)
27
+ def self.create(name, color, api_interface, **)
28
28
  api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/labels"
29
29
  request_data = { name: name, color: "##{color}" }
30
30