geet 0.3.15 → 0.4.1

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