Class: Gitgo::Git

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Utils
Defined in:
lib/gitgo/git.rb,
lib/gitgo/git/tree.rb,
lib/gitgo/git/utils.rb

Overview

A wrapper to a Grit::Repo that allows access and modification of a git repository without checking files out (under most circumstances). The api is patterned after the git command line interface.

Usage

Checkout, add, and commit new content:

git = Git.init("example", :author => "John Doe <[email protected]>")
git.add(
  "README" => "New Project",
  "lib/project.rb" => "module Project\nend",
  "remove_this_file" => "won't be here long...")

git.commit("setup a new project")

Content may be removed as well:

git.rm("remove_this_file")
git.commit("removed extra file")

Now access the content:

git["/"]                          # => ["README", "lib"]
git["/lib/project.rb"]            # => "module Project\nend"
git["/remove_this_file"]          # => nil

You can go back in time if you wish:

git.branch = "gitgo^"
git["/remove_this_file"]          # => "won't be here long..."

For direct access to the Grit objects, use get:

git.get("/lib").id                # => "cad0dc0df65848aa8f3fee72ce047142ec707320"
git.get("/lib/project.rb").id     # => "636e25a2c9fe1abc3f4d3f380956800d5243800e"

The Working Tree

Changes to the repo are tracked by an in-memory working tree until being committed. Trees can be thought of as a hash of (path, [:mode, sha]) pairs representing the contents of a directory.

git = Git.init("example", :author => "John Doe <[email protected]>")
git.add(
  "README" => "New Project",
  "lib/project.rb" => "module Project\nend"
).commit("added files")

git.tree
# => {
#   "README" => [:"100644", "73a86c2718da3de6414d3b431283fbfc074a79b1"],
#   "lib" => {
#     "project.rb" => [:"100644", "636e25a2c9fe1abc3f4d3f380956800d5243800e"]
#   }
# }

Trees can be collapsed using reset. Afterwards subtrees are only expanded as needed; before expansion they appear as a [:mode, sha] pair and after expansion they appear as a hash. Symbol paths are used to differentiate subtrees (which can be expanded) from blobs (which cannot be expanded).

git.reset
git.tree
# => {
#   "README" => [:"100644", "73a86c2718da3de6414d3b431283fbfc074a79b1"],
#   :lib =>     [:"040000", "cad0dc0df65848aa8f3fee72ce047142ec707320"]
# }

git.add("lib/project/utils.rb" => "module Project\n  module Utils\n  end\nend")
git.tree
# => {
#   "README" => [:"100644", "73a86c2718da3de6414d3b431283fbfc074a79b1"],
#   "lib" => {
#     "project.rb" => [:"100644", "636e25a2c9fe1abc3f4d3f380956800d5243800e"],
#     "project" => {
#       "utils.rb" => [:"100644", "c4f9aa58d6d5a2ebdd51f2f628b245f9454ff1a4"]
#     }
#   }
# }

git.rm("README")
git.tree
# => {
#   "lib" => {
#     "project.rb" => [:"100644", "636e25a2c9fe1abc3f4d3f380956800d5243800e"],
#     "project" => {
#       "utils.rb" => [:"100644", "c4f9aa58d6d5a2ebdd51f2f628b245f9454ff1a4"]
#     }
#   }
# }

The working tree can be compared with the commit tree to produce a list of files that have been added and removed using the status method:

git.status
# => {
#   "README" => :rm
#   "lib/project/utils.rb" => :add
# }

Tracking, Push/Pull

Git provides limited support for setting a tracking branch and doing push/pull from tracking branches without checking the gitgo branch out. More complicated operations can are left to the command line, where the current branch can be directly manipulated by the git program.

Unlike git (the program), Git (the class) requires the upstream branch setup by ‘git branch –track’ to be an existing tracking branch. As an example, if you were to setup this:

% git branch --track remote/branch

Or equivalently this:

git = Git.init
git.track "remote/branch"

Then Git would assume:

  • the upstream branch is ‘remote/branch’

  • the tracking branch is ‘remotes/remote/branch’

  • the ‘branch.name.remote’ config is ‘remote’

  • the ‘branch.name.merge’ config is ‘refs/heads/branch’

If ever these assumptions are broken, for instance if the gitgo branch is manually set up to track a local branch, methods like pull/push could cause odd failures. To help check:

  • track will raise an error if the upstream branch is not a tracking branch

  • upstream_branch raises an error if the ‘branch.name.merge’ config doesn’t follow the ‘ref/heads/branch’ pattern

  • pull/push raise an error given a non-tracking branch

Under normal circumstances, all these assumptions will be met.

Defined Under Namespace

Modules: Utils Classes: Tree

Constant Summary collapse

DEFAULT_BRANCH =

The default branch

'gitgo'
DEFAULT_UPSTREAM_BRANCH =

The default upstream branch for push/pull

'origin/gitgo'
DEFAULT_WORK_DIR =

The default directory for gitgo-related files

'gitgo'
DEFAULT_BLOB_MODE =

The default blob mode used for added blobs

'100644'.to_sym
DEFAULT_TREE_MODE =

The default tree mode used for added trees

'40000'.to_sym
SHA =

A regexp matching a valid sha sum

/\A[A-Fa-f\d]{40}\z/
GIT_VERSION =

The minimum required version of git (see Git.version_ok?)

[1,6,4,2]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

nil_or_empty?, nil_or_empty_string?, split, with_env

Constructor Details

#initialize(path = Dir.pwd, options = {}) ⇒ Git

Initializes a new Git bound to the repository at the specified path. Raises an error if no such repository exists. Options can specify the following:

:branch     the branch for self
:author     the author for self
+ any Grit::Repo options


255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/gitgo/git.rb', line 255

def initialize(path=Dir.pwd, options={})
  @grit = path.kind_of?(Grit::Repo) ? path : Grit::Repo.new(path, options)
  @sandbox = false
  @branch = nil
  @work_dir = path(options[:work_dir] || DEFAULT_WORK_DIR)
  @work_tree  = options[:work_tree] || File.join(work_dir, 'tmp', object_id.to_s)
  @index_file = options[:index_file] || File.join(work_dir, 'tmp', "#{object_id}.index")
  
  self.author = options[:author] || nil
  self.checkout options[:branch] || DEFAULT_BRANCH
  self.default_blob_mode = options[:default_blob_mode] || DEFAULT_BLOB_MODE
  self.default_tree_mode = options[:default_tree_mode] || DEFAULT_TREE_MODE
end

Instance Attribute Details

#branchObject (readonly)

The gitgo branch



224
225
226
# File 'lib/gitgo/git.rb', line 224

def branch
  @branch
end

#default_blob_modeObject

The default blob mode for self (see DEFAULT_BLOB_MODE)



242
243
244
# File 'lib/gitgo/git.rb', line 242

def default_blob_mode
  @default_blob_mode
end

#default_tree_modeObject

The default tree mode for self (see DEFAULT_TREE_MODE)



245
246
247
# File 'lib/gitgo/git.rb', line 245

def default_tree_mode
  @default_tree_mode
end

#gritObject (readonly)

The internal Grit::Repo



221
222
223
# File 'lib/gitgo/git.rb', line 221

def grit
  @grit
end

#headObject (readonly)

Returns the sha for the branch



230
231
232
# File 'lib/gitgo/git.rb', line 230

def head
  @head
end

#index_fileObject (readonly)

The path to the temporary index_file



239
240
241
# File 'lib/gitgo/git.rb', line 239

def index_file
  @index_file
end

#treeObject (readonly)

The in-memory working tree tracking any adds and removes



227
228
229
# File 'lib/gitgo/git.rb', line 227

def tree
  @tree
end

#work_dirObject (readonly)

The path to the instance working directory



233
234
235
# File 'lib/gitgo/git.rb', line 233

def work_dir
  @work_dir
end

#work_treeObject (readonly)

The path to the temporary working tree



236
237
238
# File 'lib/gitgo/git.rb', line 236

def work_tree
  @work_tree
end

Class Method Details

.debug(dev = $stdout) ⇒ Object

Sets up Grit to log to the device for the duration of the block. Primarily useful during debugging, and inadvisable otherwise because the logger must be shared among all Grit::Repo instances (this is a consequence of how logging is implemented in Grit).



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/gitgo/git.rb', line 170

def debug(dev=$stdout)
  current_logger = Grit.logger
  current_debug = Grit.debug
  begin
    Grit.logger = Logger.new(dev)
    Grit.debug = true
    yield
  ensure
    Grit.logger = current_logger
    Grit.debug = current_debug
  end
end

.init(path = Dir.pwd, options = {}) ⇒ Object

Creates a Git instance for path, initializing the repo if necessary.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/gitgo/git.rb', line 148

def init(path=Dir.pwd, options={})
  unless File.exists?(path)
    FileUtils.mkdir_p(path)
    
    Dir.chdir(path) do
      bare = options[:is_bare] ? true : false
      gitdir = bare || path =~ /\.git$/ ? path : File.join(path, ".git")
      
      Utils.with_env('GIT_DIR' => gitdir) do
        git = Grit::Git.new(gitdir)
        git.init({:bare => bare})
      end
    end
  end
  
  new(path, options)
end

.versionObject

Returns the git version as an array of integers like [1,6,4,2]. The version query performed once and then cached.



185
186
187
# File 'lib/gitgo/git.rb', line 185

def version
  @version ||= `git --version`.split(/\s/).last.split(".").collect {|i| i.to_i}
end

.version_ok?Boolean

Checks if the git version is compatible with GIT_VERSION. This check is performed once and then cached.

Returns:

  • (Boolean)


191
192
193
# File 'lib/gitgo/git.rb', line 191

def version_ok?
  @version_ok ||= ((GIT_VERSION <=> version) <= 0)
end

Instance Method Details

#[](path, entry = false, committed = false) ⇒ Object

Gets the content for path; either the blob data or an array of content names for a tree. Returns nil if path doesn’t exist.



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/gitgo/git.rb', line 356

def [](path, entry=false, committed=false)
  tree = committed ? commit_tree : @tree
  
  segments = split(path)
  unless basename = segments.pop
    return entry ? tree : tree.keys
  end
  
  unless tree = tree.subtree(segments)
    return nil 
  end
  
  obj = tree[basename]
  return obj if entry
  
  case obj
  when Array then get(:blob, obj[1]).data
  when Tree  then obj.keys
  else nil
  end
end

#[]=(path, content = nil) ⇒ Object

Sets content for path. The content can either be:

  • a string of content

  • a symbol sha, translated to [default_blob_mode, sha]

  • an array like [mode, sha]

  • a nil, to remove content

Note that set content is immediately stored in the repo and tracked in the in-memory working tree but not committed until commit is called.



387
388
389
390
391
392
393
394
395
# File 'lib/gitgo/git.rb', line 387

def []=(path, content=nil)
  segments = split(path)
  unless basename = segments.pop
    raise "invalid path: #{path.inspect}"
  end
  
  tree = @tree.subtree(segments, true)
  tree[basename] = convert_to_entry(content)
end

#add(paths) ⇒ Object

Adds a hash of (path, content) pairs (see AGET for valid content).



452
453
454
455
456
457
458
# File 'lib/gitgo/git.rb', line 452

def add(paths)
  paths.each_pair do |path, content|
    self[path] = content
  end
  
  self
end

#authorObject

Returns the configured author (which should be a Grit::Actor, or similar). If no author is is currently set, a default author will be determined from the git configurations.



289
290
291
292
293
294
295
# File 'lib/gitgo/git.rb', line 289

def author
  @author ||= begin
    name =  grit.config['user.name']
    email = grit.config['user.email']
    Grit::Actor.new(name, email)
  end
end

#author=(input) ⇒ Object

Sets the author. The input may be a Grit::Actor, an array like [author, email], a git-formatted author string, or nil.



299
300
301
302
303
304
305
306
# File 'lib/gitgo/git.rb', line 299

def author=(input)
  @author = case input
  when Grit::Actor, nil then input
  when Array  then Grit::Actor.new(*input)
  when String then Grit::Actor.from_string(*input)
  else raise "could not convert to Grit::Actor: #{input.class}"
  end
end

#checkout(branch = self.branch) ⇒ Object

Sets the current branch and updates tree.

Checkout does not actually checkout any files unless a block is given. In that case, the current branch will be checked out for the duration of the block into work_tree; a gitgo-specific directory distinct from the user’s working directory. Checkout with a block permits the execution of git commands that must be performed in a working directory.

Returns self.



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/gitgo/git.rb', line 577

def checkout(branch=self.branch) # :yields: working_dir
  if branch != @branch
    @branch = branch
    reset
  end
  
  if block_given?
    sandbox do |git, work_tree, index_file|
      git.read_tree({:index_output => index_file}, branch)
      git.checkout_index({:a => true})
      yield(work_tree)
    end
  end
  
  self
end

#clone(path, options = {}) ⇒ Object

Clones self into the specified path and sets up tracking of branch in the new grit. Clone was primarily implemented for testing; normally clones are managed by the user.



675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'lib/gitgo/git.rb', line 675

def clone(path, options={})
  with_env do
    grit.git.clone(options, grit.path, path)
    clone = Grit::Repo.new(path)

    if options[:bare]
      # bare origins directly copy branch heads without mapping them to
      # 'refs/remotes/origin/' (see git-clone docs). this maps the branch
      # head so the bare grit can checkout branch
      clone.git.remote({}, "add", "origin", grit.path)
      clone.git.fetch({}, "origin")
      clone.git.branch({}, "-D", branch)
    end

    # sets up branch to track the origin to enable pulls
    clone.git.branch({:track => true}, branch, "origin/#{branch}")
    self.class.new(clone, :branch => branch, :author => author)
  end
end

#commit(message, options = {}) ⇒ Object

Commits the in-memory working tree to branch with the specified message and returns the sha for the new commit. The branch is created if it doesn’t already exist. Options can specify (as symbols):

tree

The sha of the tree this commit points to (default the sha for tree, the in-memory working tree)

parents

An array of shas representing parent commits (default the current commit)

author

A Grit::Actor, or similar representing the commit author (default author)

authored_date

The authored date (default now)

committer

A Grit::Actor, or similar representing the user making the commit (default author)

committed_date

The authored date (default now)

Raises an error if there are no changes to commit.



482
483
484
485
# File 'lib/gitgo/git.rb', line 482

def commit(message, options={})
  raise "no changes to commit" if status.empty?
  commit!(message, options)
end

#commit!(message, options = {}) ⇒ Object

Same as commit but does not check if there are changes to commit, useful when you know there are changes to commit and don’t want the overhead of checking for changes.



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/gitgo/git.rb', line 490

def commit!(message, options={})
  now = Time.now
  
  sha = options.delete(:tree) || tree.write_to(self).at(1)
  parents = options.delete(:parents) || (head ? [head] : [])
  author = options[:author] || self.author
  authored_date = options[:authored_date] || now
  committer = options[:committer] || author
  committed_date = options[:committed_date] || now

  # commit format:
  #---------------------------------------------------
  #   tree sha
  #   parent sha
  #   author name <email> time_as_int zone_offset
  #   committer name <email> time_as_int zone_offset
  #   
  #   messsage
  #   
  #---------------------------------------------------
  # Note there is a trailing newline after the message.
  #
  lines = []
  lines << "tree #{sha}"
  parents.each do |parent|
    lines << "parent #{parent}"
  end
  lines << "author #{author.name} <#{author.email}> #{authored_date.strftime("%s %z")}"
  lines << "committer #{committer.name} <#{committer.email}> #{committed_date.strftime("%s %z")}"
  lines << ""
  lines << message
  lines << ""
  
  @head = set('commit', lines.join("\n"))
  grit.update_ref(branch, head)
  
  head
end

#commit_grep(pattern, treeish = head) ⇒ Object

Greps for commits with messages matching the pattern, starting at the specified treeish. Each matching commit yielded to the block.

Instead of a pattern, a hash of git-log options may be provided.



826
827
828
829
830
831
832
833
834
835
836
837
# File 'lib/gitgo/git.rb', line 826

def commit_grep(pattern, treeish=head) # :yields: commit
  options = pattern.respond_to?(:merge) ? pattern.dup : {:grep => pattern}
  options.delete_if {|key, value| nil_or_empty?(value) }
  options[:format] = "%H"
  
  sandbox do |git, work_tree, index_file|
    git.log(options, treeish).split("\n").each do |sha|
      yield grit.commit(sha)
    end
  end
  self
end

#diff_tree(a, b = "^#{a}") ⇒ Object

Retuns an array of added, deleted, and modified files keyed by ‘A’, ‘D’, and ‘M’ respectively.



734
735
736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/gitgo/git.rb', line 734

def diff_tree(a, b="^#{a}")
  sandbox do |git,w,i|
    output = git.run('', :diff_tree, '', {:r => true, :name_status => true}, [a, b])
    
    diff = {'A' => [], 'D' => [], 'M' => []}
    output.split("\n").each do |line|
      mode, path = line.split(' ', 2)
      array = diff[mode] or raise "unexpected diff output:\n#{output}"
      array << path
    end

    diff
  end
end

#fetch(remote = self.remote) ⇒ Object

Fetches from the remote.



595
596
597
598
# File 'lib/gitgo/git.rb', line 595

def fetch(remote=self.remote)
  sandbox {|git,w,i| git.fetch({}, remote) }
  self
end

#fsckObject

Performs ‘git fsck’ and returns the output.



853
854
855
856
857
858
# File 'lib/gitgo/git.rb', line 853

def fsck
  sandbox do |git, work_tree, index_file|
    stdout, stderr = git.sh("#{Grit::Git.git_binary} fsck")
    "#{stdout}#{stderr}"
  end
end

#gcObject

Performs ‘git gc’ and resets self so that grit will use the updated pack files. Returns self.



847
848
849
850
# File 'lib/gitgo/git.rb', line 847

def gc
  sandbox {|git,w,i| git.gc }
  reset(true)
end

#get(type, id) ⇒ Object

Gets the specified object, returning an instance of the appropriate Grit class. Raises an error for unknown types.



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/gitgo/git.rb', line 330

def get(type, id)
  case type.to_sym
  when :blob   then grit.blob(id)
  when :tree   then grit.tree(id)
  when :commit then grit.commit(id)
  when :tag
    
    object = grit.git.ruby_git.get_object_by_sha1(id)
    if object.type == :tag 
      Grit::Tag.new(object.tag, grit.commit(object.object))
    else
      nil
    end
  
  else raise "unknown type: #{type}"
  end
end

#grep(pattern, treeish = head) ⇒ Object

Greps for paths matching the pattern, at the specified treeish. Each matching path and blob are yielded to the block.

Instead of a pattern, a hash of grep options may be provided. The following options are allowed:

:ignore_case
:invert_match
:fixed_strings
:e


767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
# File 'lib/gitgo/git.rb', line 767

def grep(pattern, treeish=head) # :yields: path, blob
  options = pattern.respond_to?(:merge) ? pattern.dup : {:e => pattern}
  options.delete_if {|key, value| nil_or_empty?(value) }
  options = options.merge!(
    :cached => true,
    :name_only => true,
    :full_name => true
  )
  
  unless commit = grit.commit(treeish)
    raise "unknown commit: #{treeish}"
  end
  
  sandbox do |git, work_tree, index_file|
    git.read_tree({:index_output => index_file}, commit.id)
    git.grep(options).split("\n").each do |path|
      yield(path, (commit.tree / path))
    end
  end
  self
end

#ls_tree(treeish) ⇒ Object

Returns an array of paths at the specified treeish.



750
751
752
753
754
# File 'lib/gitgo/git.rb', line 750

def ls_tree(treeish)
  sandbox do |git,w,i|
    git.run('', :ls_tree, '', {:r => true, :name_only => true}, [treeish]).split("\n")
  end
end

#merge(treeish = upstream_branch) ⇒ Object

Merges the specified reference with the current branch, fast-forwarding when possible. This method does not need to checkout the branch into a working directory to perform the merge.



616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# File 'lib/gitgo/git.rb', line 616

def merge(treeish=upstream_branch)
  sandbox do |git, work_tree, index_file|
    des, src = safe_rev_parse(branch, treeish)
    base = des.nil? ? nil : git.merge_base({}, des, src).chomp("\n")
    
    case
    when base == src
      break
    when base == des
      # fast forward situation
      grit.update_ref(branch, src)
    else
      # todo: add rebase as an option
      
      git.read_tree({
        :m => true,          # merge
        :i => true,          # without a working tree
        :trivial => true,    # only merge if no file-level merges are required
        :aggressive => true, # allow resolution of removes
        :index_output => index_file
      }, base, branch, src)
      
      commit!("gitgo merge of #{treeish} into #{branch}", 
        :tree => git.write_tree.chomp("\n"),
        :parents => [des, src]
      )
    end
    
    reset
  end
  
  self
end

#merge?(treeish = upstream_branch) ⇒ Boolean

Returns true if a merge update is available for branch.

Returns:

  • (Boolean)


601
602
603
604
605
606
607
608
609
610
611
# File 'lib/gitgo/git.rb', line 601

def merge?(treeish=upstream_branch)
  sandbox do |git, work_tree, index_file|
    des, src = safe_rev_parse(branch, treeish)
    
    case
    when src.nil? then false
    when des.nil? then true
    else des != src && git.merge_base({}, des, src).chomp("\n") != src
    end
  end
end

#path(*segments) ⇒ Object

Returns the specified path relative to the git repository (ie the .git directory). With no arguments path returns the repository path.



281
282
283
284
# File 'lib/gitgo/git.rb', line 281

def path(*segments)
  segments.collect! {|segment| segment.to_s }
  File.join(grit.path, *segments)
end

#pruneObject

Peforms ‘git prune’ and returns self.



840
841
842
843
# File 'lib/gitgo/git.rb', line 840

def prune
  sandbox {|git,w,i| git.prune }
  self
end

#pull(tracking_branch = upstream_branch) ⇒ Object

Fetches the tracking branch and merges with branch. No other branches are fetched. Raises an error if given a non-tracking branch (see the Tracking, Push/Pull notes above).



663
664
665
666
667
668
669
670
# File 'lib/gitgo/git.rb', line 663

def pull(tracking_branch=upstream_branch)
  sandbox do |git, work_tree, index_file|
    remote, remote_branch = parse_tracking_branch(tracking_branch)
    git.fetch({}, remote, "#{remote_branch}:remotes/#{tracking_branch}")
    merge(tracking_branch)
  end
  reset
end

#push(tracking_branch = upstream_branch) ⇒ Object

Pushes branch to the tracking branch. No other branches are pushed. Raises an error if given a non-tracking branch (see the Tracking, Push/Pull notes above).



653
654
655
656
657
658
# File 'lib/gitgo/git.rb', line 653

def push(tracking_branch=upstream_branch)
  sandbox do |git, work_tree, index_file|
    remote, remote_branch = parse_tracking_branch(tracking_branch)
    git.push({}, remote, "#{branch}:#{remote_branch}") unless head.nil?
  end
end

#remoteObject

Returns the remote as setup by track, or origin if tracking has not been setup.



437
438
439
# File 'lib/gitgo/git.rb', line 437

def remote
  grit.config["branch.#{branch}.remote"] || 'origin'
end

#reset(full = false) ⇒ Object

Resets the working tree. Also reinitializes grit if full is specified; this can be useful after operations that change configurations or the cached packs (see gc).



532
533
534
535
536
537
538
539
# File 'lib/gitgo/git.rb', line 532

def reset(full=false)
  @grit = Grit::Repo.new(path, :is_bare => grit.bare) if full
  commit = grit.commits(branch, 1).first
  @head = commit ? commit.sha : nil
  @tree = commit_tree
  
  self
end

#resolve(id) ⇒ Object

Returns a full sha for the identifier, as determined by rev_parse. All valid sha string are returned immediately; there is no guarantee the sha will point to an object currently in the repo.

Returns nil the identifier cannot be resolved to an sha.



313
314
315
316
317
318
# File 'lib/gitgo/git.rb', line 313

def resolve(id)
  case id
  when SHA, nil then id
  else rev_parse(id).first
  end
end

#rev_list(*treeishs) ⇒ Object

Returns an array of revisions (commits) reachable from the treeish.



727
728
729
730
# File 'lib/gitgo/git.rb', line 727

def rev_list(*treeishs)
  return treeishs if treeishs.empty?
  sandbox {|git,w,i| git.run('', :rev_list, '', {}, treeishs).split("\n") }
end

#rev_parse(*args) ⇒ Object

Returns an array of shas identified by the args (ex a sha, short-sha, or treeish). Raises an error if not all args can be converted into a valid sha.

Note there is no guarantee the resulting shas indicate objects in the repository; not even ‘git rev-parse’ will do that.



701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
# File 'lib/gitgo/git.rb', line 701

def rev_parse(*args)
  return args if args.empty?
  
  sandbox do |git,w,i|
    shas = git.run('', :rev_parse, '', {}, args).split("\n")
    
    # Grit::Git#run only makes stdout available, not stderr, and so this
    # wonky check relies on the fact that git rev-parse will print the
    # unresolved ref to stdout and quit if it can't succeed. That means
    # the last printout will not look like a sha in the event of an error.
    unless shas.last.to_s =~ SHA
      raise "could not resolve to a sha: #{shas.last}"
    end
    
    shas
  end
end

#rm(*paths) ⇒ Object

Removes the content at each of the specified paths



461
462
463
464
# File 'lib/gitgo/git.rb', line 461

def rm(*paths)
  paths.each {|path| self[path] = nil }
  self
end

#safe_rev_parse(*args) ⇒ Object

Same as rev_parse but always returns an array. Arguments that cannot be converted to a valid sha will be represented by nil. This method is slower than rev_parse because it converts arguments one by one



722
723
724
# File 'lib/gitgo/git.rb', line 722

def safe_rev_parse(*args)
  args.collect! {|arg| rev_parse(arg).at(0) rescue nil }
end

#sandboxObject

Creates and sets a work tree and index file so that git will have an environment it can work in. Specifically sandbox creates an empty work_tree and index_file, the sets these ENV variables:

GIT_DIR:: set to the repo path
GIT_WORK_TREE:: work_tree,
GIT_INDEX_FILE:: index_file

Once these are set, sandbox yields grit.git, the work_tree, and index_file to the block. After the block returns, the work_tree and index_file are removed. Nested calls to sandbox will reuse the previous sandbox and yield immediately to the block.

Note that no content is checked out into work_tree or index_file by this method; that must be done as needed within the block.



890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
# File 'lib/gitgo/git.rb', line 890

def sandbox # :yields: git, work_tree, index_file
  if @sandbox
    return yield(grit.git, work_tree, index_file)
  end
  
  FileUtils.rm_r(work_tree) if File.exists?(work_tree)
  FileUtils.rm(index_file)  if File.exists?(index_file)
  
  begin
    FileUtils.mkdir_p(work_tree)
    @sandbox = true
    
    with_env(
      'GIT_DIR' => grit.path, 
      'GIT_WORK_TREE' => work_tree,
      'GIT_INDEX_FILE' => index_file
    ) do
      
      yield(grit.git, work_tree, index_file)
    end
  ensure
    FileUtils.rm_r(work_tree) if File.exists?(work_tree)
    FileUtils.rm(index_file)  if File.exists?(index_file)
    @sandbox = false
  end
end

#set(type, content) ⇒ Object

Sets an object of the specified type into the git repository and returns the object sha.



350
351
352
# File 'lib/gitgo/git.rb', line 350

def set(type, content)
  grit.git.put_raw_object(content, type.to_s)
end

#statsObject

Returns a hash of repo statistics parsed from ‘git count-objects –verbose’.



862
863
864
865
866
867
868
869
870
871
872
873
# File 'lib/gitgo/git.rb', line 862

def stats
  sandbox do |git, work_tree, index_file|
    stdout, stderr = git.sh("#{Grit::Git.git_binary} count-objects --verbose")
    stats = YAML.load(stdout)
    
    unless stats.kind_of?(Hash)
      raise stderr
    end
    
    stats
  end
end

#status(full = false) ⇒ Object

Returns a hash of (path, state) pairs indicating paths that have been added or removed. States are add/rm/mod only – renames, moves, and copies are not detected.



544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/gitgo/git.rb', line 544

def status(full=false)
  a = commit_tree.flatten
  b = tree.flatten
  
  diff = {}
  (a.keys | b.keys).collect do |key|
    a_entry = a.has_key?(key) ? a[key] : nil
    b_entry = b.has_key?(key) ? b[key] : nil
    
    change = case
    when a_entry && b_entry
      next unless a_entry != b_entry
      :mod
    when a_entry
      :rm
    when b_entry
      :add
    end
    
    diff[key] = full ? [change, a_entry || [], b_entry || []] : change
  end
  diff
end

#track(upstream_branch) ⇒ Object

Sets branch to track the specified upstream_branch. The upstream_branch must be an existing tracking branch; an error is raised if this requirement is not met (see the Tracking, Push/Pull notes above).



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/gitgo/git.rb', line 400

def track(upstream_branch)
  if upstream_branch.nil?
    # currently grit.config does not support unsetting (grit-2.0.0)
    grit.git.config({:unset => true}, "branch.#{branch}.remote")
    grit.git.config({:unset => true}, "branch.#{branch}.merge")
  else
    unless tracking_branch?(upstream_branch)
      raise "the upstream branch is not a tracking branch: #{upstream_branch}"
    end
    
    remote, remote_branch = upstream_branch.split('/', 2)
    grit.config["branch.#{branch}.remote"] = remote
    grit.config["branch.#{branch}.merge"] = "refs/heads/#{remote_branch}"
  end
end

#tracking_branch?(ref) ⇒ Boolean

Returns true if the specified ref is a tracking branch, ie it is the name of an existing remote ref.

Returns:

  • (Boolean)


443
444
445
# File 'lib/gitgo/git.rb', line 443

def tracking_branch?(ref)
  ref && grit.remotes.find {|remote| remote.name == ref }
end

#tree_grep(pattern, treeish = head) ⇒ Object

Greps for trees matching the pattern, at the specified treeish. Each matching path and tree are yielded to the block.

Instead of a pattern, a hash of grep options may be provided. The following options are allowed:

:ignore_case
:invert_match
:fixed_strings
:e


800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
# File 'lib/gitgo/git.rb', line 800

def tree_grep(pattern, treeish=head) # :yields: path, tree
  options = pattern.respond_to?(:merge) ? pattern.dup : {:e => pattern}
  options.delete_if {|key, value| nil_or_empty?(value) }
  
  unless commit = grit.commit(treeish)
    raise "unknown commit: #{treeish}"
  end
  
  sandbox do |git, work_tree, index_file|
    postfix = options.empty? ? '' : begin
      grep_options = git.transform_options(options)
      " | grep #{grep_options.join(' ')}"
    end
    
    stdout, stderr = git.sh("#{Grit::Git.git_binary} ls-tree -r --name-only #{git.e(commit.id)} #{postfix}")
    stdout.split("\n").each do |path|
      yield(path, commit.tree / path)
    end
  end
  self
end

#type(sha) ⇒ Object

Returns the type of the object identified by sha; the output of:

% git cat-file -t sha


324
325
326
# File 'lib/gitgo/git.rb', line 324

def type(sha)
  grit.git.cat_file({:t => true}, sha)
end

#upstream_branchObject

Returns the upstream_branch as setup by track. Raises an error if the ‘branch.name.merge’ config doesn’t follow the pattern ‘ref/heads/branch’ (see the Tracking, Push/Pull notes above).



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/gitgo/git.rb', line 419

def upstream_branch
  remote = grit.config["branch.#{branch}.remote"]
  merge  = grit.config["branch.#{branch}.merge"]
  
  # No remote, no merge, no tracking.
  if remote.nil? || merge.nil?
    return nil
  end
  
  unless merge =~ /^refs\/heads\/(.*)$/
    raise "invalid upstream branch"
  end
  
  "#{remote}/#{$1}"
end