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
@@ -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)
@@ -26,14 +26,15 @@ module Geet
26
26
  # interactive: set when required; in this case, a different API will be used (`system()`
27
27
  # instead of `popen3`).
28
28
  # silent_stderr: don't print the stderr output
29
+ # allow_error: don't raise error on failure
29
30
  #
30
- def execute_command(command, description: nil, interactive: false, silent_stderr: false)
31
+ def execute_command(command, description: nil, interactive: false, silent_stderr: false, allow_error: false)
31
32
  description_message = " on #{description}" if description
32
33
 
33
34
  if interactive
34
35
  system(command)
35
36
 
36
- if !$CHILD_STATUS.success?
37
+ if !$CHILD_STATUS.success? && !allow_error
37
38
  raise "Error#{description_message} (exit status: #{$CHILD_STATUS.exitstatus})"
38
39
  end
39
40
  else
@@ -43,7 +44,7 @@ module Geet
43
44
 
44
45
  puts stderr_content if stderr_content != '' && !silent_stderr
45
46
 
46
- if !wait_thread.value.success?
47
+ if !wait_thread.value.success? && !allow_error
47
48
  error_message = stderr_content.lines.first&.strip || "Error running command #{command.inspect}"
48
49
  raise "Error#{description_message}: #{error_message}"
49
50
  end
@@ -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
@@ -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
@@ -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
 
@@ -23,7 +25,7 @@ module Geet
23
25
  #
24
26
  def execute(
25
27
  title, description, labels: nil, milestone: nil, reviewers: nil,
26
- base: nil, no_open_pr: nil, automated_mode: false, **
28
+ base: nil, draft: false, no_open_pr: nil, automated_mode: false, **
27
29
  )
28
30
  ensure_clean_tree if automated_mode
29
31
 
@@ -37,7 +39,7 @@ module Geet
37
39
 
38
40
  sync_with_upstream_branch if automated_mode
39
41
 
40
- pr = create_pr(title, description, base)
42
+ pr = create_pr(title, description, base: base, draft: draft)
41
43
 
42
44
  if user_has_write_permissions
43
45
  edit_pr(pr, selected_labels, selected_milestone, selected_reviewers)
@@ -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
@@ -93,10 +95,12 @@ module Geet
93
95
  end
94
96
  end
95
97
 
96
- def create_pr(title, description, base)
98
+ def create_pr(title, description, base:, draft:)
97
99
  @out.puts 'Creating PR...'
98
100
 
99
- @repository.create_pr(title, description, @git_client.current_branch, base: base)
101
+ base ||= @git_client.main_branch
102
+
103
+ @repository.create_pr(title, description, @git_client.current_branch, base, draft)
100
104
  end
101
105
 
102
106
  def edit_pr(pr, labels, milestone, reviewers)
@@ -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
@@ -12,6 +12,7 @@ module Geet
12
12
  #
13
13
  class MergePr
14
14
  include Geet::Helpers::ServicesWorkflowHelper
15
+ include Geet::Shared
15
16
 
16
17
  DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
17
18
 
@@ -24,8 +25,30 @@ module Geet
24
25
  def execute(delete_branch: false)
25
26
  merge_owner, merge_head = find_merge_head
26
27
  pr = checked_find_branch_pr(merge_owner, merge_head)
28
+
27
29
  merge_pr(pr)
28
- do_delete_branch if delete_branch
30
+
31
+ if delete_branch
32
+ branch = @git_client.current_branch
33
+
34
+ delete_remote_branch(branch)
35
+ end
36
+
37
+ fetch_repository
38
+
39
+ if upstream_branch_gone?
40
+ pr_branch = @git_client.current_branch
41
+ main_branch = @git_client.main_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
@@ -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
@@ -86,6 +88,13 @@ module Geet
86
88
  # select_entries('reviewer', all_collaborators, 'donaldduck', nil)
87
89
  #
88
90
  def select_entries(entry_type, entries, pattern, name_method)
91
+ # Support both formats Array and String.
92
+ # It seems that at some point, SimpleScripting started splitting arrays automatically, so until
93
+ # the code is adjusted accordingly, this accommodates both the CLI and the test suite.
94
+ # Tracked here: https://github.com/saveriomiroddi/geet/issues/171.
95
+ #
96
+ pattern = pattern.join(',') if pattern.is_a?(Array)
97
+
89
98
  if pattern == MANUAL_LIST_SELECTION_FLAG
90
99
  Geet::Utils::ManualListSelection.new.select_entries(entry_type, entries, name_method: name_method)
91
100
  else
@@ -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
 
@@ -10,20 +11,33 @@ module Geet
10
11
  class GitClient
11
12
  include Geet::Helpers::OsHelper
12
13
 
13
- ORIGIN_NAME = 'origin'
14
+ ORIGIN_NAME = 'origin'
14
15
  UPSTREAM_NAME = 'upstream'
15
16
 
16
- # For simplicity, we match any character except the ones the separators.
17
- REMOTE_ORIGIN_REGEX = %r{
17
+ # Simplified, but good enough, pattern.
18
+ #
19
+ # Relevant matches:
20
+ #
21
+ # 1: protocol + suffix
22
+ # 2: domain
23
+ # 3: domain<>path separator
24
+ # 4: path (repo, project)
25
+ # 5: suffix
26
+ #
27
+ REMOTE_URL_REGEX = %r{
18
28
  \A
19
- (?:https://(.+?)/|git@(.+?):)
20
- ([^/]+/.*?)
21
- (?:\.git)?
29
+ (https://|git@)
30
+ (.+?)
31
+ ([/:])
32
+ (.+/.+?)
33
+ (\.git)?
22
34
  \Z
23
35
  }x
24
36
 
25
37
  UPSTREAM_BRANCH_REGEX = %r{\A[^/]+/([^/]+)\Z}
26
38
 
39
+ MAIN_BRANCH_CONFIG_ENTRY = 'custom.development-branch'
40
+
27
41
  CLEAN_TREE_MESSAGE_REGEX = /^nothing to commit, working tree clean$/
28
42
 
29
43
  def initialize(location: nil)
@@ -38,13 +52,13 @@ module Geet
38
52
  # (which start with `-`)
39
53
  #
40
54
  def cherry(limit)
41
- raw_commits = execute_command("git #{gitdir_option} cherry #{limit.shellescape}")
55
+ raw_commits = execute_git_command("cherry #{limit.shellescape}")
42
56
 
43
57
  raw_commits.split("\n").grep(/^\+/).map { |line| line[3..-1] }
44
58
  end
45
59
 
46
60
  def current_branch
47
- branch = execute_command("git #{gitdir_option} rev-parse --abbrev-ref HEAD")
61
+ branch = execute_git_command("rev-parse --abbrev-ref HEAD")
48
62
 
49
63
  raise "Couldn't find current branch" if branch == 'HEAD'
50
64
 
@@ -53,12 +67,14 @@ module Geet
53
67
 
54
68
  # Not to be confused with `upstream` repository!
55
69
  #
70
+ # This API doesn't reveal if the remote branch is gone.
71
+ #
56
72
  # return: nil, if the upstream branch is not configured.
57
73
  #
58
74
  def upstream_branch
59
- head_symbolic_ref = execute_command("git #{gitdir_option} symbolic-ref -q HEAD")
75
+ head_symbolic_ref = execute_git_command("symbolic-ref -q HEAD")
60
76
 
61
- raw_upstream_branch = execute_command("git #{gitdir_option} for-each-ref --format='%(upstream:short)' #{head_symbolic_ref.shellescape}").strip
77
+ raw_upstream_branch = execute_git_command("for-each-ref --format='%(upstream:short)' #{head_symbolic_ref.shellescape}").strip
62
78
 
63
79
  if raw_upstream_branch != ''
64
80
  raw_upstream_branch[UPSTREAM_BRANCH_REGEX, 1] || raise("Unexpected upstream format: #{raw_upstream_branch}")
@@ -67,8 +83,43 @@ module Geet
67
83
  end
68
84
  end
69
85
 
86
+ # TODO: May be merged with :upstream_branch, although it would require designing how a gone
87
+ # remote branch is expressed.
88
+ #
89
+ # Sample command output:
90
+ #
91
+ # ## add_milestone_closing...origin/add_milestone_closing [gone]
92
+ # M spec/integration/merge_pr_spec.rb
93
+ #
94
+ def upstream_branch_gone?
95
+ git_command = "status -b --porcelain"
96
+ status_output = execute_git_command(git_command)
97
+
98
+ # Simplified branch naming pattern. The exact one (see https://stackoverflow.com/a/3651867)
99
+ # is not worth implementing.
100
+ #
101
+ if status_output =~ %r(^## .+\.\.\..+?( \[gone\])?$)
102
+ !!$LAST_MATCH_INFO[1]
103
+ else
104
+ raise "Unexpected git command #{git_command.inspect} output: #{status_output}"
105
+ end
106
+ end
107
+
108
+ # See https://saveriomiroddi.github.io/Conveniently-Handling-non-master-development-default-branches-in-git-hub
109
+ #
110
+ def main_branch
111
+ branch_name = execute_git_command("config --get #{MAIN_BRANCH_CONFIG_ENTRY}", allow_error: true)
112
+
113
+ if branch_name.empty?
114
+ full_branch_name = execute_git_command("rev-parse --abbrev-ref #{ORIGIN_NAME}/HEAD")
115
+ full_branch_name.split('/').last
116
+ else
117
+ branch_name
118
+ end
119
+ end
120
+
70
121
  def working_tree_clean?
71
- git_message = execute_command("git #{gitdir_option} status")
122
+ git_message = execute_git_command("status")
72
123
 
73
124
  !!(git_message =~ CLEAN_TREE_MESSAGE_REGEX)
74
125
  end
@@ -80,19 +131,26 @@ module Geet
80
131
  # Show the description ("<subject>\n\n<body>") for the given git object.
81
132
  #
82
133
  def show_description(object)
83
- execute_command("git #{gitdir_option} show --quiet --format='%s\n\n%b' #{object.shellescape}")
134
+ execute_git_command("show --quiet --format='%s\n\n%b' #{object.shellescape}")
84
135
  end
85
136
 
86
137
  ##########################################################################
87
- # REPOSITORY/REMOTE APIS
138
+ # REPOSITORY/REMOTE QUERYING APIS
88
139
  ##########################################################################
89
140
 
141
+ # Return the components of the remote, according to REMOTE_URL_REGEX; doesn't include the full
142
+ # match.
143
+ #
144
+ def remote_components(name: nil)
145
+ remote.match(REMOTE_URL_REGEX)[1..]
146
+ end
147
+
90
148
  # Example: `donaldduck/geet`
91
149
  #
92
150
  def path(upstream: false)
93
- remote_name = upstream ? UPSTREAM_NAME : ORIGIN_NAME
151
+ remote_name_option = upstream ? {name: UPSTREAM_NAME} : {}
94
152
 
95
- remote(remote_name)[REMOTE_ORIGIN_REGEX, 3]
153
+ remote(**remote_name_option)[REMOTE_URL_REGEX, 4]
96
154
  end
97
155
 
98
156
  def owner
@@ -101,12 +159,10 @@ module Geet
101
159
 
102
160
  def provider_domain
103
161
  # We assume that it's not possible to have origin and upstream on different providers.
104
- #
105
- remote_url = remote(ORIGIN_NAME)
106
162
 
107
- domain = remote_url[REMOTE_ORIGIN_REGEX, 1] || remote_url[REMOTE_ORIGIN_REGEX, 2]
163
+ domain = remote()[REMOTE_URL_REGEX, 2]
108
164
 
109
- raise "Can't identify domain in the provider domain string: #{domain}" if domain !~ /(.*)\.\w+/
165
+ raise "Can't identify domain in the provider domain string: #{domain}" if domain !~ /\w+\.\w+/
110
166
 
111
167
  domain
112
168
  end
@@ -116,12 +172,15 @@ module Geet
116
172
  #
117
173
  # The result is in the format `git@github.com:donaldduck/geet.git`
118
174
  #
119
- def remote(name)
120
- remote_url = execute_command("git #{gitdir_option} ls-remote --get-url #{name}")
175
+ # options
176
+ # :name: remote name; if unspecified, the default remote is used.
177
+ #
178
+ def remote(name: nil)
179
+ remote_url = execute_git_command("ls-remote --get-url #{name}")
121
180
 
122
- if remote_url == name
181
+ if !remote_defined?(name)
123
182
  raise "Remote #{name.inspect} not found!"
124
- elsif remote_url !~ REMOTE_ORIGIN_REGEX
183
+ elsif remote_url !~ REMOTE_URL_REGEX
125
184
  raise "Unexpected remote reference format: #{remote_url.inspect}"
126
185
  end
127
186
 
@@ -132,9 +191,10 @@ module Geet
132
191
  # purposes, any any action that needs to work with the remote, uses #remote.
133
192
  #
134
193
  def remote_defined?(name)
135
- remote_url = execute_command("git #{gitdir_option} ls-remote --get-url #{name}")
194
+ remote_url = execute_git_command("ls-remote --get-url #{name}")
136
195
 
137
- # If the remote is not define, `git ls-remote` will return the passed value.
196
+ # If the remote is not defined, `git ls-remote` will return the passed value.
197
+ #
138
198
  remote_url != name
139
199
  end
140
200
 
@@ -142,12 +202,36 @@ module Geet
142
202
  # OPERATION APIS
143
203
  ##########################################################################
144
204
 
205
+ def checkout(branch)
206
+ execute_git_command("checkout #{branch.shellescape}")
207
+ end
208
+
209
+ # Unforced deletion.
210
+ #
211
+ def delete_branch(branch)
212
+ execute_git_command("branch --delete #{branch.shellescape}")
213
+ end
214
+
215
+ def rebase
216
+ execute_git_command("rebase")
217
+ end
218
+
145
219
  # upstream_branch: create an upstream branch.
146
220
  #
147
221
  def push(upstream_branch: nil)
148
- upstream_branch_option = "-u origin #{upstream_branch.shellescape}" if upstream_branch
222
+ upstream_branch_option = "-u #{ORIGIN_NAME} #{upstream_branch.shellescape}" if upstream_branch
223
+
224
+ execute_git_command("push #{upstream_branch_option}")
225
+ end
226
+
227
+ # Performs pruning.
228
+ #
229
+ def fetch
230
+ execute_git_command("fetch --prune")
231
+ end
149
232
 
150
- execute_command("git #{gitdir_option} push #{upstream_branch_option}")
233
+ def add_remote(name, url)
234
+ execute_git_command("remote add #{name.shellescape} #{url}")
151
235
  end
152
236
 
153
237
  ##########################################################################
@@ -156,8 +240,17 @@ module Geet
156
240
 
157
241
  private
158
242
 
159
- def gitdir_option
160
- "-C #{@location.shellescape}" if @location
243
+ # If executing a git command without calling this API, don't forget to split `gitdir_option`
244
+ # and use it!
245
+ #
246
+ # options (passed to :execute_command):
247
+ # - allow_error
248
+ # - (others)
249
+ #
250
+ def execute_git_command(command, **options)
251
+ gitdir_option = "-C #{@location.shellescape}" if @location
252
+
253
+ execute_command("git #{gitdir_option} #{command}", **options)
161
254
  end
162
255
  end
163
256
  end