diff --git a/cmd/log.go b/cmd/log.go new file mode 100644 index 0000000..4c4b1ff --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/richardjennings/mygit/internal/mygit" + "github.com/spf13/cobra" + "log" +) + +var logCmd = &cobra.Command{ + Use: "log", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := configure(); err != nil { + log.Fatalln(err) + } + return mygit.Log() + }, +} + +func init() { + rootCmd.AddCommand(logCmd) +} diff --git a/internal/mygit/commits/commit.go b/internal/mygit/commits/commit.go deleted file mode 100644 index 940bdd5..0000000 --- a/internal/mygit/commits/commit.go +++ /dev/null @@ -1,31 +0,0 @@ -package commits - -import ( - "github.com/richardjennings/mygit/internal/mygit/refs" - "time" -) - -type ( - Commit struct { - Tree []byte - Parents []string - Author string - AuthoredTime time.Time - Committer string - CommittedTime time.Time - Message string - } -) - - - -func PreviousCommits() ([]string, error) { - previousCommit, err := refs.LastCommit() - if err != nil { - return nil, err - } - if previousCommit != nil { - return []string{string(previousCommit)}, nil - } - return nil, nil -} diff --git a/internal/mygit/commits/writer.go b/internal/mygit/commits/writer.go deleted file mode 100644 index 599d00e..0000000 --- a/internal/mygit/commits/writer.go +++ /dev/null @@ -1,36 +0,0 @@ -package commits - -import ( - "encoding/hex" - "fmt" - "github.com/richardjennings/mygit/internal/mygit/config" - "github.com/richardjennings/mygit/internal/mygit/objects" - "github.com/richardjennings/mygit/internal/mygit/refs" -) - -func Write(c *Commit) ([]byte, error) { - var parentCommits string - for _, v := range c.Parents { - parentCommits += fmt.Sprintf("parent %s\n", v) - } - content := []byte(fmt.Sprintf( - "tree %s\n%sauthor %s %d +0000\ncommitter %s %d +0000\n\n%s", - hex.EncodeToString(c.Tree), - parentCommits, - c.Author, - c.AuthoredTime.Unix(), - c.Committer, - c.CommittedTime.Unix(), - c.Message, - )) - header := []byte(fmt.Sprintf("commit %d%s", len(content), string(byte(0)))) - sha, err := objects.WriteObject(header, content, "", config.ObjectPath()) - if err != nil { - return nil, err - } - branch, err := refs.CurrentBranch() - if err != nil { - return nil, err - } - return sha, refs.UpdateHead(branch, sha) -} diff --git a/internal/mygit/index/status.go b/internal/mygit/index/status.go index a53d579..f9eb14a 100644 --- a/internal/mygit/index/status.go +++ b/internal/mygit/index/status.go @@ -25,7 +25,7 @@ func (idx *Index) CommitStatus(sha []byte) ([]*fs.File, error) { } return files, nil } - obj, err := objects.ReadObject(sha) + obj, err := objects.ReadObjectTree(sha) if err != nil { return nil, err } diff --git a/internal/mygit/mygit.go b/internal/mygit/mygit.go index 6bb8f6b..9a40a9e 100644 --- a/internal/mygit/mygit.go +++ b/internal/mygit/mygit.go @@ -3,7 +3,6 @@ package mygit import ( "errors" "fmt" - "github.com/richardjennings/mygit/internal/mygit/commits" "github.com/richardjennings/mygit/internal/mygit/config" "github.com/richardjennings/mygit/internal/mygit/fs" "github.com/richardjennings/mygit/internal/mygit/index" @@ -34,6 +33,27 @@ func Init() error { return os.WriteFile(config.GitHeadPath(), []byte(fmt.Sprintf("ref: %s\n", config.Config.DefaultBranch)), 0644) } +// Log prints out the commit log for the current branch +func Log() error { + branch, err := refs.CurrentBranch() + if err != nil { + return err + } + commitSha, err := refs.HeadSHA(branch) + if err != nil { + return err + } + commit, err := objects.ReadCommit(commitSha) + if err != nil { + return err + } + fmt.Printf("tree: %s\n", string(commit.Tree)) + for _, v := range commit.Parents { + fmt.Printf("parent: %s\n", string(v)) + } + return nil +} + // Add adds one or more file paths to the Index. func Add(paths ...string) error { idx, err := index.ReadIndex() @@ -104,12 +124,12 @@ func Commit() ([]byte, error) { } // git has the --allow-empty flag which here defaults to true currently // @todo check for changes to be committed. - previousCommits, err := commits.PreviousCommits() + previousCommits, err := refs.PreviousCommits() if err != nil { return nil, err } - return commits.Write( - &commits.Commit{ + return objects.WriteCommit( + &objects.Commit{ Tree: tree, Parents: previousCommits, Author: "Richard Jennings ", diff --git a/internal/mygit/objects/object.go b/internal/mygit/objects/object.go index 2213e10..319d86a 100644 --- a/internal/mygit/objects/object.go +++ b/internal/mygit/objects/object.go @@ -1,14 +1,43 @@ package objects +import ( + "io" + "time" +) + type ( Object struct { - Path string - Typ objectType - Sha []byte - Objects []*Object + Path string + Typ objectType + Sha []byte + Objects []*Object + Length int + HeaderLength int + ReadCloser func() (io.ReadCloser, error) //mode string } objectType int + Commit struct { + Sha []byte + Tree []byte + Parents [][]byte + Author string + AuthoredTime time.Time + Committer string + CommittedTime time.Time + Message string + } + Tree struct { + Sha []byte + Typ objectType + Path string + Items []*TreeItem + } + TreeItem struct { + Sha []byte + Typ objectType + Path string + } ) const ( diff --git a/internal/mygit/objects/reader.go b/internal/mygit/objects/reader.go index 9f54fe1..e0e9549 100644 --- a/internal/mygit/objects/reader.go +++ b/internal/mygit/objects/reader.go @@ -6,11 +6,13 @@ import ( "compress/zlib" "encoding/hex" "errors" + "fmt" "github.com/richardjennings/mygit/internal/mygit/config" "github.com/richardjennings/mygit/internal/mygit/fs" "io" "os" "path/filepath" + "strconv" ) // FlattenTree turns a TreeObject structure into a flat list of file paths @@ -30,93 +32,198 @@ func (o *Object) FlattenTree() []*fs.File { return objFiles } -// ReadObject reads an object from the object store func ReadObject(sha []byte) (*Object, error) { - path := filepath.Join(config.ObjectPath(), string(sha[0:2]), string(sha[2:])) - f, err := os.OpenFile(path, os.O_RDONLY, 0644) - if err != nil { - return nil, err - } - defer func() { _ = f.Close() }() - z, err := zlib.NewReader(f) + var err error + o := &Object{Sha: sha} + o.ReadCloser = ObjectReadCloser(sha) + z, err := o.ReadCloser() if err != nil { - return nil, err + return o, err } defer func() { _ = z.Close() }() buf := bufio.NewReader(z) - - // read parts by null byte p, err := buf.ReadBytes(0) if err != nil && !errors.Is(err, io.EOF) { return nil, err } - + o.HeaderLength = len(p) header := bytes.Fields(p) - o := &Object{Sha: sha} + switch string(header[0]) { case "commit": o.Typ = ObjectCommit - content, err := buf.ReadBytes(0) - if err != nil && !errors.Is(err, io.EOF) { + case "tree": + o.Typ = ObjectTree + case "blob": + o.Typ = ObjectBlob + default: + return nil, fmt.Errorf("unknown %s", string(header[0])) + } + o.Length, err = strconv.Atoi(string(header[1][:len(header[1])-1])) + return o, err +} + +func ObjectReadCloser(sha []byte) func() (io.ReadCloser, error) { + return func() (io.ReadCloser, error) { + path := filepath.Join(config.ObjectPath(), string(sha[0:2]), string(sha[2:])) + f, err := os.OpenFile(path, os.O_RDONLY, 0644) + if err != nil { return nil, err } - tree := bytes.Fields(content) - if len(tree) < 2 { - return nil, errors.New("expected at least 2 parts") - } - - _ = z.Close() + defer func() { _ = f.Close() }() + return zlib.NewReader(f) + } +} - co, err := ReadObject(tree[1]) +// ReadObjectTree reads an object from the object store +func ReadObjectTree(sha []byte) (*Object, error) { + obj, err := ReadObject(sha) + if err != nil { + return nil, err + } + switch obj.Typ { + case ObjectCommit: + commit, err := readCommit(obj) + if err != nil { + return obj, err + } + co, err := ReadObjectTree(commit.Tree) if err != nil { return nil, err } - o.Objects = append(o.Objects, co) - return o, nil - case "tree": - o.Typ = ObjectTree - sha := make([]byte, 20) - - // there should be a null byte after file path, then 20 byte sha - for { - p, err = buf.ReadBytes(0) - + obj.Objects = append(obj.Objects, co) + return obj, nil + case ObjectTree: + tree, err := ReadTree(obj) + if err != nil { + return nil, err + } + for _, v := range tree.Items { + o, err := ReadObject(v.Sha) if err != nil { - if errors.Is(err, io.EOF) { - break - } return nil, err } - _, err = io.ReadFull(buf, sha) - // buf.ReadBytes just keeps returning the same data with no error ? - - item := bytes.Fields(p) - co := &Object{} - co.Sha = []byte(hex.EncodeToString(sha)) - if string(item[0]) == "40000" { - co.Typ = ObjectTree - co, err = ReadObject(co.Sha) - if err != nil { - return nil, err - } - } else { - co.Typ = ObjectBlob + o.Path = v.Path + if o.Typ != v.Typ { + return nil, errors.New("types did not match somehow") } - co.Path = string(item[1][:len(item[1])-1]) + obj.Objects = append(obj.Objects, o) + } + return obj, nil + case ObjectBlob: + // lets not read the whole blob + return nil, nil + } + return nil, errors.New("unhandled object type") + +} - o.Objects = append(o.Objects, co) +func ReadTree(obj *Object) (*Tree, error) { + var err error + var p []byte - if err == io.EOF { + tree := &Tree{} + r, err := obj.ReadCloser() + if err != nil { + return nil, err + } + defer func() { _ = r.Close() }() + if err := readHeadBytes(r, obj); err != nil { + return nil, err + } + // + sha := make([]byte, 20) + buf := bufio.NewReader(r) + // there should be a null byte after file path, then 20 byte sha + for { + itm := &TreeItem{} + p, err = buf.ReadBytes(0) + + if err != nil { + if errors.Is(err, io.EOF) { break } + return nil, err + } + _, err = io.ReadFull(buf, sha) + item := bytes.Fields(p) + itm.Sha = []byte(hex.EncodeToString(sha)) + if string(item[0]) == "40000" { + itm.Typ = ObjectTree + if err != nil { + return nil, err + } + } else { + itm.Typ = ObjectBlob + } + itm.Path = string(item[1][:len(item[1])-1]) + if err == io.EOF { + break } - return o, nil - case "blob": - // lets not read the whole blob - return nil, nil + tree.Items = append(tree.Items, itm) + } + return tree, nil +} - default: - return nil, errors.New("unhandled object type") +func readHeadBytes(r io.ReadCloser, obj *Object) error { + n, err := r.Read(make([]byte, obj.HeaderLength)) + if err != nil { + return err + } + if n != obj.HeaderLength { + return fmt.Errorf("read %d not %d", n, obj.HeaderLength) + } + return nil +} + +func ReadCommit(sha []byte) (*Commit, error) { + o, err := ReadObject(sha) + if err != nil { + return nil, err + } + return readCommit(o) +} + +func readCommit(obj *Object) (*Commit, error) { + r, err := obj.ReadCloser() + if err != nil { + return nil, err } + defer func() { _ = r.Close() }() + if err := readHeadBytes(r, obj); err != nil { + return nil, err + } + c := &Commit{Sha: obj.Sha} + + buf := bufio.NewReader(r) + l, _, err := buf.ReadLine() + if err != nil { + return nil, err + } + b := bytes.Split(l, []byte(" ")) + if len(b) != 2 || string(b[0]) != "tree" { + return nil, fmt.Errorf("invalid %s", string(l)) + } + c.Tree = b[1] + for { + l, _, err = buf.ReadLine() + if err != nil { + if !errors.Is(err, io.EOF) { + return nil, err + } + break + } + b := bytes.Split(l, []byte(" ")) + if len(b) < 2 { + return nil, fmt.Errorf("invalid %s", string(l)) + } + if string(b[0]) != "parent" { + break + } else { + c.Parents = append(c.Parents, b[1]) + } + } + + return c, nil } diff --git a/internal/mygit/objects/writer.go b/internal/mygit/objects/writer.go index ff0ed57..c4b8c83 100644 --- a/internal/mygit/objects/writer.go +++ b/internal/mygit/objects/writer.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "github.com/richardjennings/mygit/internal/mygit/config" + "github.com/richardjennings/mygit/internal/mygit/refs" "io" "io/fs" "os" @@ -110,3 +111,30 @@ func WriteBlob(path string) (*Object, error) { sha, err := WriteObject(header, nil, path, config.ObjectPath()) return &Object{Sha: sha, Path: path}, err } + +func WriteCommit(c *Commit) ([]byte, error) { + var parentCommits string + for _, v := range c.Parents { + parentCommits += fmt.Sprintf("parent %s\n", v) + } + content := []byte(fmt.Sprintf( + "tree %s\n%sauthor %s %d +0000\ncommitter %s %d +0000\n\n%s", + hex.EncodeToString(c.Tree), + parentCommits, + c.Author, + c.AuthoredTime.Unix(), + c.Committer, + c.CommittedTime.Unix(), + c.Message, + )) + header := []byte(fmt.Sprintf("commit %d%s", len(content), string(byte(0)))) + sha, err := WriteObject(header, content, "", config.ObjectPath()) + if err != nil { + return nil, err + } + branch, err := refs.CurrentBranch() + if err != nil { + return nil, err + } + return sha, refs.UpdateHead(branch, sha) +} diff --git a/internal/mygit/refs/refs.go b/internal/mygit/refs/refs.go index d2df942..63f3e3e 100644 --- a/internal/mygit/refs/refs.go +++ b/internal/mygit/refs/refs.go @@ -55,3 +55,14 @@ func LastCommit() ([]byte, error) { } return HeadSHA(currentBranch) } + +func PreviousCommits() ([][]byte, error) { + previousCommit, err := LastCommit() + if err != nil { + return nil, err + } + if previousCommit != nil { + return [][]byte{previousCommit}, nil + } + return nil, nil +}