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



253
254
255
# File 'lib/geet/utils/git_client.rb', line 253

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

#checkout(branch) ⇒ Object

OPERATION APIS



225
226
227
# File 'lib/geet/utils/git_client.rb', line 225

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

#cherry(base: nil) ⇒ Object

Return the commit SHAs between HEAD and ‘base`, excluding the already applied commits (which start with `-`)



54
55
56
57
58
59
60
# File 'lib/geet/utils/git_client.rb', line 54

def cherry(base: nil)
  base ||= main_branch

  raw_commits = execute_git_command("cherry #{base.shellescape}")

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

#current_branchObject



62
63
64
65
66
67
68
# File 'lib/geet/utils/git_client.rb', line 62

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) ⇒ Object

Unforced deletion.



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

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

#fetchObject

Performs pruning.



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

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

#main_branchObject



116
117
118
119
120
121
122
123
124
125
# File 'lib/geet/utils/git_client.rb', line 116

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



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

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

#path(upstream: false) ⇒ Object

Example: ‘donaldduck/geet`



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

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

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

#provider_domainObject



180
181
182
183
184
185
186
187
188
# File 'lib/geet/utils/git_client.rb', line 180

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.



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

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



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

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.


198
199
200
201
202
203
204
205
206
207
208
# File 'lib/geet/utils/git_client.rb', line 198

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.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/geet/utils/git_client.rb', line 76

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



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

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.



129
130
131
132
133
# File 'lib/geet/utils/git_client.rb', line 129

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)


100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/geet/utils/git_client.rb', line 100

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.



164
165
166
# File 'lib/geet/utils/git_client.rb', line 164

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)


213
214
215
216
217
218
219
# File 'lib/geet/utils/git_client.rb', line 213

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.



153
154
155
# File 'lib/geet/utils/git_client.rb', line 153

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

#working_tree_clean?Boolean

Returns:

  • (Boolean)


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

def working_tree_clean?
  git_message = execute_git_command("status")

  !!(git_message =~ CLEAN_TREE_MESSAGE_REGEX)
end