geet 0.3.13 → 0.3.18

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