Skip to content

Instantly share code, notes, and snippets.

@umurgdk
Created March 18, 2019 05:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save umurgdk/04a6fedc2595706cf2d9c7148912d138 to your computer and use it in GitHub Desktop.
Save umurgdk/04a6fedc2595706cf2d9c7148912d138 to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"crypto/sha1"
"encoding/gob"
"flag"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"sync"
"time"
)
type Context struct {
dir string
HEAD string
Perm os.FileMode
}
type Object struct {
Typ string
Data []byte
}
type Commit struct {
Parent string
Author string
CreatedAt time.Time
Index []string
}
type Index []string
func NewContext(dir string) *Context {
head, _ := ioutil.ReadFile(filepath.Join(dir, "HEAD"))
return &Context{dir, string(head), 0640}
}
func (c *Context) objsDir() string {
return path.Join(c.dir, "objects")
}
func (c *Context) PutObject(typ string, data []byte) (string, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(Object{typ, data}); err != nil {
return "", err
}
sum := sha1.Sum(buf.Bytes())
hash := fmt.Sprintf("%x", sum)
fileName := filepath.Join(c.objsDir(), hash)
err := ioutil.WriteFile(fileName, buf.Bytes(), c.Perm)
return hash, err
}
func (c *Context) ReadObject(hash string) (Object, error) {
filename := filepath.Join(c.objsDir(), hash)
file, err := os.Open(filename)
if err != nil {
return Object{}, err
}
defer file.Close()
var obj Object
dec := gob.NewDecoder(file)
if err := dec.Decode(&obj); err != nil {
return Object{}, err
}
return obj, nil
}
func (c *Context) PutCommit(commit Commit) (string, error) {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(commit); err != nil {
return "", err
}
return c.PutObject("commit", buf.Bytes())
}
func (c *Context) ReadCommit(hash string) (Commit, error) {
obj, err := c.ReadObject(hash)
if err != nil {
return Commit{}, err
}
if obj.Typ != "commit" {
return Commit{}, fmt.Errorf("invalid blob: %s, expected commit, got %s", hash, obj.Typ)
}
return Obj2Commit(obj)
}
func (c *Context) WriteHead(hash string) error {
fileName := filepath.Join(c.dir, "HEAD")
if err := ioutil.WriteFile(fileName, []byte(hash), c.Perm); err != nil {
return err
}
c.HEAD = hash
return nil
}
func (c *Context) History() History {
return History{
ctx: c,
Hash: c.HEAD,
Commit: Commit{Parent: c.HEAD},
Error: nil,
}
}
func Obj2Commit(obj Object) (Commit, error) {
var commit Commit
err := gob.NewDecoder(bytes.NewReader(obj.Data)).Decode(&commit)
return commit, err
}
type History struct {
ctx *Context
Hash string
Commit Commit
Error error
}
func (h *History) Parent() bool {
if h.Error != nil {
return false
}
if h.Commit.Parent == "" {
return false
}
parent, err := h.ctx.ReadCommit(h.Commit.Parent)
if err != nil {
h.Error = err
return false
}
h.Hash = h.Commit.Parent
h.Commit = parent
return true
}
func printUsage() {
fmt.Printf(`Usage: %s task [arguments]
Tasks:
log Print the history
put <type> <path, [path]> Puts a file to the storage
read <hash> Read an object
commit <author> <hash,[hash]> Commits given list of hashes`, os.Args[0])
println()
}
func main() {
flag.Usage = printUsage
flag.Parse()
if flag.NArg() == 0 {
flag.Usage()
os.Exit(-1)
}
os.MkdirAll("_dolap/objects", 0750)
ctx := NewContext("_dolap")
var err error
switch flag.Arg(0) {
case "log":
err = cmdLog(ctx)
case "put":
if flag.NArg() < 3 {
flag.Usage()
os.Exit(-1)
}
err = cmdPut(ctx, flag.Arg(1), flag.Args()[2:])
case "read":
if flag.NArg() != 2 {
flag.Usage()
os.Exit(-1)
}
err = cmdRead(ctx, flag.Arg(1))
case "commit":
if flag.NArg() < 3 {
flag.Usage()
os.Exit(-1)
}
err = cmdCommit(ctx, flag.Arg(1), flag.Args()[2:])
}
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
func cmdPut(ctx *Context, typ string, files []string) error {
errors := make(chan error, len(files))
fileContents := make([][]byte, len(files))
var wg sync.WaitGroup
for i, filename := range files {
wg.Add(1)
go func(i int, filename string) {
defer wg.Done()
bytes, err := ioutil.ReadFile(filename)
if err != nil {
errors <- err
return
}
fileContents[i] = bytes
}(i, filename)
}
wg.Wait()
close(errors)
if err := <-errors; err != nil {
return err
}
wg = sync.WaitGroup{}
wg.Add(len(fileContents))
errors = make(chan error, len(fileContents))
hashes := make([]string, len(fileContents))
for i, fileContent := range fileContents {
go func(i int, fileContent []byte) {
defer wg.Done()
hash, err := ctx.PutObject(typ, fileContent)
if err != nil {
errors <- err
return
}
hashes[i] = hash
}(i, fileContent)
}
wg.Wait()
close(errors)
if err := <-errors; err != nil {
return err
}
for _, hash := range hashes {
println(hash)
}
return nil
}
func cmdRead(ctx *Context, hash string) error {
obj, err := ctx.ReadObject(hash)
if err != nil {
return err
}
switch obj.Typ {
case "commit":
commit, err := Obj2Commit(obj)
if err != nil {
return err
}
fmt.Printf("%s %v %s\n", commit.Author, commit.CreatedAt, hash)
fmt.Printf("Parent: %s\n", commit.Parent)
fmt.Printf("Files:\n")
for _, blobHash := range commit.Index {
fmt.Printf("* %s\n", blobHash)
}
default:
fmt.Printf("Type: %s\nData: %s\n", obj.Typ, string(obj.Data))
}
return nil
}
func cmdCommit(ctx *Context, author string, hashes []string) error {
commit := Commit{
Author: author,
CreatedAt: time.Now(),
Index: hashes,
Parent: ctx.HEAD,
}
hash, err := ctx.PutCommit(commit)
if err != nil {
return err
}
if err := ctx.WriteHead(hash); err != nil {
return err
}
println(hash)
return nil
}
func cmdLog(ctx *Context) error {
history := ctx.History()
for history.Parent() {
fmt.Printf("%s %v %s\n", history.Commit.Author, history.Commit.CreatedAt, history.Hash)
}
return history.Error
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment