Class: Geet::Utils::GitClient

Inherits:
Object
  • Object
show all
Includes:
Helpers::OsHelper
Defined in:
lib/geet/utils/git_client.rb

Overview

Represents the git program interface; used for performing git operations.

Constant Summary collapse

ORIGIN_NAME =
'origin'
UPSTREAM_NAME =
'upstream'
REMOTE_URL_REGEX =

Simplified, but good enough, pattern.

Relevant matches:

1: protocol + suffix
2: domain
3: domain<>path separator
4: path (repo, project)
5: suffix
%r{
  \A
  (https://|git@)
  (.+?)
  ([/:])
  (.+/.+?)
  (\.git)?
  \Z
}x
REMOTE_BRANCH_REGEX =
%r{\A[^/]+/(.+)\Z}
MAIN_BRANCH_CONFIG_ENTRY =
'custom.development-branch'
CLEAN_TREE_MESSAGE_REGEX =
/^nothing to commit, working tree clean$/

Instance Method Summary collapse

Methods included from Helpers::OsHelper

#execute_command, #open_file_with_default_application

Constructor Details

#initialize(location: nil) ⇒ GitClient

Returns a new instance of GitClient.



43
44
45
# File 'lib/geet/utils/git_client.rb', line 43

def initialize(location: nil)
  @location = location
end

Instance Method Details

#add_remote(name, url) ⇒ Object



259
260
261
# File 'lib/geet/utils/git_client.rb', line 259

def add_remote(name, url)
  execute_git_command("remote add #{name.shellescape} #{url}")
end

#checkout(branch) ⇒ Object

OPERATION APIS



231
232
233
# File 'lib/geet/utils/git_client.rb', line 231

def checkout(branch)
  execute_git_command("checkout #{branch.shellescape}")
end

#cherry(upstream, head: nil) ⇒ Object

Return the commit SHAs between :head and :upstream, excluding the already applied commits (which start with ‘-`)

  • upstream: String; pass :main_branch to use the main branch

  • head: String (optional); pass :main_branch to use the main branch



57
58
59
60
61
62
63
64
65
66
# File 'lib/geet/utils/git_client.rb', line 57

def cherry(upstream, head: nil)
  upstream = main_branch if upstream == :main_branch
  head = main_branch if head == :main_branch

  git_params = [upstream, head].compact.map(&:shellescape)

  raw_commits = execute_git_command("cherry #{git_params.join(' ')}")

  raw_commits.split("\n").grep(/^\+/).map { |line| line[3..-1] }
end

#current_branchObject



68
69
70
71
72
73
74
# File 'lib/geet/utils/git_client.rb', line 68

def current_branch
  branch = execute_git_command("rev-parse --abbrev-ref HEAD")

  raise "Couldn't find current branch" if branch == 'HEAD'

  branch
end

#delete_branch(branch, force:) ⇒ Object



235
236
237
238
239
# File 'lib/geet/utils/git_client.rb', line 235

def delete_branch(branch, force:)
  force_option = "--force" if force

  execute_git_command("branch --delete #{force_option} #{branch.shellescape}")
end

#fetchObject

Performs pruning.



255
256
257
# File 'lib/geet/utils/git_client.rb', line 255

def fetch
  execute_git_command("fetch --prune")
end

#main_branchObject



122
123
124
125
126
127
128
129
130
131
# File 'lib/geet/utils/git_client.rb', line 122

def main_branch
  branch_name = execute_git_command("config --get #{MAIN_BRANCH_CONFIG_ENTRY}", allow_error: true)

  if branch_name.empty?
    full_branch_name = execute_git_command("rev-parse --abbrev-ref #{ORIGIN_NAME}/HEAD")
    full_branch_name.split('/').last
  else
    branch_name
  end
end

#ownerObject



182
183
184
# File 'lib/geet/utils/git_client.rb', line 182

def owner
  path.split('/')[0]
end

#path(upstream: false) ⇒ Object

Example: ‘donaldduck/geet`



176
177
178
179
180
# File 'lib/geet/utils/git_client.rb', line 176

def path(upstream: false)
  remote_name_option = upstream ? {name: UPSTREAM_NAME} : {}

  remote(**remote_name_option)[REMOTE_URL_REGEX, 4]
end

#provider_domainObject



186
187
188
189
190
191
192
193
194
# File 'lib/geet/utils/git_client.rb', line 186

def provider_domain
  # We assume that it's not possible to have origin and upstream on different providers.

  domain = remote()[REMOTE_URL_REGEX, 2]

  raise "Can't identify domain in the provider domain string: #{domain}" if domain !~ /\w+\.\w+/

  domain
end

#push(remote_branch: nil, force: false) ⇒ Object

remote_branch: create an upstream branch.



247
248
249
250
251
# File 'lib/geet/utils/git_client.rb', line 247

def push(remote_branch: nil, force: false)
  remote_branch_option = "-u #{ORIGIN_NAME} #{remote_branch.shellescape}" if remote_branch

  execute_git_command("push #{"--force" if force} #{remote_branch_option}")
end

#rebaseObject



241
242
243
# File 'lib/geet/utils/git_client.rb', line 241

def rebase
  execute_git_command("rebase")
end

#remote(name: nil) ⇒ Object

Returns the URL of the remote with the given name. Sanity checks are performed.

The result is in the format ‘[email protected]:donaldduck/geet.git`

options

:name:           remote name; if unspecified, the default remote is used.


204
205
206
207
208
209
210
211
212
213
214
# File 'lib/geet/utils/git_client.rb', line 204

def remote(name: nil)
  remote_url = execute_git_command("ls-remote --get-url #{name}")

  if !remote_defined?(name)
    raise "Remote #{name.inspect} not found!"
  elsif remote_url !~ REMOTE_URL_REGEX
    raise "Unexpected remote reference format: #{remote_url.inspect}"
  end

  remote_url
end

#remote_branch(qualify: false) ⇒ Object

This API doesn’t reveal if the remote branch is gone.

qualify: (false) include the remote if true, don’t otherwise

return: nil, if the remote branch is not configured.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/geet/utils/git_client.rb', line 82

def remote_branch(qualify: false)
  head_symbolic_ref = execute_git_command("symbolic-ref -q HEAD")

  raw_remote_branch = execute_git_command("for-each-ref --format='%(upstream:short)' #{head_symbolic_ref.shellescape}").strip

  if raw_remote_branch != ''
    if qualify
      raw_remote_branch
    else
      raw_remote_branch[REMOTE_BRANCH_REGEX, 1] || raise("Unexpected remote branch format: #{raw_remote_branch}")
    end
  else
    nil
  end
end

#remote_branch_diffObject



141
142
143
144
145
# File 'lib/geet/utils/git_client.rb', line 141

def remote_branch_diff
  remote_branch = remote_branch(qualify: true)

  execute_git_command("diff #{remote_branch.shellescape}")
end

#remote_branch_diff_commitsObject

List of different commits between local and corresponding remote branch.



135
136
137
138
139
# File 'lib/geet/utils/git_client.rb', line 135

def remote_branch_diff_commits
  remote_branch = remote_branch(qualify: true)

  execute_git_command("rev-list #{remote_branch.shellescape}..HEAD")
end

#remote_branch_gone?Boolean

TODO: May be merged with :remote_branch, although it would require designing how a gone remote branch is expressed.

Sample command output:

## add_milestone_closing...origin/add_milestone_closing [gone]
 M spec/integration/merge_pr_spec.rb

Returns:

  • (Boolean)


106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/geet/utils/git_client.rb', line 106

def remote_branch_gone?
  git_command = "status -b --porcelain"
  status_output = execute_git_command(git_command)

  # Simplified branch naming pattern. The exact one (see https://stackoverflow.com/a/3651867)
  # is not worth implementing.
  #
  if status_output =~ %r(^## .+\.\.\..+?( \[gone\])?$)
    !!$LAST_MATCH_INFO[1]
  else
    raise "Unexpected git command #{git_command.inspect} output: #{status_output}"
  end
end

#remote_components(name: nil) ⇒ Object

Return the components of the remote, according to REMOTE_URL_REGEX; doesn’t include the full match.



170
171
172
# File 'lib/geet/utils/git_client.rb', line 170

def remote_components(name: nil)
  remote.match(REMOTE_URL_REGEX)[1..]
end

#remote_defined?(name) ⇒ Boolean

Doesn’t sanity check for the remote url format; this action is for querying purposes, any any action that needs to work with the remote, uses #remote.

Returns:

  • (Boolean)


219
220
221
222
223
224
225
# File 'lib/geet/utils/git_client.rb', line 219

def remote_defined?(name)
  remote_url = execute_git_command("ls-remote --get-url #{name}")

  # If the remote is not defined, `git ls-remote` will return the passed value.
  #
  remote_url != name
end

#show_description(object) ⇒ Object

Show the description (“<subject>nn<body>”) for the given git object.



159
160
161
# File 'lib/geet/utils/git_client.rb', line 159

def show_description(object)
  execute_git_command("show --quiet --format='%s\n\n%b' #{object.shellescape}")
end

#working_tree_clean?Boolean

Returns:

  • (Boolean)


147
148
149
150
151
# File 'lib/geet/utils/git_client.rb', line 147

def working_tree_clean?
  git_message = execute_git_command("status")

  !!(git_message =~ CLEAN_TREE_MESSAGE_REGEX)
end