geet 0.3.14 → 0.4.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -0
  4. data/README.md +4 -8
  5. data/bin/geet +23 -8
  6. data/geet.gemspec +1 -1
  7. data/lib/geet/commandline/commands.rb +4 -0
  8. data/lib/geet/commandline/configuration.rb +26 -1
  9. data/lib/geet/git/repository.rb +21 -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 +29 -2
  15. data/lib/geet/github/pr.rb +1 -2
  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/comment_pr.rb +2 -2
  26. data/lib/geet/services/create_issue.rb +5 -3
  27. data/lib/geet/services/create_milestone.rb +24 -0
  28. data/lib/geet/services/create_pr.rb +10 -6
  29. data/lib/geet/services/list_issues.rb +4 -1
  30. data/lib/geet/services/merge_pr.rb +55 -4
  31. data/lib/geet/services/open_repo.rb +50 -0
  32. data/lib/geet/shared/selection.rb +3 -0
  33. data/lib/geet/utils/attributes_selection_manager.rb +12 -3
  34. data/lib/geet/utils/git_client.rb +122 -29
  35. data/lib/geet/version.rb +1 -1
  36. data/spec/integration/comment_pr_spec.rb +1 -1
  37. data/spec/integration/create_issue_spec.rb +4 -4
  38. data/spec/integration/create_label_spec.rb +5 -5
  39. data/spec/integration/create_milestone_spec.rb +34 -0
  40. data/spec/integration/create_pr_spec.rb +13 -8
  41. data/spec/integration/list_issues_spec.rb +6 -6
  42. data/spec/integration/list_labels_spec.rb +4 -4
  43. data/spec/integration/list_milestones_spec.rb +4 -4
  44. data/spec/integration/list_prs_spec.rb +3 -3
  45. data/spec/integration/merge_pr_spec.rb +33 -4
  46. data/spec/integration/open_pr_spec.rb +1 -1
  47. data/spec/integration/open_repo_spec.rb +46 -0
  48. data/spec/vcr_cassettes/create_issue.yml +1 -1
  49. data/spec/vcr_cassettes/create_issue_upstream.yml +1 -1
  50. data/spec/vcr_cassettes/github_com/create_milestone.yml +82 -0
  51. metadata +11 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a0d6f3d992faa566e45dd0c1316a2b9a0a2b6748cf49daeee209a33477f50c2
4
- data.tar.gz: '080403688353f05f693edb9ebb1edcc2fa67d8fc2fdc1d349323104df4442f4a'
3
+ metadata.gz: b2329057378f97cc59535b5821aeeca1f527bca27bd4ced6bb3250e46a55042c
4
+ data.tar.gz: 680b9175785b58deac3d5df918b2b843168ac709aea396168bd35091b6b7810b
5
5
  SHA512:
6
- metadata.gz: a80700d27dcaa3304132ec4c91e6b31ff5c81ea40573a37205f1d2da20a17e88e3ae21e4e99e6c9f5296ace0ca10c1689e395e7c6cf04b67a04a6cf5e31a4983
7
- data.tar.gz: 3daedde1e53f5ce6cabbd10bce4f729a1d0b446c8f431e619981b49c821085f497ec8c5a794f543954f525ba25e01b4c7427b362252fb6e6c5ab3a5135472c64
6
+ metadata.gz: 9dbb35ae368c401562677fa3a84138b53e1f15d4741802b478dca6cc166e65c451bf892316dcc9c32add3ca503d031a53d7964ffcc8910713fd2255f4b7c0220
7
+ data.tar.gz: ecba5a04fd823553108c83bc1e7a182d81d3a3e08d8e875584213e05f966153419fd30a4237f63126a419bcb8ce1007a1ad55e093ff24ea7d1f038ca3ad9c0cf
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
@@ -1,9 +1,17 @@
1
+ dist: bionic
1
2
  language: ruby
2
3
  rvm:
3
4
  - 2.3
4
5
  - 2.4
5
6
  - 2.5
6
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
7
15
  # API tokens are always required, but not used in testing, since no requests are actually made.
8
16
  env:
9
17
  - GITHUB_API_TOKEN=phony GITLAB_API_TOKEN=phony
data/README.md CHANGED
@@ -18,16 +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 PR
26
+ - create gist, issue, milestone, PR
31
27
 
32
28
  ## Samples
33
29
 
data/bin/geet CHANGED
@@ -28,43 +28,58 @@ 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)
56
+ when MILESTONE_CREATE_COMMAND
57
+ title = options.delete(:title)
58
+
59
+ Services::CreateMilestone.new(repository).execute(title)
49
60
  when MILESTONE_LIST_COMMAND
50
61
  Services::ListMilestones.new(repository).execute
51
62
  when PR_COMMENT_COMMAND
52
63
  comment = options.delete(:comment)
53
64
 
54
- Services::CommentPr.new(repository).execute(comment)
65
+ Services::CommentPr.new(repository).execute(comment, **options)
55
66
  when PR_CREATE_COMMAND
56
67
  summary = options[:summary] || edit_pr_summary(base: options[:base])
57
68
  title, description = split_summary(summary)
58
69
 
59
70
  options = default_to_manual_selection(options, :labels, :milestone, :reviewers)
60
71
 
61
- Services::CreatePr.new(repository).execute(title, description, options)
72
+ Services::CreatePr.new(repository).execute(title, description, **options)
62
73
  when PR_LIST_COMMAND
63
74
  Services::ListPrs.new(repository).execute
64
75
  when PR_MERGE_COMMAND
65
- Services::MergePr.new(repository).execute(options)
76
+ Services::MergePr.new(repository).execute(**options)
66
77
  when PR_OPEN_COMMAND
67
- 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)
68
83
  else
69
84
  raise "Internal error - Unrecognized command #{command.inspect}"
70
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-08-28'
13
+ s.date = '2021-06-08'
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,12 +8,16 @@ 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'
12
+ MILESTONE_CREATE_COMMAND = 'milestone.create'
11
13
  MILESTONE_LIST_COMMAND = 'milestone.list'
12
14
  PR_COMMENT_COMMAND = 'pr.comment'
13
15
  PR_CREATE_COMMAND = 'pr.create'
14
16
  PR_LIST_COMMAND = 'pr.list'
15
17
  PR_MERGE_COMMAND = 'pr.merge'
16
18
  PR_OPEN_COMMAND = 'pr.open'
19
+ REPO_ADD_UPSTREAM_COMMAND = 'repo.add_upstream'
20
+ REPO_OPEN_COMMAND = 'repo.open'
17
21
  end
18
22
  end
19
23
  end
@@ -45,11 +45,21 @@ 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
+
52
+ MILESTONE_CREATE_OPTIONS = [
53
+ 'title',
54
+ long_help: 'Create a milestone.'
55
+ ]
56
+
48
57
  MILESTONE_LIST_OPTIONS = [
49
58
  ['-u', '--upstream', 'List on the upstream repository'],
50
59
  ].freeze
51
60
 
52
61
  PR_COMMENT_OPTIONS = [
62
+ ['-n', '--no-open-pr', "Don't open the PR link in the browser after creation"],
53
63
  'comment',
54
64
  long_help: 'Add a comment to the PR for the current branch.'
55
65
  ]
@@ -57,7 +67,7 @@ module Geet
57
67
  PR_CREATE_OPTIONS = [
58
68
  ['-A', '--automated-mode', "Automate the branch operations (see long help)"],
59
69
  ['-n', '--no-open-pr', "Don't open the PR link in the browser after creation"],
60
- ['-b', '--base develop', "Specify the base branch; defaults to `master`"],
70
+ ['-b', '--base develop', "Specify the base branch; defaults to the main branch"],
61
71
  ['-l', '--labels "legacy,code review"', 'Labels'],
62
72
  ['-m', '--milestone 1.5.0', 'Milestone title pattern'],
63
73
  ['-r', '--reviewers john,tom,adrian,kevin', 'Reviewer logins'],
@@ -88,6 +98,15 @@ module Geet
88
98
  long_help: 'Open in the browser the PR for the current branch'
89
99
  ]
90
100
 
101
+ REPO_ADD_UPSTREAM_OPTIONS = [
102
+ long_help: 'Add the upstream repository to the current repository (configuration).'
103
+ ]
104
+
105
+ REPO_OPEN_OPTIONS = [
106
+ ['-u', '--upstream', 'Open the upstream repository'],
107
+ long_help: 'Open the current repository in the browser'
108
+ ]
109
+
91
110
  # Commands decoding table
92
111
 
93
112
  COMMANDS_DECODING_TABLE = {
@@ -103,6 +122,8 @@ module Geet
103
122
  'list' => LABEL_LIST_OPTIONS,
104
123
  },
105
124
  'milestone' => {
125
+ 'close' => MILESTONE_CLOSE_OPTIONS,
126
+ 'create' => MILESTONE_CREATE_OPTIONS,
106
127
  'list' => MILESTONE_LIST_OPTIONS,
107
128
  },
108
129
  'pr' => {
@@ -112,6 +133,10 @@ module Geet
112
133
  'merge' => PR_MERGE_OPTIONS,
113
134
  'open' => PR_OPEN_OPTIONS,
114
135
  },
136
+ 'repo' => {
137
+ 'add_upstream' => REPO_ADD_UPSTREAM_OPTIONS,
138
+ 'open' => REPO_OPEN_OPTIONS,
139
+ },
115
140
  }
116
141
 
117
142
  # Public interface
@@ -58,6 +58,10 @@ module Geet
58
58
  attempt_provider_call(:Issue, :list, api_interface, assignee: assignee, milestone: milestone)
59
59
  end
60
60
 
61
+ def create_milestone(title)
62
+ attempt_provider_call(:Milestone, :create, title, api_interface)
63
+ end
64
+
61
65
  def milestone(number)
62
66
  attempt_provider_call(:Milestone, :find, number, api_interface)
63
67
  end
@@ -66,17 +70,27 @@ module Geet
66
70
  attempt_provider_call(:Milestone, :list, api_interface)
67
71
  end
68
72
 
69
- 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)
70
78
  confirm(LOCAL_ACTION_ON_UPSTREAM_REPOSITORY_MESSAGE) if local_action_on_upstream_repository? && @warnings
71
79
  confirm(ACTION_ON_PROTECTED_REPOSITORY_MESSAGE) if action_on_protected_repository? && @warnings
72
80
 
73
- attempt_provider_call(:PR, :create, title, description, head, api_interface, base: base)
81
+ attempt_provider_call(:PR, :create, title, description, head, api_interface, base)
74
82
  end
75
83
 
76
84
  def prs(owner: nil, head: nil, milestone: nil)
77
85
  attempt_provider_call(:PR, :list, api_interface, owner: owner, head: head, milestone: milestone)
78
86
  end
79
87
 
88
+ # Returns the RemoteRepository instance.
89
+ #
90
+ def remote
91
+ attempt_provider_call(:RemoteRepository, :find, api_interface)
92
+ end
93
+
80
94
  # REMOTE FUNCTIONALITIES (ACCOUNT)
81
95
 
82
96
  def authenticated_user
@@ -115,7 +129,11 @@ module Geet
115
129
  raise "The functionality invoked (#{class_name}.#{meth}) is not currently supported!"
116
130
  end
117
131
 
118
- 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)
119
137
  else
120
138
  raise "The class referenced (#{full_class_name}) is not currently supported!"
121
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
 
@@ -21,9 +23,23 @@ module Geet
21
23
  @api_interface = api_interface
22
24
  end
23
25
 
26
+ # See https://developer.github.com/v3/issues/milestones/#create-a-milestone
27
+ def self.create(title, api_interface, **)
28
+ api_path = 'milestones'
29
+ request_data = { title: title }
30
+
31
+ response = api_interface.send_request(api_path, data: request_data)
32
+
33
+ number = response.fetch('number')
34
+ title = response.fetch('title')
35
+ due_on = nil
36
+
37
+ new(number, title, due_on, api_interface)
38
+ end
39
+
24
40
  # See https://developer.github.com/v3/issues/milestones/#get-a-single-milestone
25
41
  #
26
- def self.find(number, api_interface)
42
+ def self.find(number, api_interface, **)
27
43
  api_path = "milestones/#{number}"
28
44
 
29
45
  response = api_interface.send_request(api_path)
@@ -37,7 +53,7 @@ module Geet
37
53
 
38
54
  # See https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository
39
55
  #
40
- def self.list(api_interface)
56
+ def self.list(api_interface, **)
41
57
  api_path = 'milestones'
42
58
 
43
59
  response = api_interface.send_request(api_path, multipage: true)
@@ -50,6 +66,17 @@ module Geet
50
66
  new(number, title, due_on, api_interface)
51
67
  end
52
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
53
80
  end
54
81
  end
55
82
  end
@@ -8,9 +8,8 @@ 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)
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
@@ -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