Skip to content

Instantly share code, notes, and snippets.

@zserge
Created January 19, 2023 18:01
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zserge/549317af15bc3aead966df462a7d5216 to your computer and use it in GitHub Desktop.
Save zserge/549317af15bc3aead966df462a7d5216 to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"compress/zlib"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
)
type Git struct {
Dir string
Branch string
User string
Email string
}
type Hash []byte
func NewHash(b []byte) (Hash, error) {
b, err := hex.DecodeString(strings.TrimSpace(string(b)))
if err != nil {
return nil, err
}
return Hash(b), nil
}
func (h Hash) String() string { return hex.EncodeToString(h) }
type Tree struct {
Blobs []Blob
Hash Hash
}
type Blob struct {
Name string
Hash Hash
}
type Commit struct {
Msg string
Parent Hash
Tree Hash
Hash Hash
}
func (g *Git) Init() error {
for _, dirs := range [][]string{
{"objects", "info"},
{"objects", "pack"},
{"refs", "heads"},
{"refs", "tags"},
} {
if err := os.MkdirAll(filepath.Join(g.Dir, filepath.Join(dirs...)), 0755); err != nil {
return err
}
}
return os.WriteFile(filepath.Join(g.Dir, "HEAD"), []byte("ref: refs/heads/"+g.Branch), 0644)
}
func (g *Git) fmt(format string, args ...any) []byte {
return []byte(fmt.Sprintf(format, args...))
}
func (g *Git) write(objType string, b []byte) (Hash, error) {
b = append(g.fmt("%s %d\x00", objType, len(b)), b...)
bz, err := zip(b)
if err != nil {
return nil, err
}
sum := sha1.Sum(b)
hash := hex.EncodeToString(sum[:])
dir := filepath.Join(g.Dir, "objects", hash[:2])
obj := filepath.Join(dir, hash[2:])
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
return sum[:], os.WriteFile(obj, bz, 0644)
}
func (g *Git) read(objType string, hash Hash) ([]byte, error) {
h := hash.String()
dir := filepath.Join(g.Dir, "objects", h[:2])
obj := filepath.Join(dir, h[2:])
b, err := os.ReadFile(obj)
if err != nil {
return nil, err
}
b, err = unzip(b)
if err != nil {
return nil, err
}
if !bytes.HasPrefix(b, []byte(objType+" ")) {
return nil, fmt.Errorf("not a %s object", objType)
}
n := bytes.IndexByte(b, 0)
if n < 0 {
return nil, fmt.Errorf("invalid %s", objType)
}
return b[n+1:], nil
}
func (g *Git) AddBlob(data []byte) (Hash, error) {
return g.write("blob", data)
}
func (g *Git) AddTree(filename string, filedata []byte) (Hash, error) {
hash, err := g.AddBlob(filedata)
if err != nil {
return nil, err
}
content := append(g.fmt("100644 %s\x00", filename), hash...)
return g.write("tree", content)
}
func (g *Git) AddCommit(filename string, data []byte, parentHash Hash, msg string) (Hash, error) {
hash, err := g.AddTree(filename, data)
if err != nil {
return nil, err
}
parent := ""
if parentHash != nil {
parent = fmt.Sprintf("parent %s\n", parentHash.String())
}
t := time.Now().Unix()
content := g.fmt("tree %s\n%sauthor %s <%s> %d +0000\ncommitter %s <%s> %d +0000\n\n%s\n",
hash, parent, g.User, g.Email, t, g.User, g.Email, t, msg)
b, err := g.write("commit", content)
if err != nil {
return nil, err
}
return b, g.SetHead(b)
}
func (g *Git) SetHead(h Hash) error {
return os.WriteFile(filepath.Join(g.Dir, "refs", "heads", g.Branch), []byte(h.String()), 0644)
}
func (g *Git) Head() (Hash, error) {
b, err := os.ReadFile(filepath.Join(g.Dir, "refs", "heads", g.Branch))
if err != nil {
return nil, err
}
return NewHash(b)
}
func (g *Git) Blob(hash []byte) ([]byte, error) {
return g.read("blob", hash)
}
func (g *Git) Tree(hash []byte) (tree *Tree, err error) {
tree = &Tree{Hash: hash}
b, err := g.read("tree", hash)
if err != nil {
return nil, err
}
for {
parts := bytes.SplitN(b, []byte{0}, 2)
fields := bytes.SplitN(parts[0], []byte{' '}, 2)
tree.Blobs = append(tree.Blobs, Blob{Name: string(fields[1]), Hash: parts[1][0:20]})
b = parts[1][20:]
if len(parts[1]) == 20 {
break
}
}
return tree, nil
}
func (g *Git) Commit(hash []byte) (ci Commit, err error) {
ci = Commit{Hash: hash}
b, err := g.read("commit", hash)
if err != nil {
return ci, err
}
lines := bytes.Split(b, []byte{'\n'})
for i, line := range lines {
if len(line) == 0 {
ci.Msg = string(bytes.Join(append(lines[i+1:]), []byte{'\n'}))
return ci, nil
}
parts := bytes.SplitN(line, []byte{' '}, 2)
switch string(parts[0]) {
case "tree":
ci.Tree, err = hex.DecodeString(string(parts[1]))
if err != nil {
return ci, err
}
case "parent":
ci.Parent, err = hex.DecodeString(string(parts[1]))
if err != nil {
return ci, err
}
}
}
return ci, nil
}
func (g *Git) Log() (commits []Commit, err error) {
hash, err := g.Head()
if err != nil {
return nil, fmt.Errorf("head: %v", err)
}
for hash != nil {
ci, err := g.Commit(hash)
if err != nil {
return nil, err
}
commits = append(commits, ci)
hash = ci.Parent
}
return commits, nil
}
func zip(content []byte) ([]byte, error) {
b := &bytes.Buffer{}
zw, _ := zlib.NewWriterLevel(b, zlib.NoCompression)
if _, err := zw.Write(content); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func unzip(content []byte) ([]byte, error) {
zw, err := zlib.NewReader(bytes.NewBuffer(content))
if err != nil {
return nil, err
}
defer zw.Close()
return io.ReadAll(zw)
}
func main() {
log.SetFlags(0)
g := &Git{Dir: ".git", Branch: "main", User: "nanogit", Email: "nanogit@example.com"}
if len(os.Args) < 2 {
log.Fatal("USAGE: nanogit <init|log|ci|co>")
}
switch os.Args[1] {
case "init":
if err := g.Init(); err != nil {
log.Fatal(err)
}
case "log":
history, err := g.Log()
if err != nil {
log.Fatal(err)
}
for _, h := range history {
fmt.Println(h.Hash, strings.TrimSpace(h.Msg))
}
case "ci":
b, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
parent, _ := g.Head()
msg := "fix"
if len(os.Args) == 3 {
msg = os.Args[2]
}
if _, err := g.AddCommit("file.txt", b, parent, msg); err != nil {
log.Fatal(err)
}
case "co":
if len(os.Args) != 3 {
log.Fatal("expected commit hash")
}
history, err := g.Log()
if err != nil {
log.Fatal(err)
}
for _, h := range history {
if strings.HasPrefix(h.Hash.String(), os.Args[2]) {
tree, err := g.Tree(h.Tree)
if err != nil {
log.Fatal(err)
}
for _, b := range tree.Blobs {
content, err := g.Blob(b.Hash)
if err != nil {
log.Fatal(err)
}
log.Println(string(content))
}
return
}
}
log.Fatal("unknown commit hash:", os.Args[2])
default:
log.Fatal("unknown command:", os.Args[1])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment