geet 0.3.11 → 0.3.16

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.byebug_history +5 -0
  3. data/.travis.yml +7 -0
  4. data/README.md +2 -0
  5. data/bin/geet +17 -0
  6. data/extra/anonymize_vcr_data +58 -0
  7. data/geet.gemspec +1 -1
  8. data/lib/geet/commandline/commands.rb +4 -0
  9. data/lib/geet/commandline/configuration.rb +23 -0
  10. data/lib/geet/git/repository.rb +10 -2
  11. data/lib/geet/github/abstract_issue.rb +9 -0
  12. data/lib/geet/github/milestone.rb +27 -0
  13. data/lib/geet/github/pr.rb +2 -2
  14. data/lib/geet/gitlab/pr.rb +16 -1
  15. data/lib/geet/helpers/services_workflow_helper.rb +33 -0
  16. data/lib/geet/services/close_milestones.rb +46 -0
  17. data/lib/geet/services/comment_pr.rb +31 -0
  18. data/lib/geet/services/create_issue.rb +5 -3
  19. data/lib/geet/services/create_milestone.rb +24 -0
  20. data/lib/geet/services/create_pr.rb +5 -3
  21. data/lib/geet/services/list_issues.rb +4 -1
  22. data/lib/geet/services/merge_pr.rb +57 -17
  23. data/lib/geet/services/open_pr.rb +30 -0
  24. data/lib/geet/shared/branches.rb +9 -0
  25. data/lib/geet/shared/selection.rb +3 -0
  26. data/lib/geet/utils/attributes_selection_manager.rb +5 -3
  27. data/lib/geet/utils/git_client.rb +65 -11
  28. data/lib/geet/version.rb +1 -1
  29. data/spec/integration/comment_pr_spec.rb +44 -0
  30. data/spec/integration/create_milestone_spec.rb +34 -0
  31. data/spec/integration/merge_pr_spec.rb +47 -16
  32. data/spec/integration/open_pr_spec.rb +44 -0
  33. data/spec/vcr_cassettes/create_gist_private.yml +1 -1
  34. data/spec/vcr_cassettes/create_gist_public.yml +1 -1
  35. data/spec/vcr_cassettes/create_issue.yml +13 -13
  36. data/spec/vcr_cassettes/create_issue_upstream.yml +2 -2
  37. data/spec/vcr_cassettes/github_com/comment_pr.yml +161 -0
  38. data/spec/vcr_cassettes/github_com/create_label.yml +1 -1
  39. data/spec/vcr_cassettes/github_com/create_label_upstream.yml +1 -1
  40. data/spec/vcr_cassettes/github_com/create_label_with_random_color.yml +1 -1
  41. data/spec/vcr_cassettes/github_com/create_milestone.yml +82 -0
  42. data/spec/vcr_cassettes/github_com/create_pr.yml +16 -16
  43. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_create_upstream.yml +7 -7
  44. data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_with_push.yml +7 -7
  45. data/spec/vcr_cassettes/github_com/create_pr_upstream.yml +8 -8
  46. data/spec/vcr_cassettes/github_com/create_pr_upstream_without_write_permissions.yml +3 -3
  47. data/spec/vcr_cassettes/github_com/list_issues.yml +5 -5
  48. data/spec/vcr_cassettes/github_com/list_issues_upstream.yml +6 -6
  49. data/spec/vcr_cassettes/github_com/list_issues_with_assignee.yml +4 -4
  50. data/spec/vcr_cassettes/github_com/list_labels.yml +1 -1
  51. data/spec/vcr_cassettes/github_com/list_labels_upstream.yml +1 -1
  52. data/spec/vcr_cassettes/github_com/list_milestones.yml +50 -50
  53. data/spec/vcr_cassettes/github_com/merge_pr.yml +35 -34
  54. data/spec/vcr_cassettes/github_com/merge_pr_with_branch_deletion.yml +48 -45
  55. data/spec/vcr_cassettes/github_com/open_pr.yml +81 -0
  56. data/spec/vcr_cassettes/gitlab_com/create_label.yml +1 -1
  57. data/spec/vcr_cassettes/gitlab_com/list_issues.yml +4 -4
  58. data/spec/vcr_cassettes/gitlab_com/list_issues_with_assignee.yml +8 -8
  59. data/spec/vcr_cassettes/gitlab_com/list_labels.yml +1 -1
  60. data/spec/vcr_cassettes/gitlab_com/list_milestones.yml +9 -9
  61. data/spec/vcr_cassettes/gitlab_com/merge_pr.yml +24 -24
  62. data/spec/vcr_cassettes/list_milestones_upstream.yml +21 -21
  63. data/spec/vcr_cassettes/list_prs.yml +10 -10
  64. data/spec/vcr_cassettes/list_prs_upstream.yml +10 -10
  65. metadata +17 -4
@@ -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,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../helpers/services_workflow_helper'
4
+ require_relative '../shared/branches'
5
+
3
6
  module Geet
4
7
  module Services
5
8
  # Merges the PR for the current branch.
@@ -9,6 +12,9 @@ module Geet
9
12
  # constraints, but speeds up the workflow.
10
13
  #
11
14
  class MergePr
15
+ include Geet::Helpers::ServicesWorkflowHelper
16
+ include Geet::Shared::Branches
17
+
12
18
  DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
13
19
 
14
20
  def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
@@ -18,40 +24,74 @@ module Geet
18
24
  end
19
25
 
20
26
  def execute(delete_branch: false)
21
- merge_head = find_merge_head
22
- pr = checked_find_branch_pr(merge_head)
27
+ merge_owner, merge_head = find_merge_head
28
+ pr = checked_find_branch_pr(merge_owner, merge_head)
29
+
23
30
  merge_pr(pr)
24
- 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
+
25
52
  pr
26
53
  end
27
54
 
28
55
  private
29
56
 
30
- def find_merge_head
31
- @git_client.current_branch
57
+ def merge_pr(pr)
58
+ @out.puts "Merging PR ##{pr.number}..."
59
+
60
+ pr.merge
32
61
  end
33
62
 
34
- # Expect to find only one.
35
- def checked_find_branch_pr(head)
36
- @out.puts "Finding PR with head (#{head})..."
63
+ def delete_remote_branch(branch)
64
+ @out.puts "Deleting remote branch #{branch}..."
37
65
 
38
- prs = @repository.prs(head: head)
66
+ @repository.delete_branch(branch)
67
+ end
39
68
 
40
- raise "Expected to find only one PR for the current branch; found: #{prs.size}" if prs.size != 1
69
+ def fetch_repository
70
+ @out.puts "Fetching repository..."
41
71
 
42
- prs[0]
72
+ @git_client.fetch
43
73
  end
44
74
 
45
- def merge_pr(pr)
46
- @out.puts "Merging PR ##{pr.number}..."
75
+ def upstream_branch_gone?
76
+ @git_client.upstream_branch_gone?
77
+ end
47
78
 
48
- pr.merge
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
49
89
  end
50
90
 
51
- def do_delete_branch
52
- @out.puts "Deleting branch #{@git_client.current_branch}..."
91
+ def delete_local_branch(branch)
92
+ @out.puts "Deleting local branch #{branch}..."
53
93
 
54
- @repository.delete_branch(@git_client.current_branch)
94
+ @git_client.delete_branch(branch)
55
95
  end
56
96
  end
57
97
  end
@@ -0,0 +1,30 @@
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
+ # Open in the browser the PR for the current branch.
9
+ #
10
+ class OpenPr
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(delete_branch: false)
23
+ merge_owner, merge_head = find_merge_head
24
+ pr = checked_find_branch_pr(merge_owner, merge_head)
25
+ open_file_with_default_application(pr.link)
26
+ pr
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module Shared
5
+ module Branches
6
+ MAIN_BRANCH = 'master'
7
+ end
8
+ end
9
+ end
@@ -4,6 +4,9 @@ module Geet
4
4
  module Shared
5
5
  module Selection
6
6
  MANUAL_LIST_SELECTION_FLAG = '-'.freeze
7
+
8
+ SELECTION_SINGLE = :single
9
+ SELECTION_MULTIPLE = :multiple
7
10
  end
8
11
  end
9
12
  end
@@ -30,8 +30,10 @@ module Geet
30
30
  @selections_data = []
31
31
  end
32
32
 
33
+ # selection_type: SELECTION_SINGLE or SELECTION_MULTIPLE
34
+ #
33
35
  def add_attribute(repository_call, description, pattern, selection_type, name_method: nil, &pre_selection_hook)
34
- raise "Unrecognized selection type #{selection_type.inspect}" if ![:single, :multiple].include?(selection_type)
36
+ raise "Unrecognized selection type #{selection_type.inspect}" if ![SELECTION_SINGLE, SELECTION_MULTIPLE].include?(selection_type)
35
37
 
36
38
  finder_thread = find_attribute_entries(repository_call)
37
39
 
@@ -47,9 +49,9 @@ module Geet
47
49
  entries = pre_selection_hook.(entries) if pre_selection_hook
48
50
 
49
51
  case selection_type
50
- when :single
52
+ when SELECTION_SINGLE
51
53
  select_entry(description, entries, pattern, name_method)
52
- when :multiple
54
+ when SELECTION_MULTIPLE
53
55
  select_entries(description, entries, pattern, name_method)
54
56
  end
55
57
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  require 'shellwords'
4
5
  require_relative '../helpers/os_helper'
5
6
 
@@ -38,13 +39,13 @@ module Geet
38
39
  # (which start with `-`)
39
40
  #
40
41
  def cherry(limit)
41
- raw_commits = execute_command("git #{gitdir_option} cherry #{limit.shellescape}")
42
+ raw_commits = execute_git_command("cherry #{limit.shellescape}")
42
43
 
43
44
  raw_commits.split("\n").grep(/^\+/).map { |line| line[3..-1] }
44
45
  end
45
46
 
46
47
  def current_branch
47
- branch = execute_command("git #{gitdir_option} rev-parse --abbrev-ref HEAD")
48
+ branch = execute_git_command("rev-parse --abbrev-ref HEAD")
48
49
 
49
50
  raise "Couldn't find current branch" if branch == 'HEAD'
50
51
 
@@ -53,12 +54,14 @@ module Geet
53
54
 
54
55
  # Not to be confused with `upstream` repository!
55
56
  #
57
+ # This API doesn't reveal if the remote branch is gone.
58
+ #
56
59
  # return: nil, if the upstream branch is not configured.
57
60
  #
58
61
  def upstream_branch
59
- head_symbolic_ref = execute_command("git #{gitdir_option} symbolic-ref -q HEAD")
62
+ head_symbolic_ref = execute_git_command("symbolic-ref -q HEAD")
60
63
 
61
- raw_upstream_branch = execute_command("git #{gitdir_option} for-each-ref --format='%(upstream:short)' #{head_symbolic_ref.shellescape}").strip
64
+ raw_upstream_branch = execute_git_command("for-each-ref --format='%(upstream:short)' #{head_symbolic_ref.shellescape}").strip
62
65
 
63
66
  if raw_upstream_branch != ''
64
67
  raw_upstream_branch[UPSTREAM_BRANCH_REGEX, 1] || raise("Unexpected upstream format: #{raw_upstream_branch}")
@@ -67,8 +70,30 @@ module Geet
67
70
  end
68
71
  end
69
72
 
73
+ # TODO: May be merged with :upstream_branch, although it would require designing how a gone
74
+ # remote branch is expressed.
75
+ #
76
+ # Sample command output:
77
+ #
78
+ # ## add_milestone_closing...origin/add_milestone_closing [gone]
79
+ # M spec/integration/merge_pr_spec.rb
80
+ #
81
+ def upstream_branch_gone?
82
+ git_command = "status -b --porcelain"
83
+ status_output = execute_git_command(git_command)
84
+
85
+ # Simplified branch naming pattern. The exact one (see https://stackoverflow.com/a/3651867)
86
+ # is not worth implementing.
87
+ #
88
+ if status_output =~ %r(^## .+\.\.\..+?( \[gone\])?$)
89
+ !!$LAST_MATCH_INFO[1]
90
+ else
91
+ raise "Unexpected git command #{git_command.inspect} output: #{status_output}"
92
+ end
93
+ end
94
+
70
95
  def working_tree_clean?
71
- git_message = execute_command("git #{gitdir_option} status")
96
+ git_message = execute_git_command("status")
72
97
 
73
98
  !!(git_message =~ CLEAN_TREE_MESSAGE_REGEX)
74
99
  end
@@ -80,7 +105,7 @@ module Geet
80
105
  # Show the description ("<subject>\n\n<body>") for the given git object.
81
106
  #
82
107
  def show_description(object)
83
- execute_command("git #{gitdir_option} show --quiet --format='%s\n\n%b' #{object.shellescape}")
108
+ execute_git_command("show --quiet --format='%s\n\n%b' #{object.shellescape}")
84
109
  end
85
110
 
86
111
  ##########################################################################
@@ -95,6 +120,10 @@ module Geet
95
120
  remote(remote_name)[REMOTE_ORIGIN_REGEX, 3]
96
121
  end
97
122
 
123
+ def owner
124
+ path.split('/')[0]
125
+ end
126
+
98
127
  def provider_domain
99
128
  # We assume that it's not possible to have origin and upstream on different providers.
100
129
  #
@@ -113,7 +142,7 @@ module Geet
113
142
  # The result is in the format `git@github.com:donaldduck/geet.git`
114
143
  #
115
144
  def remote(name)
116
- remote_url = execute_command("git #{gitdir_option} ls-remote --get-url #{name}")
145
+ remote_url = execute_git_command("ls-remote --get-url #{name}")
117
146
 
118
147
  if remote_url == name
119
148
  raise "Remote #{name.inspect} not found!"
@@ -128,7 +157,7 @@ module Geet
128
157
  # purposes, any any action that needs to work with the remote, uses #remote.
129
158
  #
130
159
  def remote_defined?(name)
131
- remote_url = execute_command("git #{gitdir_option} ls-remote --get-url #{name}")
160
+ remote_url = execute_git_command("ls-remote --get-url #{name}")
132
161
 
133
162
  # If the remote is not define, `git ls-remote` will return the passed value.
134
163
  remote_url != name
@@ -138,12 +167,32 @@ module Geet
138
167
  # OPERATION APIS
139
168
  ##########################################################################
140
169
 
170
+ def checkout(branch)
171
+ execute_git_command("checkout #{branch.shellescape}")
172
+ end
173
+
174
+ # Unforced deletion.
175
+ #
176
+ def delete_branch(branch)
177
+ execute_git_command("branch --delete #{branch.shellescape}")
178
+ end
179
+
180
+ def rebase
181
+ execute_git_command("rebase")
182
+ end
183
+
141
184
  # upstream_branch: create an upstream branch.
142
185
  #
143
186
  def push(upstream_branch: nil)
144
187
  upstream_branch_option = "-u origin #{upstream_branch.shellescape}" if upstream_branch
145
188
 
146
- execute_command("git #{gitdir_option} push #{upstream_branch_option}")
189
+ execute_git_command("push #{upstream_branch_option}")
190
+ end
191
+
192
+ # Performs pruning.
193
+ #
194
+ def fetch
195
+ execute_git_command("fetch --prune")
147
196
  end
148
197
 
149
198
  ##########################################################################
@@ -152,8 +201,13 @@ module Geet
152
201
 
153
202
  private
154
203
 
155
- def gitdir_option
156
- "-C #{@location.shellescape}" if @location
204
+ # If executing a git command without calling this API, don't forget to split `gitdir_option`
205
+ # and use it!
206
+ #
207
+ def execute_git_command(command)
208
+ gitdir_option = "-C #{@location.shellescape}" if @location
209
+
210
+ execute_command("git #{gitdir_option} #{command}")
157
211
  end
158
212
  end
159
213
  end