Created
January 19, 2023 18:01
-
-
Save zserge/549317af15bc3aead966df462a7d5216 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: "[email protected]"} | |
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