rubylibgit2rugged

Why am I seeing inaccurate diff deltas in libgit2 / rugged?


When using rugged to remove files to stage a commit, the diff for the commit comes out with false information about which files were deleted. The files that are supposed to be there are still actually there and the resulting state of the project is correct. It's just that the diff is reporting incorrectly.

I may be misunderstanding / misusing Rugged::Tree::Builder, but other than the diffs, it everything seems to come out correctly. Note that I am leaving an empty tree object here, but I don't see why that should affect this.

Project structure:

demo-project/ └── MasterDir ├── Directory1 │   ├── File1 │   ├── File2 │   └── File3 ├── Directory2 │   ├── File4 │   └── File5 ├── Directory3 │   └── File6 └── Directory4 └── File7

Call sequence

require 'rugged'

repo = Rugged::Repository.new("/Users/davetakahashi/demo-project")

# get the tree for "MasterDir/Directory1"
tree = repo.lookup(repo.head.target.tree.path("MasterDir/Directory1")[:oid])

# initialize a builder with the tree
builder = Rugged::Tree::Builder.new(tree)

# remove the files
builder.remove("File1")

builder.remove("File2")

builder.remove("File3")

# write the tree, saving the oid to write to parent tree
empty_directory_1_oid = builder.write(repo)

# get the tree for "MasterDir"
parent_tree = repo.lookup(repo.head.target.tree.path("MasterDir")[:oid])

# initialize a builder with it
builder = Rugged::Tree::Builder.new(parent_tree)

# add the updated entry for "Directory1"
builder << {:oid => empty_directory_1_oid, :filemode => 040000, :name => "Directory1", :type => :tree}

# write to repo, save oid
new_master_dir_oid = builder.write(repo)

# oid for "MasterDir" @ HEAD
old_master_dir_oid = repo.head.target.tree.path("MasterDir")[:oid]

# new MasterDir tree with empty Directory1
new_thing = repo.lookup(new_master_dir_oid)

# original MasterDir tree 
old_thing = repo.lookup(old_master_dir_oid)

# diff shows that every file in MasterDir was deleted
old_thing.diff(new_thing).deltas

irb session:

  [demo-project (master)]$ irb

  2.1.3 :001 > require 'rugged'
   => true 

  2.1.3 :002 > repo = Rugged::Repository.new("/Users/davetakahashi/demo-project")
   => #<Rugged::Repository:70270137753220 {path: "/Users/davetakahashi/demo-project/.git/"}> 

  2.1.3 :003 > tree = repo.lookup(repo.head.target.tree.path("MasterDir/Directory1")[:oid])
   => #<Rugged::Tree:70270137716900 {oid: abdee7bc13e432263b8da96776faa870a0b705b5}>
    <"File1" edd02898ce2deab5b06f9af79c97225427aa0202>
    <"File2" 2289c92f857605706b5bb1405e8b62b188777f36>
    <"File3" 248bce651b8c13085beb99c77b63d83a9866fbe5>

  2.1.3 :004 > builder = Rugged::Tree::Builder.new(tree)
   => #<Rugged::Tree::Builder:0x007fd215310a08> 

  2.1.3 :005 > builder.remove("File1")
   => true 

  2.1.3 :006 > builder.remove("File2")
   => true 

  2.1.3 :007 > builder.remove("File3")
   => true 

  2.1.3 :008 > empty_directory_1_oid = builder.write(repo)
   => "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 

  2.1.3 :009 > parent_tree = repo.lookup(repo.head.target.tree.path("MasterDir")[:oid])
   => #<Rugged::Tree:70270137598260 {oid: 67e1a15042779fd7a0bb5f55517d15acb407a36a}>
    <"Directory1" abdee7bc13e432263b8da96776faa870a0b705b5>
    <"Directory2" ba23af1963e685c6385440ea1d76d92eb0fc4728>
    <"Directory3" eaea4f258ad3488468c52e068994c6289c9ee4ea>
    <"Directory4" 3344b4aa8fec8f28d189d5d2f05285e60f26cbd4>

  2.1.3 :010 > builder = Rugged::Tree::Builder.new(parent_tree)
   => #<Rugged::Tree::Builder:0x007fd2152d2730> 

  2.1.3 :011 > builder << {:oid => empty_directory_1_oid, :filemode => 040000, :name => "Directory1", :type => :tree}
   => nil 

  2.1.3 :012 > new_master_dir_oid = builder.write(repo)
   => "a1a2ebae492b845684bb53a01981b5710a9d1814" 

  2.1.3 :013 > old_master_dir_oid = repo.head.target.tree.path("MasterDir")[:oid]
   => "67e1a15042779fd7a0bb5f55517d15acb407a36a" 

  2.1.3 :014 > new_thing = repo.lookup(new_master_dir_oid)
   => #<Rugged::Tree:70270137454060 {oid: a1a2ebae492b845684bb53a01981b5710a9d1814}>
    <"Directory1" 4b825dc642cb6eb9a060e54bf8d69288fbee4904>
    <"Directory2" ba23af1963e685c6385440ea1d76d92eb0fc4728>
    <"Directory3" eaea4f258ad3488468c52e068994c6289c9ee4ea>
    <"Directory4" 3344b4aa8fec8f28d189d5d2f05285e60f26cbd4>

  2.1.3 :015 > old_thing = repo.lookup(old_master_dir_oid)
   => #<Rugged::Tree:70270137436000 {oid: 67e1a15042779fd7a0bb5f55517d15acb407a36a}>
    <"Directory1" abdee7bc13e432263b8da96776faa870a0b705b5>
    <"Directory2" ba23af1963e685c6385440ea1d76d92eb0fc4728>
    <"Directory3" eaea4f258ad3488468c52e068994c6289c9ee4ea>
    <"Directory4" 3344b4aa8fec8f28d189d5d2f05285e60f26cbd4>

  2.1.3 :016 > old_thing.diff(new_thing).deltas
   => [#<Rugged::Diff::Delta:70270137419160 {old_file: {:oid=>"edd02898ce2deab5b06f9af79c97225427aa0202", :path=>"Directory1/File1", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory1/File1", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>, 
       #<Rugged::Diff::Delta:70270137418980 {old_file: {:oid=>"2289c92f857605706b5bb1405e8b62b188777f36", :path=>"Directory1/File2", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory1/File2", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>, 
       #<Rugged::Diff::Delta:70270137418840 {old_file: {:oid=>"248bce651b8c13085beb99c77b63d83a9866fbe5", :path=>"Directory1/File3", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory1/File3", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>, 
       #<Rugged::Diff::Delta:70270137418700 {old_file: {:oid=>"252bae4bbacebe7a44e02b688960ccaff0515ee1", :path=>"Directory2/File4", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory2/File4", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>, 
       #<Rugged::Diff::Delta:70270137418560 {old_file: {:oid=>"a4b3a268c46f5a5340ab8a59fc74ebdc38d9a74a", :path=>"Directory2/File5", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory2/File5", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>, 
       #<Rugged::Diff::Delta:70270137418420 {old_file: {:oid=>"1928ee6db9fcd32139f8b7bd315766d645aec086", :path=>"Directory3/File6", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory3/File6", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>, 
       #<Rugged::Diff::Delta:70270137418280 {old_file: {:oid=>"d4fc3289375ca9a25b52badad06540fe31f9ed73", :path=>"Directory4/File7", :size=>0, :flags=>4, :mode=>33188}, new_file: {:oid=>"0000000000000000000000000000000000000000", :path=>"Directory4/File7", :size=>0, :flags=>4, :mode=>0}, similarity: 0, status: :deleted>] 

So even though I've only removed 3 files, the deltas show I've deleted everything (7 files). I've tried all the options that are available on diff(), so I'm guessing I'm missing a step in using Tree::Builder. Am I not allowed to leave the empty tree object?


Solution

  • I suspect this is because you're inserting the empty tree into another tree, which is not something that git would do. If you want to remove 'MasterDir/Directory1', then do so and it should work well.

    The usual way of handling multi-level trees is via the Index which takes care of removing empty trees.