Class: Babushka::GitRepo

Inherits:
Object show all
Extended by:
ShellHelpers
Includes:
ShellHelpers
Defined in:
lib/babushka/git_repo.rb

Overview

Provides some wrapper methods for interacting concisely with a git repository.

The repo is accessed by shelling out to `git` via the #repo_shell method, which makes sure the repo exists and that the commands are run in the right place. Hence, GitRepo doesn't depend on your current working directory.

Most of the methods return a boolean and are used to discover things about the repository, like whether it's #clean? or #dirty?, currently #merging? or #rebasing?, and whether it's #ahead? or #behind? the default remote.

Some return a piece of data, like #current_head and #current_branch.

There are also some methods that make simple changes to the repository, like #checkout! and #reset_hard!.

To perform other operations on the repository like committing or rebasing, check out grit. This class is just a little `git` wrapper.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ShellHelpers

cmd_dir, current_username, log_shell, login_shell, raw_shell, shell, shell!, shell?, shell_cmd, sudo, which

Methods included from LogHelpers

debug, deprecated!, log, log_block, log_error, log_ok, log_stderr, log_warn, removed!

Constructor Details

#initialize(path, opts = {}) ⇒ GitRepo

Returns a new instance of GitRepo.


46
47
48
49
# File 'lib/babushka/git_repo.rb', line 46

def initialize path, opts = {}
  @path = path.p
  @run_as_owner = !!opts[:run_as_owner]
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.


32
33
34
# File 'lib/babushka/git_repo.rb', line 32

def path
  @path
end

Class Method Details

.repo_for(path) ⇒ Object

The full path to the root of the repo #path is within, if it is within one somewhere; otherwise nil.


36
37
38
39
40
41
42
43
44
# File 'lib/babushka/git_repo.rb', line 36

def self.repo_for path
  maybe = shell?("git rev-parse --git-dir", :cd => path) if path.p.dir?

  if maybe == '.git'
    path.p
  elsif !maybe.nil?
    maybe / '..'
  end
end

Instance Method Details

#ahead?Boolean

True if there are any commits in the current branch's history that aren't also present on the corresponding remote branch, or if the remote doesn't exist.

Returns:

  • (Boolean)

139
140
141
142
# File 'lib/babushka/git_repo.rb', line 139

def ahead?
  !remote_branch_exists? ||
  !repo_shell("git rev-list #{current_remote_branch}..").split("\n").empty?
end

#all_branchesObject


199
200
201
202
# File 'lib/babushka/git_repo.rb', line 199

def all_branches
  names = repo_shell('git show-ref') || ""
  names.split("\n").collapse(%r{^\w+ refs/(?:heads|remotes)/}) - ['origin/HEAD']
end

#applying?Boolean

True if a patch is partway through being applied – perhaps because applying it caused conflicts that are yet to be resolved.

Returns:

  • (Boolean)

162
163
164
165
166
# File 'lib/babushka/git_repo.rb', line 162

def applying?
  %w[rebase rebase-apply ../.dotest].any? {|d|
    (git_dir / d / 'applying').exists?
  }
end

#behind?Boolean

True if there are any commits in the current branch's corresponding remote branch that aren't also present locally, if the remote branch exists.

Returns:

  • (Boolean)

146
147
148
149
# File 'lib/babushka/git_repo.rb', line 146

def behind?
  remote_branch_exists? &&
  !repo_shell("git rev-list ..#{current_remote_branch}").split("\n").empty?
end

#bisecting?Boolean

True if a bisect is currently in progress.

Returns:

  • (Boolean)

175
176
177
# File 'lib/babushka/git_repo.rb', line 175

def bisecting?
  (git_dir / 'BISECT_LOG').exists?
end

#branch!(branch, ref = 'HEAD') ⇒ Object

Create a new local branch called branch with ref (defaulting to HEAD) as its tip.


291
292
293
# File 'lib/babushka/git_repo.rb', line 291

def branch! branch, ref = 'HEAD'
  repo_shell_as_owner("git branch '#{branch}' '#{ref}'")
end

#branchesObject

An array of the names of all the local branches in this repo.


194
195
196
197
# File 'lib/babushka/git_repo.rb', line 194

def branches
  names = repo_shell('git show-ref --heads') || ""
  names.split("\n").collapse(%r{^\w+ refs/heads/})
end

#checkout!(ref) ⇒ Object

Check out the supplied ref, detaching the HEAD if the named ref isn't a branch.


304
305
306
# File 'lib/babushka/git_repo.rb', line 304

def checkout! ref
  repo_shell_as_owner("git checkout '#{ref}'")
end

#clean?Boolean

True if the repo is clean, i.e. when the content in its index and working copy match the commit that HEAD refers to.

Returns:

  • (Boolean)

117
118
119
120
# File 'lib/babushka/git_repo.rb', line 117

def clean?
  repo_shell("git status") # Sometimes git caches invalid index info; this clears it.
  repo_shell("git diff-index --name-status HEAD", &:stdout).blank?
end

#clone!(from) ⇒ Object

Clone the remote at from to this GitRepo's path. The path must be nonexistent; an error is raised if the local repo already exists.

Raises:


278
279
280
281
282
283
# File 'lib/babushka/git_repo.rb', line 278

def clone! from
  raise GitRepoExists, "Can't clone #{from} to existing path #{path}." if exists?
  shell("git clone '#{from}' '#{path.basename}'", :cd => path.parent, :create => true) {|shell|
    shell.ok? || raise(GitRepoError, "Couldn't clone to #{path}: #{error_message_for shell.stderr}")
  }
end

#commit!(message) ⇒ Object


285
286
287
# File 'lib/babushka/git_repo.rb', line 285

def commit! message
  repo_shell_as_owner("git", "commit", "-m", message)
end

#current_branchObject

The name of the branch that's currently checked out, if any. If there is no current branch (i.e. if the HEAD is detached), the HEAD's SHA is returned instead.


207
208
209
210
211
212
213
214
215
# File 'lib/babushka/git_repo.rb', line 207

def current_branch
  # Can't use --short because many VPS gits lack it.
  symbolic_ref = repo_shell?("git symbolic-ref -q HEAD")
  if symbolic_ref
    symbolic_ref.sub(%r{^refs/heads/}, '')
  else
    current_full_head
  end
end

#current_full_headObject

The full 40 character SHA of the current HEAD.


232
233
234
# File 'lib/babushka/git_repo.rb', line 232

def current_full_head
  repo_shell("git rev-parse HEAD")
end

#current_headObject

The short SHA of the repo's current HEAD. This is usually 7 characters, but is longer when extra characters are required to disambiguate it.


227
228
229
# File 'lib/babushka/git_repo.rb', line 227

def current_head
  repo_shell("git rev-parse --short HEAD")
end

#current_remote_branchObject

The namespaced name of the remote branch that the current local branch is tracking, or on origin if the branch isn't tracking an explicit remote branch.


220
221
222
223
# File 'lib/babushka/git_repo.rb', line 220

def current_remote_branch
  branch = current_branch
  "#{remote_for(branch)}/#{branch}"
end

#detach!(ref = 'HEAD') ⇒ Object

Check out the supplied ref, detaching the HEAD. If the ref is a branch or tag, HEAD will reference the commit at the tip of the ref.


310
311
312
# File 'lib/babushka/git_repo.rb', line 310

def detach! ref = 'HEAD'
  repo_shell_as_owner("git checkout '#{resolve(ref)}'")
end

#dirty?Boolean

The inverse of #clean? – true if the content in the repo's index or working copy differs from the commit HEAD refers to.

Returns:

  • (Boolean)

124
125
126
# File 'lib/babushka/git_repo.rb', line 124

def dirty?
  !clean?
end

#exists?Boolean

True if root points to an existing git repo.

The repo doesn't always have to exist. For example, you can pass a nonexistent path when you initialize a GitRepo, and then call #clone! on it.

Returns:

  • (Boolean)

70
71
72
# File 'lib/babushka/git_repo.rb', line 70

def exists?
  !root.nil? && root.exists?
end

#git_dirObject

This repo's .git directory, where git stores its objects and other repo data.


61
62
63
# File 'lib/babushka/git_repo.rb', line 61

def git_dir
  root / '.git'
end

#include?(ref) ⇒ Boolean

True if the commit referenced by ref is present somewhere in this repo.

Note that the ref being present doesn't mean that it's a parent of HEAD, just that it currently resolves to a commit.

Returns:

  • (Boolean)

132
133
134
# File 'lib/babushka/git_repo.rb', line 132

def include? ref
  repo_shell("git rev-list -n 1 '#{ref}'")
end

#init!(gitignore_contents = '') ⇒ Object

Initialize this repository, if it doesn't already exist, using the supplied gitignore contents (or a blank one).


266
267
268
269
270
271
272
273
274
# File 'lib/babushka/git_repo.rb', line 266

def init! gitignore_contents = ''
  if !exists?
    path.mkdir
    shell('git init', :cd => path)
    (path / '.gitignore').write(gitignore_contents)
    shell('git add .gitignore', :cd => path)
    shell('git commit -m "Add .gitignore."', :cd => path)
  end
end

#inspectObject


320
321
322
323
324
325
326
# File 'lib/babushka/git_repo.rb', line 320

def inspect
  if !exists?
    "#<GitRepo:#{path} (nonexistent)>"
  else
    "#<GitRepo:#{root} : #{current_branch}@#{current_head}#{' (dirty)' if dirty?}>"
  end
end

#merging?Boolean

True if a merge is in progress – perhaps because it produced conflicts that are yet to be resolved.

Returns:

  • (Boolean)

170
171
172
# File 'lib/babushka/git_repo.rb', line 170

def merging?
  (git_dir / 'MERGE_HEAD').exists? or rebase_merging?
end

#rebase_merging?Boolean

Returns:

  • (Boolean)

179
180
181
182
183
# File 'lib/babushka/git_repo.rb', line 179

def rebase_merging?
  %w[rebase-merge .dotest-merge].any? {|d|
    (git_dir / d).exists?
  }
end

#rebasing?Boolean

True if the repo is partway through a rebase of some kind. This could be because one of the commits conflicted when it was replayed, or that the rebase is interactive and is awaiting another command.

Returns:

  • (Boolean)

154
155
156
157
158
# File 'lib/babushka/git_repo.rb', line 154

def rebasing?
  %w[rebase rebase-apply ../.dotest].any? {|d|
    (git_dir / d).exists?
  } or rebase_merging? or rebasing_interactively?
end

#rebasing_interactively?Boolean

Returns:

  • (Boolean)

185
186
187
188
189
# File 'lib/babushka/git_repo.rb', line 185

def rebasing_interactively?
  %w[rebase-merge .dotest-merge].any? {|d|
    (git_dir / d / 'interactive').exists?
  }
end

#remote_branch_exists?Boolean

True if origin contains a branch of the same name as the current local branch.

Returns:

  • (Boolean)

260
261
262
# File 'lib/babushka/git_repo.rb', line 260

def remote_branch_exists?
  repo_shell?("git rev-parse refs/remotes/#{current_remote_branch}")
end

#remote_for(branch) ⇒ Object

The remote assigned to branch in the git config, or 'origin' if none is set. This is the remote that git pushes to and fetches from for this branch by default, and the branch that comparisons like #ahead? and #behind? are made against.


254
255
256
# File 'lib/babushka/git_repo.rb', line 254

def remote_for branch
  repo_shell?("git config branch.#{branch}.remote") || 'origin'
end

#repo_shell(*cmd, &block) ⇒ Object

Run cmd on the shell, changing to this repo's #root. If the repo doesn't exist, a GitRepoError is raised.

A GitRepo with a nonexistent #root is valid - it will only fail if an operation that requires an existing repo is attempted.


79
80
81
82
83
84
85
86
# File 'lib/babushka/git_repo.rb', line 79

def repo_shell *cmd, &block
  if !exists?
    raise GitRepoError, "There is no repo at #{@path}."
  else
    opts = cmd.extract_options!
    shell(*cmd.push(opts.merge(:cd => root)), &block)
  end
end

#repo_shell?(*cmd) ⇒ Boolean

Run cmd on the shell using shell?.

The semantics of this command are identical to those of repo_shell, except that shell? is used to invoke the command instead of shell.

(See the ShellHelpers docs for details on those two methods).

Returns:

  • (Boolean)

94
95
96
97
98
99
100
101
# File 'lib/babushka/git_repo.rb', line 94

def repo_shell? *cmd
  if !exists?
    raise GitRepoError, "There is no repo at #{@path}."
  else
    opts = cmd.extract_options!
    shell?(*cmd.push(opts.merge(:cd => root)))
  end
end

#repo_shell_as_owner(*cmd, &block) ⇒ Object

Run cmd via repo_shell, sudoing as the owner of the repository if the run_as_owner? flag is set.

This command is useful for cleanly working with a root-owned repo without having to run babushka as root.


108
109
110
111
112
113
# File 'lib/babushka/git_repo.rb', line 108

def repo_shell_as_owner *cmd, &block
  opts = cmd.extract_options!
  opts[:as] = root.owner if run_as_owner?

  repo_shell(*cmd.push(opts), &block)
end

#reset_hard!(ref = 'HEAD') ⇒ Object

Reset the repo to the given ref, discarding changes in the index and working copy.


316
317
318
# File 'lib/babushka/git_repo.rb', line 316

def reset_hard! ref = 'HEAD'
  repo_shell_as_owner("git reset --hard '#{ref}'")
end

#resolve(ref) ⇒ Object

The short SHA of the commit that ref currently refers to.

Note that it's possible for git's short SHA to change when new commits enter the repo (via commit or fetch), as git adds characters to keep short SHAs unique.


241
242
243
# File 'lib/babushka/git_repo.rb', line 241

def resolve ref
  repo_shell?("git rev-parse --short #{ref}^{commit}")
end

#resolve_full(ref) ⇒ Object

The full SHA of the commit that ref currently refers to.


246
247
248
# File 'lib/babushka/git_repo.rb', line 246

def resolve_full ref
  repo_shell?("git rev-parse #{ref}^{commit}")
end

#rootObject

This repo's top-level directory.


56
57
58
# File 'lib/babushka/git_repo.rb', line 56

def root
  @root ||= self.class.repo_for(path)
end

#run_as_owner?Boolean

Returns:

  • (Boolean)

51
52
53
# File 'lib/babushka/git_repo.rb', line 51

def run_as_owner?
  @run_as_owner
end

#track!(branch) ⇒ Object

Create a new local tracking branch for branch, which should be specified as remote/branch. For example, if “origin/next” is passed, a local 'next' branch will be created to track origin's 'next' branch.


298
299
300
# File 'lib/babushka/git_repo.rb', line 298

def track! branch
  repo_shell_as_owner("git checkout -t '#{branch}' -b '#{branch.sub(%r{^.*/}, '')}'")
end