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

Returns a new instance of GitRepo.



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

def initialize path
  @path = path.p
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)


115
116
117
118
# File 'lib/babushka/git_repo.rb', line 115

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

#all_branchesObject



175
176
177
178
179
180
181
182
# File 'lib/babushka/git_repo.rb', line 175

def all_branches
  names = repo_shell('git branch -a').split("\n").map {|l| l.sub(/^[* ]+/, '') }
  (names - ['(no branch)']).reject {|i|
    i['/origin/HEAD ->']
  }.map {|i|
    i.sub(/^remotes\//, '')
  }
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)


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

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)


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

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)


151
152
153
# File 'lib/babushka/git_repo.rb', line 151

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.



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

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

#branchesObject

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



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

def branches
  names = repo_shell('git branch').split("\n").map {|l| l.sub(/^[* ]+/, '') }
  names - ['(no branch)']
end

#checkout!(ref) ⇒ Object

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



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

def checkout! ref
  repo_shell("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)


93
94
95
96
# File 'lib/babushka/git_repo.rb', line 93

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:



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

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

#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.



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

def current_branch
  repo_shell("cat .git/HEAD").strip.sub(/^.*refs\/heads\//, '')
end

#current_full_headObject

The full 40 character SHA of the current HEAD.



206
207
208
# File 'lib/babushka/git_repo.rb', line 206

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.



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

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.



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

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.



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

def detach! ref = 'HEAD'
  repo_shell("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)


100
101
102
# File 'lib/babushka/git_repo.rb', line 100

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)


65
66
67
# File 'lib/babushka/git_repo.rb', line 65

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

#git_dirObject

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



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

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)


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

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

#inspectObject



269
270
271
# File 'lib/babushka/git_repo.rb', line 269

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

#merging?Boolean

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

Returns:

  • (Boolean)


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

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

#rebase_merging?Boolean

Returns:

  • (Boolean)


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

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)


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

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)


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

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)


225
226
227
# File 'lib/babushka/git_repo.rb', line 225

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.



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

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

#repo_shell(cmd, opts = {}, &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.



74
75
76
77
78
79
80
# File 'lib/babushka/git_repo.rb', line 74

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

#repo_shell?(cmd, opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


82
83
84
85
86
87
88
# File 'lib/babushka/git_repo.rb', line 82

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

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

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



265
266
267
# File 'lib/babushka/git_repo.rb', line 265

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

#resolve(ref) ⇒ Object

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



211
212
213
# File 'lib/babushka/git_repo.rb', line 211

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

#rootObject

This repo’s top-level directory.



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

def root
  @root ||= self.class.repo_for(path)
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.



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

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