geet 0.3.13 → 0.3.18

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -0
  4. data/README.md +5 -8
  5. data/bin/geet +26 -7
  6. data/extra/anonymize_vcr_data +58 -0
  7. data/geet.gemspec +1 -1
  8. data/lib/geet/commandline/commands.rb +5 -0
  9. data/lib/geet/commandline/configuration.rb +31 -0
  10. data/lib/geet/git/repository.rb +19 -1
  11. data/lib/geet/github/abstract_issue.rb +9 -0
  12. data/lib/geet/github/api_interface.rb +2 -0
  13. data/lib/geet/github/branch.rb +1 -1
  14. data/lib/geet/github/issue.rb +1 -1
  15. data/lib/geet/github/label.rb +2 -2
  16. data/lib/geet/github/milestone.rb +29 -2
  17. data/lib/geet/github/remote_repository.rb +37 -0
  18. data/lib/geet/github/user.rb +2 -2
  19. data/lib/geet/gitlab/api_interface.rb +2 -0
  20. data/lib/geet/gitlab/label.rb +2 -2
  21. data/lib/geet/gitlab/milestone.rb +1 -1
  22. data/lib/geet/gitlab/user.rb +1 -1
  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 +31 -0
  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 +5 -3
  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/branches.rb +9 -0
  33. data/lib/geet/shared/selection.rb +3 -0
  34. data/lib/geet/utils/attributes_selection_manager.rb +12 -3
  35. data/lib/geet/utils/git_client.rb +101 -28
  36. data/lib/geet/version.rb +1 -1
  37. data/spec/integration/comment_pr_spec.rb +44 -0
  38. data/spec/integration/create_issue_spec.rb +4 -4
  39. data/spec/integration/create_label_spec.rb +5 -5
  40. data/spec/integration/create_milestone_spec.rb +34 -0
  41. data/spec/integration/create_pr_spec.rb +8 -8
  42. data/spec/integration/list_issues_spec.rb +6 -6
  43. data/spec/integration/list_labels_spec.rb +4 -4
  44. data/spec/integration/list_milestones_spec.rb +4 -4
  45. data/spec/integration/list_prs_spec.rb +3 -3
  46. data/spec/integration/merge_pr_spec.rb +29 -4
  47. data/spec/integration/open_pr_spec.rb +1 -1
  48. data/spec/integration/open_repo_spec.rb +46 -0
  49. data/spec/vcr_cassettes/create_gist_private.yml +1 -1
  50. data/spec/vcr_cassettes/create_gist_public.yml +1 -1
  51. data/spec/vcr_cassettes/create_issue.yml +13 -13
  52. data/spec/vcr_cassettes/create_issue_upstream.yml +2 -2
  53. data/spec/vcr_cassettes/github_com/comment_pr.yml +161 -0
  54. data/spec/vcr_cassettes/github_com/create_label.yml +1 -1
  55. data/spec/vcr_cassettes/github_com/create_label_upstream.yml +1 -1
  56. data/spec/vcr_cassettes/github_com/create_label_with_random_color.yml +1 -1
  57. data/spec/vcr_cassettes/github_com/create_milestone.yml +82 -0
  58. data/spec/vcr_cassettes/github_com/create_pr.yml +16 -16
  59. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_create_upstream.yml +7 -7
  60. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_with_push.yml +7 -7
  61. data/spec/vcr_cassettes/github_com/create_pr_upstream.yml +8 -8
  62. data/spec/vcr_cassettes/github_com/create_pr_upstream_without_write_permissions.yml +3 -3
  63. data/spec/vcr_cassettes/github_com/list_issues.yml +5 -5
  64. data/spec/vcr_cassettes/github_com/list_issues_upstream.yml +6 -6
  65. data/spec/vcr_cassettes/github_com/list_issues_with_assignee.yml +4 -4
  66. data/spec/vcr_cassettes/github_com/list_labels.yml +1 -1
  67. data/spec/vcr_cassettes/github_com/list_labels_upstream.yml +1 -1
  68. data/spec/vcr_cassettes/github_com/list_milestones.yml +50 -50
  69. data/spec/vcr_cassettes/github_com/merge_pr.yml +2 -2
  70. data/spec/vcr_cassettes/github_com/merge_pr_with_branch_deletion.yml +2 -2
  71. data/spec/vcr_cassettes/github_com/open_pr.yml +2 -2
  72. data/spec/vcr_cassettes/gitlab_com/create_label.yml +1 -1
  73. data/spec/vcr_cassettes/gitlab_com/list_issues.yml +4 -4
  74. data/spec/vcr_cassettes/gitlab_com/list_issues_with_assignee.yml +8 -8
  75. data/spec/vcr_cassettes/gitlab_com/list_labels.yml +1 -1
  76. data/spec/vcr_cassettes/gitlab_com/list_milestones.yml +9 -9
  77. data/spec/vcr_cassettes/gitlab_com/merge_pr.yml +5 -5
  78. data/spec/vcr_cassettes/list_milestones_upstream.yml +21 -21
  79. data/spec/vcr_cassettes/list_prs.yml +10 -10
  80. data/spec/vcr_cassettes/list_prs_upstream.yml +10 -10
  81. metadata +16 -4
@@ -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
 
@@ -17,7 +17,7 @@ module Geet
17
17
 
18
18
  # See https://docs.gitlab.com/ee/api/milestones.html#list-project-milestones
19
19
  #
20
- def self.list(api_interface)
20
+ def self.list(api_interface, **)
21
21
  api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/milestones"
22
22
 
23
23
  response = api_interface.send_request(api_path, multipage: true)
@@ -13,7 +13,7 @@ module Geet
13
13
 
14
14
  # Returns an array of User instances
15
15
  #
16
- def self.list_collaborators(api_interface)
16
+ def self.list_collaborators(api_interface, **)
17
17
  api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/members"
18
18
 
19
19
  response = api_interface.send_request(api_path, multipage: true)
@@ -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
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../shared/selection'
4
+
5
+ module Geet
6
+ module Services
7
+ class CloseMilestones
8
+ include Geet::Shared::Selection
9
+
10
+ def initialize(repository, out: $stdout)
11
+ @repository = repository
12
+ @out = out
13
+ end
14
+
15
+ def execute(numbers: nil)
16
+ numbers = find_and_select_milestone_numbers(numbers)
17
+
18
+ close_milestone_threads = close_milestones(numbers)
19
+
20
+ close_milestone_threads.each(&:join)
21
+ end
22
+
23
+ private
24
+
25
+ def find_and_select_milestone_numbers(numbers)
26
+ selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
27
+
28
+ selection_manager.add_attribute(:milestones, 'milestone', numbers, SELECTION_MULTIPLE, name_method: :title)
29
+
30
+ milestones = selection_manager.select_attributes[0]
31
+
32
+ milestones.map(&:number)
33
+ end
34
+
35
+ def close_milestones(numbers)
36
+ @out.puts "Closing milestones #{numbers.join(', ')}..."
37
+
38
+ numbers.map do |number|
39
+ Thread.new do
40
+ @repository.close_milestone(number)
41
+ end
42
+ end
43
+ end
44
+ end # CloseMilestones
45
+ end # Services
46
+ end # Geet
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/os_helper'
4
+ require_relative '../helpers/services_workflow_helper'
5
+
6
+ module Geet
7
+ module Services
8
+ # Add a comment to the PR for the current branch.
9
+ #
10
+ class CommentPr
11
+ include Geet::Helpers::OsHelper
12
+ include Geet::Helpers::ServicesWorkflowHelper
13
+
14
+ DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
15
+
16
+ def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
17
+ @repository = repository
18
+ @out = out
19
+ @git_client = git_client
20
+ end
21
+
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)
25
+ pr.comment(comment)
26
+ open_file_with_default_application(pr.link) unless no_open_pr
27
+ pr
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative 'abstract_create_issue'
4
4
  require_relative '../shared/repo_permissions'
5
+ require_relative '../shared/selection'
5
6
 
6
7
  module Geet
7
8
  module Services
8
9
  class CreateIssue < AbstractCreateIssue
9
10
  include Geet::Shared::RepoPermissions
11
+ include Geet::Shared::Selection
10
12
 
11
13
  # options:
12
14
  # :labels
@@ -56,9 +58,9 @@ module Geet
56
58
  def find_and_select_attributes(labels, milestone, assignees)
57
59
  selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
58
60
 
59
- selection_manager.add_attribute(:labels, 'label', labels, :multiple, name_method: :name) if labels
60
- selection_manager.add_attribute(:milestones, 'milestone', milestone, :single, name_method: :title) if milestone
61
- selection_manager.add_attribute(:collaborators, 'assignee', assignees, :multiple, name_method: :username) if assignees
61
+ selection_manager.add_attribute(:labels, 'label', labels, SELECTION_MULTIPLE, name_method: :name) if labels
62
+ selection_manager.add_attribute(:milestones, 'milestone', milestone, SELECTION_SINGLE, name_method: :title) if milestone
63
+ selection_manager.add_attribute(:collaborators, 'assignee', assignees, SELECTION_MULTIPLE, name_method: :username) if assignees
62
64
 
63
65
  selection_manager.select_attributes
64
66
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module Services
5
+ class CreateMilestone
6
+ def initialize(repository, out: $stdout)
7
+ @repository = repository
8
+ @out = out
9
+ end
10
+
11
+ def execute(title)
12
+ create_milestone(title)
13
+ end
14
+
15
+ private
16
+
17
+ def create_milestone(title)
18
+ @out.puts 'Creating milestone...'
19
+
20
+ @repository.create_milestone(title)
21
+ end
22
+ end # class CreateMilestone
23
+ end # module Services
24
+ end # module Geet
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative 'abstract_create_issue'
4
4
  require_relative '../shared/repo_permissions'
5
+ require_relative '../shared/selection'
5
6
 
6
7
  module Geet
7
8
  module Services
8
9
  class CreatePr < AbstractCreateIssue
9
10
  include Geet::Shared::RepoPermissions
11
+ include Geet::Shared::Selection
10
12
 
11
13
  DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
12
14
 
@@ -66,11 +68,11 @@ module Geet
66
68
  def find_and_select_attributes(labels, milestone, reviewers)
67
69
  selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
68
70
 
69
- selection_manager.add_attribute(:labels, 'label', labels, :multiple, name_method: :name) if labels
70
- selection_manager.add_attribute(:milestones, 'milestone', milestone, :single, name_method: :title) if milestone
71
+ selection_manager.add_attribute(:labels, 'label', labels, SELECTION_MULTIPLE, name_method: :name) if labels
72
+ selection_manager.add_attribute(:milestones, 'milestone', milestone, SELECTION_SINGLE, name_method: :title) if milestone
71
73
 
72
74
  if reviewers
73
- selection_manager.add_attribute(:collaborators, 'reviewer', reviewers, :multiple, name_method: :username) do |all_reviewers|
75
+ selection_manager.add_attribute(:collaborators, 'reviewer', reviewers, SELECTION_MULTIPLE, name_method: :username) do |all_reviewers|
74
76
  authenticated_user = @repository.authenticated_user
75
77
  all_reviewers.delete_if { |reviewer| reviewer.username == authenticated_user.username }
76
78
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../utils/attributes_selection_manager'
4
+ require_relative '../shared/selection'
4
5
 
5
6
  module Geet
6
7
  module Services
7
8
  class ListIssues
9
+ include Geet::Shared::Selection
10
+
8
11
  def initialize(repository, out: $stdout)
9
12
  @repository = repository
10
13
  @out = out
@@ -25,7 +28,7 @@ module Geet
25
28
  def find_and_select_attributes(assignee)
26
29
  selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
27
30
 
28
- selection_manager.add_attribute(:collaborators, 'assignee', assignee, :single, name_method: :username)
31
+ selection_manager.add_attribute(:collaborators, 'assignee', assignee, SELECTION_SINGLE, name_method: :username)
29
32
 
30
33
  selection_manager.select_attributes[0]
31
34
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helpers/services_workflow_helper'
4
+ require_relative '../shared/branches'
4
5
 
5
6
  module Geet
6
7
  module Services
@@ -12,6 +13,7 @@ module Geet
12
13
  #
13
14
  class MergePr
14
15
  include Geet::Helpers::ServicesWorkflowHelper
16
+ include Geet::Shared::Branches
15
17
 
16
18
  DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
17
19
 
@@ -24,8 +26,29 @@ module Geet
24
26
  def execute(delete_branch: false)
25
27
  merge_owner, merge_head = find_merge_head
26
28
  pr = checked_find_branch_pr(merge_owner, merge_head)
29
+
27
30
  merge_pr(pr)
28
- do_delete_branch if delete_branch
31
+
32
+ if delete_branch
33
+ branch = @git_client.current_branch
34
+
35
+ delete_remote_branch(branch)
36
+ end
37
+
38
+ fetch_repository
39
+
40
+ if upstream_branch_gone?
41
+ pr_branch = @git_client.current_branch
42
+
43
+ # The rebase could also be placed after the branch deletion. There are pros/cons;
44
+ # currently, it's not important.
45
+ #
46
+ checkout_branch(MAIN_BRANCH)
47
+ rebase
48
+
49
+ delete_local_branch(pr_branch)
50
+ end
51
+
29
52
  pr
30
53
  end
31
54
 
@@ -37,10 +60,38 @@ module Geet
37
60
  pr.merge
38
61
  end
39
62
 
40
- def do_delete_branch
41
- @out.puts "Deleting branch #{@git_client.current_branch}..."
63
+ def delete_remote_branch(branch)
64
+ @out.puts "Deleting remote branch #{branch}..."
65
+
66
+ @repository.delete_branch(branch)
67
+ end
68
+
69
+ def fetch_repository
70
+ @out.puts "Fetching repository..."
71
+
72
+ @git_client.fetch
73
+ end
74
+
75
+ def upstream_branch_gone?
76
+ @git_client.upstream_branch_gone?
77
+ end
78
+
79
+ def checkout_branch(branch)
80
+ @out.puts "Checking out #{branch}..."
81
+
82
+ @git_client.checkout(branch)
83
+ end
84
+
85
+ def rebase
86
+ @out.puts "Rebasing..."
87
+
88
+ @git_client.rebase
89
+ end
90
+
91
+ def delete_local_branch(branch)
92
+ @out.puts "Deleting local branch #{branch}..."
42
93
 
43
- @repository.delete_branch(@git_client.current_branch)
94
+ @git_client.delete_branch(branch)
44
95
  end
45
96
  end
46
97
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/os_helper'
4
+
5
+ module Geet
6
+ module Services
7
+ # Open in the browser the current repository.
8
+ #
9
+ class OpenRepo
10
+ include Helpers::OsHelper
11
+
12
+ DEFAULT_GIT_CLIENT = Utils::GitClient.new
13
+
14
+ def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
15
+ @repository = repository
16
+ @out = out
17
+ @git_client = git_client
18
+ end
19
+
20
+ def execute(upstream: false)
21
+ remote_options = upstream ? {name: Utils::GitClient::UPSTREAM_NAME} : {}
22
+
23
+ repo_url = @git_client.remote(**remote_options)
24
+ repo_url = convert_repo_url_to_http_protocol(repo_url)
25
+
26
+ open_file_with_default_application(repo_url)
27
+
28
+ repo_url
29
+ end
30
+
31
+ private
32
+
33
+ # The repository URL may be in any of the git/http protocols.
34
+ #
35
+ def convert_repo_url_to_http_protocol(repo_url)
36
+ case repo_url
37
+ when /https:/
38
+ when /git@/
39
+ else
40
+ # Minimal error, due to match guaranteed by GitClient#remote.
41
+ raise
42
+ end
43
+
44
+ domain, _, path = repo_url.match(Utils::GitClient::REMOTE_URL_REGEX)[2..4]
45
+
46
+ "https://#{domain}/#{path}"
47
+ end
48
+ end
49
+ end
50
+ end