This document covers working with Git objects: commits, trees, blobs, and tags. It includes reading objects, creating objects, iterating through collections, and computing diffs.
Git has four main object types:
All objects are stored in the object database and referenced by their SHA-1/SHA-256 hash.
type Object interface {
ID() plumbing.Hash
Type() plumbing.ObjectType
Decode(plumbing.EncodedObject) error
Encode(plumbing.EncodedObject) error
}type Commit struct {
Hash plumbing.Hash
Author Signature
Committer Signature
MergeTag string
PGPSignature string
Message string
TreeHash plumbing.Hash
ParentHashes []plumbing.Hash
Encoding MessageEncoding
ExtraHeaders []ExtraHeader
}
type Signature struct {
Name string
Email string
When time.Time
}func (r *Repository) CommitObject(h plumbing.Hash) (*object.Commit, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Get specific commit
hash := plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")
commit, err := r.CommitObject(hash)
if err != nil {
panic(err)
}
// Access commit fields
fmt.Println("Author:", commit.Author.Name)
fmt.Println("Email:", commit.Author.Email)
fmt.Println("Date:", commit.Author.When)
fmt.Println("Message:", commit.Message)
fmt.Println("Tree:", commit.TreeHash)
fmt.Println("Parents:", commit.ParentHashes)
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Get HEAD reference
ref, err := r.Head()
if err != nil {
panic(err)
}
// Get commit at HEAD
commit, err := r.CommitObject(ref.Hash())
if err != nil {
panic(err)
}
fmt.Println(commit)
}func (c *Commit) Tree() (*Tree, error)
func (c *Commit) Parents() CommitIter
func (c *Commit) NumParents() int
func (c *Commit) Parent(n int) (*Commit, error)
func (c *Commit) File(path string) (*File, error)
func (c *Commit) Files() (*FileIter, error)
func (c *Commit) Patch(to *Commit) (*Patch, error)
func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error)
func (c *Commit) String() stringpackage main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
// Get commit's root tree
tree, err := commit.Tree()
if err != nil {
panic(err)
}
fmt.Println("Tree hash:", tree.Hash)
fmt.Println("Tree entries:", len(tree.Entries))
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
// Get specific file from commit
file, err := commit.File("README.md")
if err != nil {
panic(err)
}
// Read file contents
contents, err := file.Contents()
if err != nil {
panic(err)
}
fmt.Println("README.md contents:")
fmt.Println(contents)
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
// Iterate all files in commit
files, err := commit.Files()
if err != nil {
panic(err)
}
err = files.ForEach(func(f *object.File) error {
fmt.Printf("%s (%s)\n", f.Name, f.Mode)
return nil
})
if err != nil {
panic(err)
}
files.Close()
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
// Number of parents
fmt.Println("Parent count:", commit.NumParents())
// Get specific parent
if commit.NumParents() > 0 {
parent, err := commit.Parent(0)
if err != nil {
panic(err)
}
fmt.Println("First parent:", parent.Hash)
}
// Iterate all parents
parents := commit.Parents()
err := parents.ForEach(func(p *object.Commit) error {
fmt.Println("Parent:", p.Hash, p.Message)
return nil
})
if err != nil {
panic(err)
}
parents.Close()
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
// Get parent commit
parent, _ := commit.Parent(0)
// Get patch from parent to commit
patch, err := parent.Patch(commit)
if err != nil {
panic(err)
}
fmt.Println(patch)
// Get statistics
stats := patch.Stats()
fmt.Println("\nStatistics:")
for _, stat := range stats {
fmt.Printf("%s: +%d -%d\n", stat.Name, stat.Addition, stat.Deletion)
}
}func (r *Repository) Log(o *LogOptions) (object.CommitIter, error)
type LogOptions struct {
From plumbing.Hash
Order LogOrder
FileName *string
PathFilter func(string) bool
All bool
Since *time.Time
Until *time.Time
}
type LogOrder int
const (
LogOrderDefault LogOrder = iota
LogOrderDFS // Depth-first search
LogOrderDFSPost // DFS post-order
LogOrderBSF // Breadth-first search
LogOrderCommitterTime // Sort by committer time
)Example - Basic Log:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
ref, err := r.Head()
if err != nil {
panic(err)
}
// Get commit history from HEAD
cIter, err := r.Log(&git.LogOptions{From: ref.Hash()})
if err != nil {
panic(err)
}
// Iterate commits
err = cIter.ForEach(func(c *object.Commit) error {
fmt.Printf("%s - %s\n", c.Hash[:7], c.Message)
return nil
})
if err != nil {
panic(err)
}
cIter.Close()
}Example - Log with Time Range:
package main
import (
"fmt"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
// Get commits from last 30 days
since := time.Now().AddDate(0, 0, -30)
cIter, err := r.Log(&git.LogOptions{
From: ref.Hash(),
Since: &since,
})
if err != nil {
panic(err)
}
err = cIter.ForEach(func(c *object.Commit) error {
fmt.Printf("%s: %s\n", c.Author.When.Format("2006-01-02"), c.Message)
return nil
})
if err != nil {
panic(err)
}
cIter.Close()
}Example - Log for Specific File:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
filename := "README.md"
cIter, err := r.Log(&git.LogOptions{
From: ref.Hash(),
FileName: &filename,
})
if err != nil {
panic(err)
}
fmt.Println("Commits affecting README.md:")
err = cIter.ForEach(func(c *object.Commit) error {
fmt.Printf("%s - %s\n", c.Hash[:7], c.Message)
return nil
})
if err != nil {
panic(err)
}
cIter.Close()
}Example - Log with Path Filter:
package main
import (
"fmt"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
// Only commits affecting .go files
cIter, err := r.Log(&git.LogOptions{
From: ref.Hash(),
PathFilter: func(path string) bool {
return strings.HasSuffix(path, ".go")
},
})
if err != nil {
panic(err)
}
err = cIter.ForEach(func(c *object.Commit) error {
fmt.Printf("%s - %s\n", c.Hash[:7], c.Message)
return nil
})
if err != nil {
panic(err)
}
cIter.Close()
}func (r *Repository) CommitObjects() (object.CommitIter, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Iterate all commit objects in repository
cIter, err := r.CommitObjects()
if err != nil {
panic(err)
}
var count int
err = cIter.ForEach(func(c *object.Commit) error {
count++
return nil
})
if err != nil {
panic(err)
}
fmt.Printf("Total commits: %d\n", count)
cIter.Close()
}type Tree struct {
Entries []TreeEntry
Hash plumbing.Hash
}
type TreeEntry struct {
Name string
Mode filemode.FileMode
Hash plumbing.Hash
}func (r *Repository) TreeObject(h plumbing.Hash) (*object.Tree, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
// Get tree from commit
tree, err := r.TreeObject(commit.TreeHash)
if err != nil {
panic(err)
}
// List entries
for _, entry := range tree.Entries {
fmt.Printf("%s %s %s\n", entry.Mode, entry.Hash, entry.Name)
}
}func (t *Tree) File(path string) (*File, error)
func (t *Tree) Size(path string) (int64, error)
func (t *Tree) Tree(path string) (*Tree, error)
func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error)
func (t *Tree) FindEntry(path string) (*TreeEntry, error)
func (t *Tree) Files() *FileIter
func (t *Tree) Diff(ctx context.Context, to *Tree) (Changes, error)package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
tree, _ := commit.Tree()
// Get file from tree
file, err := tree.File("src/main.go")
if err != nil {
panic(err)
}
contents, _ := file.Contents()
fmt.Println(contents)
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
tree, _ := commit.Tree()
// Get subdirectory tree
subtree, err := tree.Tree("src")
if err != nil {
panic(err)
}
fmt.Println("Files in src/:")
for _, entry := range subtree.Entries {
fmt.Println(" ", entry.Name)
}
}package main
import (
"context"
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
parent, _ := commit.Parent(0)
parentTree, _ := parent.Tree()
currentTree, _ := commit.Tree()
// Get changes between trees
changes, err := parentTree.Diff(context.Background(), currentTree)
if err != nil {
panic(err)
}
for _, change := range changes {
action := change.Action()
name := change.Name()
fmt.Printf("%s: %s\n", actionString(action), name)
}
}
func actionString(a object.Action) string {
switch a {
case object.Insert:
return "Add"
case object.Delete:
return "Delete"
case object.Modify:
return "Modify"
default:
return "Unknown"
}
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
tree, _ := commit.Tree()
// Iterate all files recursively
files := tree.Files()
err := files.ForEach(func(f *object.File) error {
fmt.Printf("%s (%d bytes)\n", f.Name, f.Blob.Size)
return nil
})
if err != nil {
panic(err)
}
files.Close()
}type Blob struct {
Hash plumbing.Hash
Size int64
}
func (b *Blob) Reader() (io.ReadCloser, error)func (r *Repository) BlobObject(h plumbing.Hash) (*object.Blob, error)Example:
package main
import (
"fmt"
"io"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Get specific blob
hash := plumbing.NewHash("d8ca47fe1cb8d5c7a4e62b9f20c4e30ed5d69ad6")
blob, err := r.BlobObject(hash)
if err != nil {
panic(err)
}
fmt.Println("Blob size:", blob.Size, "bytes")
// Read blob contents
reader, err := blob.Reader()
if err != nil {
panic(err)
}
defer reader.Close()
contents, _ := io.ReadAll(reader)
fmt.Println(string(contents))
}func (r *Repository) BlobObjects() (*object.BlobIter, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Iterate all blobs
bIter, err := r.BlobObjects()
if err != nil {
panic(err)
}
var totalSize int64
err = bIter.ForEach(func(b *object.Blob) error {
totalSize += b.Size
return nil
})
if err != nil {
panic(err)
}
fmt.Printf("Total blob size: %d bytes\n", totalSize)
bIter.Close()
}The File type combines a blob with its path and mode information.
type File struct {
Name string
Mode filemode.FileMode
Blob Blob
}
func (f *File) Contents() (string, error)
func (f *File) Reader() (io.ReadCloser, error)
func (f *File) Lines() ([]string, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
file, err := commit.File("README.md")
if err != nil {
panic(err)
}
fmt.Println("Name:", file.Name)
fmt.Println("Mode:", file.Mode)
fmt.Println("Size:", file.Blob.Size)
// Get contents as string
contents, _ := file.Contents()
fmt.Println("\nContents:")
fmt.Println(contents)
// Get contents as lines
lines, _ := file.Lines()
fmt.Printf("\nLine count: %d\n", len(lines))
}type Tag struct {
Hash plumbing.Hash
Name string
Tagger Signature
Message string
PGPSignature string
TargetType plumbing.ObjectType
Target plumbing.Hash
}
func (t *Tag) Object() (Object, error)func (r *Repository) TagObject(h plumbing.Hash) (*object.Tag, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Get annotated tag
hash := plumbing.NewHash("abc123...")
tag, err := r.TagObject(hash)
if err != nil {
panic(err)
}
fmt.Println("Tag name:", tag.Name)
fmt.Println("Tagger:", tag.Tagger.Name)
fmt.Println("Message:", tag.Message)
fmt.Println("Target:", tag.Target)
fmt.Println("Target type:", tag.TargetType)
}package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
hash := plumbing.NewHash("abc123...")
tag, _ := r.TagObject(hash)
// Get the tagged object
obj, err := tag.Object()
if err != nil {
panic(err)
}
// Usually tags point to commits
if commit, ok := obj.(*object.Commit); ok {
fmt.Println("Tagged commit:", commit.Hash)
fmt.Println("Message:", commit.Message)
}
}func (r *Repository) TagObjects() (*object.TagIter, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Iterate annotated tags
tIter, err := r.TagObjects()
if err != nil {
panic(err)
}
err = tIter.ForEach(func(t *object.Tag) error {
fmt.Printf("%s: %s\n", t.Name, t.Message)
return nil
})
if err != nil {
panic(err)
}
tIter.Close()
}type Change struct {
From ChangeEntry
To ChangeEntry
}
type ChangeEntry struct {
Name string
Tree plumbing.Hash
TreeEntry TreeEntry
}
type Action int
const (
Insert Action = iota
Delete
Modify
Copy
)
func (c *Change) Action() Action
func (c *Change) Name() string
func (c *Change) Files() ([]*File, error)
func (c *Change) String() stringtype Patch struct {
// contains filtered or unexported fields
}
func (p *Patch) FilePatches() []FilePatch
func (p *Patch) String() string
func (p *Patch) Stats() FileStats
func (p *Patch) Message() stringExample - Generate Patch:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
parent, _ := commit.Parent(0)
// Generate patch
patch, err := parent.Patch(commit)
if err != nil {
panic(err)
}
// Print unified diff
fmt.Println(patch)
// Get file patches
filePatches := patch.FilePatches()
fmt.Printf("\n%d files changed\n", len(filePatches))
// Get statistics
stats := patch.Stats()
for _, stat := range stats {
fmt.Printf("%s: +%d -%d\n", stat.Name, stat.Addition, stat.Deletion)
}
}func (r *Repository) Object(t plumbing.ObjectType, h plumbing.Hash) (object.Object, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
hash := plumbing.NewHash("abc123...")
// Get object of any type
obj, err := r.Object(plumbing.AnyObject, hash)
if err != nil {
panic(err)
}
// Type switch on object
switch o := obj.(type) {
case *object.Commit:
fmt.Println("Commit:", o.Message)
case *object.Tree:
fmt.Println("Tree with", len(o.Entries), "entries")
case *object.Blob:
fmt.Println("Blob size:", o.Size)
case *object.Tag:
fmt.Println("Tag:", o.Name)
}
}func (r *Repository) Objects() (*object.ObjectIter, error)Example:
package main
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, err := git.PlainOpen("/tmp/repo")
if err != nil {
panic(err)
}
// Iterate all objects
oIter, err := r.Objects()
if err != nil {
panic(err)
}
counts := map[plumbing.ObjectType]int{}
err = oIter.ForEach(func(o object.Object) error {
counts[o.Type()]++
return nil
})
if err != nil {
panic(err)
}
fmt.Println("Object counts:")
fmt.Printf(" Commits: %d\n", counts[plumbing.CommitObject])
fmt.Printf(" Trees: %d\n", counts[plumbing.TreeObject])
fmt.Printf(" Blobs: %d\n", counts[plumbing.BlobObject])
fmt.Printf(" Tags: %d\n", counts[plumbing.TagObject])
oIter.Close()
}package main
import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
cIter, _ := r.Log(&git.LogOptions{From: ref.Hash()})
defer cIter.Close() // Always close iterators
_ = cIter.ForEach(func(c *object.Commit) error {
// Process commit
return nil
})
}package main
import (
"context"
"time"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
parent, _ := commit.Parent(0)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
patch, err := parent.PatchContext(ctx, commit)
if err != nil {
panic(err)
}
_ = patch
}package main
import (
"io"
"github.com/go-git/go-git/v5"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
ref, _ := r.Head()
commit, _ := r.CommitObject(ref.Hash())
file, _ := commit.File("largefile.bin")
// For large files, use Reader instead of Contents()
reader, _ := file.Reader()
defer reader.Close()
// Process in chunks
buf := make([]byte, 4096)
for {
n, err := reader.Read(buf)
if err == io.EOF {
break
}
// Process buf[:n]
}
}package main
import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
func main() {
r, _ := git.PlainOpen("/tmp/repo")
hash := plumbing.NewHash("abc123...")
// Get commit, check for error
commit, err := r.CommitObject(hash)
if err == plumbing.ErrObjectNotFound {
println("Commit not found")
return
} else if err != nil {
panic(err)
}
_ = commit
}var (
ErrObjectNotFound = errors.New("object not found")
ErrInvalidType = errors.New("invalid object type")
ErrUnsupportedObject = errors.New("unsupported object type")
ErrMaxTreeDepth = errors.New("maximum tree depth exceeded")
ErrFileNotFound = errors.New("file not found")
ErrDirectoryNotFound = errors.New("directory not found")
ErrEntryNotFound = errors.New("entry not found")
ErrEntriesNotSorted = errors.New("tree entries not sorted")
ErrParentNotFound = errors.New("parent not found")
)