diff --git a/Makefile b/Makefile index 83f7ca1..960bcd5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -EXECUTABLE := microgit +EXECUTABLE := mgit build: - go build -o $(EXECUTABLE) cmd/cmd.go + go build -o $(EXECUTABLE) main.go install: build sudo mv $(EXECUTABLE) /usr/local/bin/ diff --git a/cmd/abstract.go b/cmd/abstract.go new file mode 100644 index 0000000..b8dfce7 --- /dev/null +++ b/cmd/abstract.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/akamensky/argparse" +) + +type ICommand interface { + Register(parser *argparse.Parser) + Handle() bool +} + +type CommandMeta struct { + Command *argparse.Command +} diff --git a/cmd/cat.go b/cmd/cat.go new file mode 100644 index 0000000..a99d53b --- /dev/null +++ b/cmd/cat.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "fmt" + "strconv" + + "micro-git/object" + + "github.com/akamensky/argparse" +) + +type CatCommand struct { + CommandMeta + fileInput *string + shouldShowObjectType *bool + shouldShowSize *bool +} + +func (c *CatCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("cat-file", "Provide content or type and size information for repository objects") + c.fileInput = c.Command.StringPositional(&argparse.Options{ + Required: true, + Help: "The name of the object to show", + }) + c.shouldShowObjectType = c.Command.Flag("t", "type", &argparse.Options{ + Default: false, + Help: "Instead of the content, show the object type", + }) + c.shouldShowSize = c.Command.Flag("s", "size", &argparse.Options{ + Default: false, + Help: "Instead of the content, show the object size", + }) +} + +func (c *CatCommand) Handle() bool { + if c.Command.Happened() { + info, err := catFile(*c.fileInput, *c.shouldShowObjectType, *c.shouldShowSize) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(info) + } + return true + } + return false +} + +func catFile(oid string, shouldShowType, shouldShowSize bool) (string, error) { + objectInfo, err := object.Read(oid) + if err != nil { + return "", err + } + + if shouldShowType && shouldShowSize { + err = fmt.Errorf("-t and -s cannot be used altogether") + return "", err + } + + if shouldShowType { + return objectInfo.Type, nil + } + if shouldShowSize { + return strconv.Itoa(objectInfo.Size), nil + } + return string(objectInfo.Content), nil +} diff --git a/cmd/cat_test.go b/cmd/cat_test.go new file mode 100644 index 0000000..d3e6673 --- /dev/null +++ b/cmd/cat_test.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "crypto/sha1" + "encoding/hex" + "os" + "path/filepath" + "testing" + + "micro-git/root" + "micro-git/testutil" + + "github.com/stretchr/testify/suite" +) + +type CatTestSuite struct { + suite.Suite + currWd string + tmpDir string + sampleOid string +} + +func (suite *CatTestSuite) SetupSuite() { + currWd, err := os.Getwd() + if err != nil { + suite.FailNow("Failed to get current working directory", "Error: %v", err) + } + + suite.currWd = currWd + + tmpDir := testutil.CreateTestDir(suite.T()) + suite.tmpDir = tmpDir + + err = os.WriteFile("test.txt", []byte("Hello"), 0o664) + if err != nil { + suite.FailNow("Failed to create test.txt file for testing", "Error: %v", err) + } + + // create the .microgit folder + err = root.InitDB() + if err != nil { + suite.FailNow("Failed to execute Init command", "Error: %v", err) + } + + // hash the test.txt file + combined := append([]byte("blob"), []byte(" 5")...) + combined = append(combined, '\x00') + combined = append(combined, []byte("Hello")...) + shaSum := sha1.Sum(combined) + hexSum := hex.EncodeToString(shaSum[:]) + + suite.sampleOid = hexSum + + folderPath := filepath.Join(".microgit", "objects", hexSum[:2]) + objectPath := filepath.Join(folderPath, hexSum[2:]) + + err = os.MkdirAll(folderPath, 0o774) + if err != nil { + suite.FailNow("Failed to create folder for hashed file of test.txt", "Error: %v", err) + } + + err = os.WriteFile(objectPath, combined, 0o664) + if err != nil { + suite.FailNow("Failed to create hashed file of test.txt", "Error: %v", err) + } +} + +func (suite *CatTestSuite) TearDownSuite() { + os.RemoveAll(suite.tmpDir) + os.Chdir(suite.currWd) +} + +func (suite *CatTestSuite) TestCatFileContentReturnCorrectResult() { + content, err := catFile(suite.sampleOid, false, false) + if err != nil { + suite.FailNow("catFile failed", "Error: %v", err) + } + suite.Equal("Hello", string(content)) +} + +func (suite *CatTestSuite) TestCatFileTypeReturnCorrectResult() { + fileType, err := catFile(suite.sampleOid, true, false) + if err != nil { + suite.FailNow("catFile failed", "Error: %v", err) + } + suite.Equal("blob", fileType) +} + +func (suite *CatTestSuite) TestCatFileSizeReturnCorrectResult() { + fileSize, err := catFile(suite.sampleOid, false, true) + if err != nil { + suite.FailNow("catFile failed", "Error: %v", err) + } + suite.Equal("5", fileSize) +} + +func TestCatTestSuite(t *testing.T) { + suite.Run(t, new(CatTestSuite)) +} diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index 2eb994b..0000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,172 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - - "micro-git/object" - "micro-git/root" - - "github.com/akamensky/argparse" -) - -func main() { - parser := argparse.NewParser("microgit", "These are common microgit commands used in various situations.") - - initCommand := parser.NewCommand("init", "Create an empty micro-git repository or reinitialize an existing one") - - hashObjectCommand := parser.NewCommand("hash-object", "Compute object ID and optionally creates a blob from a file") - hashObjectFileType := hashObjectCommand.String("t", "type", &argparse.Options{ - Help: "Specify the type (default: \"blob\")", - Default: "blob", - }) - hashObjectWriteFlag := hashObjectCommand.Flag("w", "write", &argparse.Options{ - Required: false, - Help: "Actually write the object into the object database", - }) - hashObjectFileInput := hashObjectCommand.StringPositional(&argparse.Options{ - Required: true, - Help: "The file to be hashed", - }) - - catFileCommand := parser.NewCommand("cat-file", "Provide content or type and size information for repository objects") - catFileInput := catFileCommand.StringPositional(&argparse.Options{ - Required: true, - Help: "The name of the object to show", - }) - catFileShouldShowObjectType := catFileCommand.Flag("t", "type", &argparse.Options{ - Default: false, - Help: "Instead of the content, show the object type", - }) - catFileShouldShowSize := catFileCommand.Flag("s", "size", &argparse.Options{ - Default: false, - Help: "Instead of the content, show the object size", - }) - - writeTreeCommand := parser.NewCommand("write-tree", "Create tree object from the current index") - writeTreePrefix := writeTreeCommand.String("", "prefix", &argparse.Options{ - Required: false, - Help: "Write a tree object from subdirectory ", - Default: ".", - }) - - readTreeCommand := parser.NewCommand("read-tree", "Reads tree information into the index") - readTreeInput := readTreeCommand.StringPositional(&argparse.Options{ - Required: true, - Help: "The id of the tree object to be read/merged", - }) - - commitCommand := parser.NewCommand("commit", "Record changes to the repository") - commitMessageInput := commitCommand.String("m", "message", &argparse.Options{ - Help: "The commit message", - Required: true, - }) - - logCommand := parser.NewCommand("log", "Shows the commit logs.") - - err := parser.Parse(os.Args) - if err != nil { - fmt.Print(parser.Usage(err)) - return - } - - if initCommand.Happened() { - err := root.InitDB() - if err != nil { - fmt.Println(err) - return - } - } - - if hashObjectCommand.Happened() { - hexSum, err := HashObject(*hashObjectFileInput, *hashObjectFileType, *hashObjectWriteFlag) - if err != nil { - fmt.Println(err) - return - } - fmt.Println(hexSum) - } - - if catFileCommand.Happened() { - info, err := CatFile(*catFileInput, *catFileShouldShowObjectType, *catFileShouldShowSize) - if err != nil { - fmt.Println(err) - return - } - fmt.Println(info) - } - - if writeTreeCommand.Happened() { - treeId, err := object.WriteTree(*writeTreePrefix) - if err != nil { - fmt.Println(err) - return - } - fmt.Println(treeId) - } - - if readTreeCommand.Happened() { - err := object.ReadTree(*readTreeInput) - if err != nil { - fmt.Println(err) - return - } - } - - if commitCommand.Happened() { - commitOid, err := object.Commit(*commitMessageInput) - if err != nil { - fmt.Println(err) - return - } - fmt.Println(commitOid) - } - - if logCommand.Happened() { - err := object.PrintCommitLogs() - if err != nil { - fmt.Println(err) - return - } - } -} - -func HashObject(path, objectType string, shouldWrite bool) (string, error) { - fileContent, err := os.ReadFile(path) - if err != nil { - err := fmt.Errorf("failed to read file content, %v", err) - return "", err - } - - if shouldWrite { - return object.Write(objectType, fileContent) - } - - objectInfo, err := object.GenInfo(objectType, fileContent) - if err != nil { - return "", err - } - - return objectInfo.Oid, nil -} - -func CatFile(oid string, shouldShowType, shouldShowSize bool) (string, error) { - objectInfo, err := object.Read(oid) - if err != nil { - return "", err - } - - if shouldShowType && shouldShowSize { - err = fmt.Errorf("-t and -s cannot be used altogether") - return "", err - } - - if shouldShowType { - return objectInfo.Type, nil - } else if shouldShowSize { - return strconv.Itoa(objectInfo.Size), nil - } else { - return string(objectInfo.Content), nil - } -} diff --git a/cmd/commit.go b/cmd/commit.go new file mode 100644 index 0000000..31065a7 --- /dev/null +++ b/cmd/commit.go @@ -0,0 +1,95 @@ +package cmd + +import ( + "fmt" + "os/user" + "time" + + "micro-git/object" + "micro-git/refs" + + "github.com/akamensky/argparse" +) + +type CommitCommand struct { + CommandMeta + commitMessage *string +} + +func (c *CommitCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("commit", "Record changes to the repository") + c.commitMessage = c.Command.String("m", "message", &argparse.Options{ + Help: "The commit message", + Required: true, + }) +} + +func (c *CommitCommand) Handle() bool { + if c.Command.Happened() { + commitOid, err := commit(*c.commitMessage) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(commitOid) + } + + return true + } + return false +} + +func commit(msg string) (string, error) { + /* + Create commit file + format: + + tree + parent + author: Author timestamp + + commit message + */ + treeOid, err := object.WriteTree(".") + if err != nil { + return "", err + } + + fileContent := fmt.Appendf([]byte{}, "%v %v\n", object.TREE_OBJECT_TYPE, treeOid) + + // parent commit + // @todo: commit the same working directory will cause parent to point to the same commit + // How to check no changes since last commit? + headRef, err := refs.GetCurrentHead() + if err != nil { + return "", fmt.Errorf("failed to read HEAD file, %v", err) + } + + prevCommitOid, err := refs.GetRefContent(headRef) + if err != nil { + return "", fmt.Errorf("failed to read ref file, %v", err) + } + fileContent = fmt.Appendf(fileContent, "parent %v\n", prevCommitOid) + + // author + usr, err := user.Current() + var username string + if err == nil { + username = usr.Username + } else { + username = "" + } + fileContent = fmt.Appendf(fileContent, "author %v %v\n\n", username, time.Now().Unix()) + + // commit message + fileContent = append(fileContent, []byte(msg)...) + + commitOid, err := object.Write(object.COMMIT_OBJECT_TYPE, fileContent) + if err != nil { + return "", err + } + + // @todo: How to handle if error happened here? + refs.SetRefContent(headRef, commitOid) + + return commitOid, nil +} diff --git a/cmd/commit_test.go b/cmd/commit_test.go new file mode 100644 index 0000000..47f9185 --- /dev/null +++ b/cmd/commit_test.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "micro-git/object" + "micro-git/root" + "micro-git/testutil" + + "github.com/stretchr/testify/suite" +) + +type CommitTestSuite struct { + suite.Suite + currWd string + tmpDir string +} + +func (suite *CommitTestSuite) SetupSuite() { + currWd, err := os.Getwd() + if err != nil { + suite.FailNow("Failed to get current working directory", "Error: %v", err) + } + + suite.currWd = currWd + + tmpDir := testutil.CreateTestDir(suite.T()) + suite.tmpDir = tmpDir + + // create .microgit root folder + err = root.InitDB() + if err != nil { + suite.FailNow("Failed to execute Init command", "Error: %v", err) + } + + // Create the directory contents + /* + folder structure: + .microgit + test.txt + src + test2.txt + */ + err = os.WriteFile("test.txt", []byte("Hello"), 0o664) + if err != nil { + suite.FailNow("Failed to create test.txt file", "Error: %v", err) + } + + srcDir := filepath.Join(suite.tmpDir, "src") + err = os.Mkdir(srcDir, 0o774) + if err != nil { + suite.FailNow("Failed to create src directory", "Error: %v", err) + } + + err = os.WriteFile(filepath.Join(srcDir, "test2.txt"), []byte("Hello World"), 0o664) + if err != nil { + suite.FailNow("Failed to create test2.txt file", "Error: %v", err) + } + + emptyDir := filepath.Join(suite.tmpDir, "empty") + err = os.Mkdir(emptyDir, 0o774) + if err != nil { + suite.FailNow("Failed to create empty directory", "Error: %v", err) + } +} + +func (suite *CommitTestSuite) TearDownSuite() { + os.RemoveAll(suite.tmpDir) + os.Chdir(suite.currWd) +} + +func (suite *CommitTestSuite) TestCommit() { + oidFromWriteTree, err := object.WriteTree(".") + if err != nil { + suite.FailNow("Failed to execute WriteTree", "Error: %v", err) + } + + oid, err := commit("commit message") + if err != nil { + suite.FailNow("Failed to execute WriteTree", "Error: %v", err) + } + + objectInfo, err := object.Read(oid) + if err != nil { + suite.FailNow("Cannot read the commit file", "Error: %v", err) + } + + commitFileContent := string(objectInfo.Content) + commitFileLines := strings.Split(commitFileContent, "\n") + + suite.Equal("commit message", commitFileLines[4]) + suite.Equal(oidFromWriteTree, strings.Split(commitFileLines[0], " ")[1]) +} + +func TestCommitTestSuite(t *testing.T) { + suite.Run(t, new(CommitTestSuite)) +} diff --git a/cmd/hashobject.go b/cmd/hashobject.go new file mode 100644 index 0000000..4804af8 --- /dev/null +++ b/cmd/hashobject.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "os" + + "micro-git/object" + + "github.com/akamensky/argparse" +) + +type HashObjectCommand struct { + CommandMeta + fileInput *string + objectType *string + shouldWrite *bool +} + +func (c *HashObjectCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("hash-object", "Compute object ID and optionally creates a blob from a file") + c.objectType = c.Command.String("t", "type", &argparse.Options{ + Help: "Specify the type (default: \"blob\")", + Default: "blob", + }) + c.shouldWrite = c.Command.Flag("w", "write", &argparse.Options{ + Required: false, + Help: "Actually write the object into the object database", + }) + c.fileInput = c.Command.StringPositional(&argparse.Options{ + Required: true, + Help: "The file to be hashed", + }) +} + +func (c *HashObjectCommand) Handle() bool { + if c.Command.Happened() { + hexSum, err := hashObject(*c.fileInput, *c.objectType, *c.shouldWrite) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(hexSum) + } + return true + } + return false +} + +func hashObject(path, objectType string, shouldWrite bool) (string, error) { + fileContent, err := os.ReadFile(path) + if err != nil { + err := fmt.Errorf("failed to read file content, %v", err) + return "", err + } + + if shouldWrite { + return object.Write(objectType, fileContent) + } + + objectInfo, err := object.GenInfo(objectType, fileContent) + if err != nil { + return "", err + } + + return objectInfo.Oid, nil +} diff --git a/cmd/cmd_test.go b/cmd/hashobject_test.go similarity index 63% rename from cmd/cmd_test.go rename to cmd/hashobject_test.go index 8dee69b..885f7f1 100644 --- a/cmd/cmd_test.go +++ b/cmd/hashobject_test.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "crypto/sha1" @@ -7,20 +7,19 @@ import ( "path/filepath" "testing" - "micro-git/object" "micro-git/root" "micro-git/testutil" "github.com/stretchr/testify/suite" ) -type CmdTestSuite struct { +type HashObjectTestSuite struct { suite.Suite currWd string tmpDir string } -func (suite *CmdTestSuite) SetupSuite() { +func (suite *HashObjectTestSuite) SetupSuite() { currWd, err := os.Getwd() if err != nil { suite.FailNow("Failed to get current working directory", "Error: %v", err) @@ -37,24 +36,24 @@ func (suite *CmdTestSuite) SetupSuite() { } } -func (suite *CmdTestSuite) TearDownSuite() { +func (suite *HashObjectTestSuite) TearDownSuite() { os.RemoveAll(suite.tmpDir) os.Chdir(suite.currWd) } -func (suite *CmdTestSuite) SetupTest() { +func (suite *HashObjectTestSuite) SetupTest() { err := root.InitDB() if err != nil { suite.FailNow("Failed to execute Init command", "Error: %v", err) } } -func (suite *CmdTestSuite) TearDownTest() { +func (suite *HashObjectTestSuite) TearDownTest() { os.RemoveAll(".microgit") } -func (suite *CmdTestSuite) TestHashBlobObjectNotWriteToDisk() { - hexSum, err := HashObject("test.txt", "blob", false) +func (suite *HashObjectTestSuite) TestHashBlobObjectNotWriteToDisk() { + hexSum, err := hashObject("test.txt", "blob", false) if err != nil { suite.FailNow("HashObject failed", "Error: %v", err) } @@ -68,8 +67,8 @@ func (suite *CmdTestSuite) TestHashBlobObjectNotWriteToDisk() { suite.Equal(expected, hexSum) } -func (suite *CmdTestSuite) TestHashBlobObjectWriteToDisk() { - hexSum, err := HashObject("test.txt", "blob", true) +func (suite *HashObjectTestSuite) TestHashBlobObjectWriteToDisk() { + hexSum, err := hashObject("test.txt", "blob", true) if err != nil { suite.FailNow("HashObject failed", "Error: %v", err) } @@ -91,29 +90,13 @@ func (suite *CmdTestSuite) TestHashBlobObjectWriteToDisk() { suite.Equal(string(combined), string(fileContent)) } -func (suite *CmdTestSuite) TestHashObjectInvalidObjectType() { - hexSum, err := HashObject("test.txt", "invalid_object_type", false) +func (suite *HashObjectTestSuite) TestHashObjectInvalidObjectType() { + hexSum, err := hashObject("test.txt", "invalid_object_type", false) suite.Empty(hexSum, "HashObject should return empty result because of invalid object type") suite.NotEmpty(err, "HashObject should return error because of invalid object type") } -func (suite *CmdTestSuite) TestCatFileReturnCorrectResult() { - hexSum, err := HashObject("test.txt", "blob", true) - if err != nil { - suite.FailNow("HashObject failed", "Error: %v", err) - } - - objInfo, err := object.Read(hexSum) - if err != nil { - suite.FailNow("CatFile failed: ", "Error: %v", err) - } - - suite.Equal("blob", objInfo.Type) - suite.Equal(5, objInfo.Size) - suite.Equal("Hello", string(objInfo.Content)) -} - -func TestCmdTestSuite(t *testing.T) { - suite.Run(t, new(CmdTestSuite)) +func TestHashObjectTestSuite(t *testing.T) { + suite.Run(t, new(HashObjectTestSuite)) } diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..99ae3da --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "fmt" + + "micro-git/root" + + "github.com/akamensky/argparse" +) + +type InitCommand struct { + CommandMeta +} + +func (c *InitCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("init", "Create an empty micro-git repository or reinitialize an existing one") +} + +func (c *InitCommand) Handle() bool { + if c.Command.Happened() { + err := root.InitDB() + if err != nil { + fmt.Println(err) + } + return true + } + return false +} diff --git a/cmd/log.go b/cmd/log.go new file mode 100644 index 0000000..71fa0b1 --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "fmt" + "strconv" + "strings" + "time" + + "micro-git/object" + "micro-git/refs" + + "github.com/akamensky/argparse" +) + +type commitInfo struct { + treeOid string + parentCommitOid string + author string + time int64 + message string +} + +type LogCommand struct { + CommandMeta +} + +func (c *LogCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("log", "Shows the commit logs.") +} + +func (c *LogCommand) Handle() bool { + if c.Command.Happened() { + err := printCommitLogs() + if err != nil { + fmt.Println(err) + } + + return true + } + return false +} + +func printCommitLogs() error { + headRef, err := refs.GetCurrentHead() + if err != nil { + return fmt.Errorf("failed to read HEAD file, %v", err) + } + + commitOid, err := refs.GetRefContent(headRef) + if err != nil { + return fmt.Errorf("failed to read ref file, %v", err) + } + if commitOid == "" { + return fmt.Errorf("fatal: Your branch does not have any commits yet") + } + + commitInfo, err := parseCommitContent(commitOid) + if err != nil { + return err + } + + /* + commit: + Author: author + Date: + + commit message + */ + + t := time.Unix(commitInfo.time, 0) + fmt.Printf("commit: %v\nAuthor: %v\nDate: %v\n\n\t%v\n\n", commitOid, commitInfo.author, t.String(), commitInfo.message) + + for curr := commitInfo; curr.parentCommitOid != ""; { + parentCommitInfo, err := parseCommitContent(curr.parentCommitOid) + if err != nil { + return err + } + t := time.Unix(parentCommitInfo.time, 0) + fmt.Printf( + "commit: %v\nAuthor: %v\nDate: %v\n\n\t%v\n\n", + curr.parentCommitOid, + parentCommitInfo.author, + t.String(), + parentCommitInfo.message, + ) + curr = parentCommitInfo + } + + return nil +} + +func parseCommitContent(commitOid string) (*commitInfo, error) { + objInfo, err := object.Read(commitOid) + if err != nil { + return nil, fmt.Errorf("failed to read commit file, %v", err) + } + commitContents := strings.Split(string(objInfo.Content), "\n\n") + message := commitContents[1] + + infos := strings.Split(commitContents[0], "\n") + treeOid := strings.Split(infos[0], " ")[1] + parent := strings.Split(infos[1], " ")[1] + + authorAndTime := strings.Split(infos[2], " ") + author := authorAndTime[1] + timestampInt64, err := strconv.ParseInt(authorAndTime[2], 10, 64) + if err != nil { + timestampInt64 = 0 + } + + return &commitInfo{ + treeOid: treeOid, + parentCommitOid: parent, + author: author, + time: timestampInt64, + message: message, + }, nil +} diff --git a/cmd/readtree.go b/cmd/readtree.go new file mode 100644 index 0000000..a8888ad --- /dev/null +++ b/cmd/readtree.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + + "micro-git/object" + + "github.com/akamensky/argparse" +) + +type ReadTreeCommand struct { + CommandMeta + treeOid *string +} + +func (c *ReadTreeCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("read-tree", "Reads tree information into the index") + c.treeOid = c.Command.StringPositional(&argparse.Options{ + Required: true, + Help: "The id of the tree object to be read/merged", + }) +} + +func (c *ReadTreeCommand) Handle() bool { + if c.Command.Happened() { + err := object.ReadTree(*c.treeOid) + if err != nil { + fmt.Println(err) + } + return true + } + return false +} diff --git a/cmd/writetree.go b/cmd/writetree.go new file mode 100644 index 0000000..84dc7c7 --- /dev/null +++ b/cmd/writetree.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + + "micro-git/object" + + "github.com/akamensky/argparse" +) + +type WriteTreeCommand struct { + CommandMeta + subdir *string +} + +func (c *WriteTreeCommand) Register(p *argparse.Parser) { + c.Command = p.NewCommand("write-tree", "Create tree object from the current index") + c.subdir = c.Command.String("", "prefix", &argparse.Options{ + Required: false, + Help: "Write a tree object from subdirectory ", + Default: ".", + }) +} + +func (c *WriteTreeCommand) Handle() bool { + if c.Command.Happened() { + treeId, err := object.WriteTree(*c.subdir) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(treeId) + } + + return true + } + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..35e338b --- /dev/null +++ b/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "os" + + "micro-git/cmd" + + "github.com/akamensky/argparse" +) + +type Pipeline struct { + commands []cmd.ICommand +} + +func (p *Pipeline) setNext(command cmd.ICommand) *Pipeline { + p.commands = append(p.commands, command) + return p +} + +func (p Pipeline) execute() { + if len(p.commands) == 0 { + return + } + + for _, command := range p.commands { + if handled := command.Handle(); handled { + break + } + } +} + +func main() { + pipeline := Pipeline{} + parser := argparse.NewParser("mgit", "These are common microgit commands used in various situations.") + + // porcelain commands + initCommand := &cmd.InitCommand{} + commitCommand := &cmd.CommitCommand{} + logCommand := &cmd.LogCommand{} + + // plumbing commands + hashObjectCommand := &cmd.HashObjectCommand{} + catCommand := &cmd.CatCommand{} + writeTreeCommand := &cmd.WriteTreeCommand{} + readTreeCommand := &cmd.ReadTreeCommand{} + + // Registration + initCommand.Register(parser) + commitCommand.Register(parser) + logCommand.Register(parser) + hashObjectCommand.Register(parser) + catCommand.Register(parser) + writeTreeCommand.Register(parser) + readTreeCommand.Register(parser) + + pipeline. + setNext(initCommand). + setNext(commitCommand). + setNext(logCommand). + setNext(hashObjectCommand). + setNext(catCommand). + setNext(writeTreeCommand). + setNext(readTreeCommand) + + err := parser.Parse(os.Args) + if err != nil { + fmt.Print(parser.Usage(err)) + return + } + + pipeline.execute() +} diff --git a/object/object.go b/object/object.go index ce1e628..3234de4 100644 --- a/object/object.go +++ b/object/object.go @@ -6,13 +6,10 @@ import ( "encoding/hex" "fmt" "os" - "os/user" "path/filepath" "strconv" "strings" - "time" - "micro-git/refs" "micro-git/root" ) @@ -37,14 +34,6 @@ type treeEntry struct { filename string } -type commitInfo struct { - treeOid string - parentCommitOid string - author string - time int64 - message string -} - func GenInfo(objectType string, fileContent []byte) (*ObjectInfo, error) { if objectType != BLOB_OBJECT_TYPE && objectType != TAG_OBJECT_TYPE && @@ -228,102 +217,12 @@ func ReadTree(oid string) error { fmt.Printf("file %v failed to write to directory, %v, skipping...\n", err, filenamePath) continue } - - fmt.Println(filenamePath) } } return nil } -func Commit(msg string) (string, error) { - treeOid, err := WriteTree(".") - if err != nil { - return "", err - } - - fileContent := []byte(fmt.Sprintf("%v %v\n", TREE_OBJECT_TYPE, treeOid)) - - // parent commit - // @todo: commit the same working directory will cause parent to point to the same commit - // How to check no changes since last commit? - headRef, err := refs.GetCurrentHead() - if err != nil { - return "", fmt.Errorf("failed to read HEAD file, %v", err) - } - - prevCommitOid, err := refs.GetRefContent(headRef) - if err != nil { - return "", fmt.Errorf("failed to read ref file, %v", err) - } - fileContent = append(fileContent, []byte(fmt.Sprintf("parent %v\n", prevCommitOid))...) - - // author - usr, err := user.Current() - var username string - if err == nil { - username = usr.Username - } else { - username = "" - } - fileContent = append(fileContent, []byte(fmt.Sprintf("author %v %v\n\n", username, time.Now().Unix()))...) - - // commit message - fileContent = append(fileContent, []byte(msg)...) - - commitOid, err := Write(COMMIT_OBJECT_TYPE, fileContent) - if err != nil { - return "", err - } - - // @todo: How to handle if error happened here? - refs.SetRefContent(headRef, commitOid) - - return commitOid, nil -} - -func PrintCommitLogs() error { - headRef, err := refs.GetCurrentHead() - if err != nil { - return fmt.Errorf("failed to read HEAD file, %v", err) - } - - commitOid, err := refs.GetRefContent(headRef) - if err != nil { - return fmt.Errorf("failed to read ref file, %v", err) - } - if commitOid == "" { - return fmt.Errorf("fatal: Your branch does not have any commits yet") - } - - commitInfo, err := parseCommitContent(commitOid) - if err != nil { - return err - } - - /* - commit: - Author: author - Date: - - commit message - */ - - t := time.Unix(commitInfo.time, 0) - fmt.Printf("commit: %v\nAuthor: %v\nDate: %v\n\n\t%v\n\n", commitOid, commitInfo.author, t.String(), commitInfo.message) - - for curr := commitInfo; curr.parentCommitOid != ""; { - curr, err = parseCommitContent(curr.parentCommitOid) - if err != nil { - return err - } - t := time.Unix(commitInfo.time, 0) - fmt.Printf("commit: %v\nAuthor: %v\nDate: %v\n\n\t%v\n\n", commitOid, curr.author, t.String(), curr.message) - } - - return nil -} - func recursivelyReadTree(oid, prefix string, treeEntries *[]treeEntry) error { objectInfo, err := Read(oid) if err != nil { @@ -364,31 +263,3 @@ func recursivelyReadTree(oid, prefix string, treeEntries *[]treeEntry) error { return nil } - -func parseCommitContent(commitOid string) (*commitInfo, error) { - objInfo, err := Read(commitOid) - if err != nil { - return nil, fmt.Errorf("failed to read commit file, %v", err) - } - commitContents := strings.Split(string(objInfo.Content), "\n\n") - message := commitContents[1] - - infos := strings.Split(commitContents[0], "\n") - treeOid := strings.Split(infos[0], " ")[1] - parent := strings.Split(infos[1], " ")[1] - - authorAndTime := strings.Split(infos[2], " ") - author := authorAndTime[1] - timestampInt64, err := strconv.ParseInt(authorAndTime[2], 10, 64) - if err != nil { - timestampInt64 = 0 - } - - return &commitInfo{ - treeOid: treeOid, - parentCommitOid: parent, - author: author, - time: timestampInt64, - message: message, - }, nil -} diff --git a/object/object_test.go b/object/object_test.go index da3c0f2..add2922 100644 --- a/object/object_test.go +++ b/object/object_test.go @@ -38,13 +38,13 @@ func TestGenInfo(t *testing.T) { assert.Equal(t, oid, objectInfo.Oid) } -type TreeAndCommitTestSuite struct { +type TreeIOTestSuite struct { suite.Suite currWd string tmpDir string } -func (suite *TreeAndCommitTestSuite) SetupSuite() { +func (suite *TreeIOTestSuite) SetupSuite() { currWd, err := os.Getwd() if err != nil { suite.FailNow("Failed to get current working directory", "Error: %v", err) @@ -56,12 +56,12 @@ func (suite *TreeAndCommitTestSuite) SetupSuite() { suite.tmpDir = tmpDir } -func (suite *TreeAndCommitTestSuite) TearDownSuite() { +func (suite *TreeIOTestSuite) TearDownSuite() { os.RemoveAll(suite.tmpDir) os.Chdir(suite.currWd) } -func (suite *TreeAndCommitTestSuite) SetupTest() { +func (suite *TreeIOTestSuite) SetupTest() { err := root.InitDB() if err != nil { suite.FailNow("Failed to execute Init command", "Error: %v", err) @@ -97,14 +97,14 @@ func (suite *TreeAndCommitTestSuite) SetupTest() { } } -func (suite *TreeAndCommitTestSuite) TearDownTest() { +func (suite *TreeIOTestSuite) TearDownTest() { os.RemoveAll(".microgit") os.RemoveAll("src") os.RemoveAll("empty") os.Remove("test.txt") } -func (suite *TreeAndCommitTestSuite) TestWriteTree() { +func (suite *TreeIOTestSuite) TestWriteTree() { oid, err := WriteTree(".") if err != nil { suite.FailNow("Failed to execute WriteTree", "Error: %v", err) @@ -185,7 +185,7 @@ func (suite *TreeAndCommitTestSuite) TestWriteTree() { suite.Equal("tree", info.objectType) } -func (suite *TreeAndCommitTestSuite) TestReadTree() { +func (suite *TreeIOTestSuite) TestReadTree() { oid, err := WriteTree(".") if err != nil { suite.FailNow("Failed to execute WriteTree", "Error: %v", err) @@ -249,29 +249,6 @@ func (suite *TreeAndCommitTestSuite) TestReadTree() { suite.Equal("Hello World", string(fileContent)) } -func (suite *TreeAndCommitTestSuite) TestCommit() { - oidFromWriteTree, err := WriteTree(".") - if err != nil { - suite.FailNow("Failed to execute WriteTree", "Error: %v", err) - } - - oid, err := Commit("commit message") - if err != nil { - suite.FailNow("Failed to execute WriteTree", "Error: %v", err) - } - - objectInfo, err := Read(oid) - if err != nil { - suite.FailNow("Cannot read the commit file", "Error: %v", err) - } - - commitFileContent := string(objectInfo.Content) - commitFileLines := strings.Split(commitFileContent, "\n") - - suite.Equal("commit message", commitFileLines[4]) - suite.Equal(oidFromWriteTree, strings.Split(commitFileLines[0], " ")[1]) -} - -func TestTreeAndCommitTestSuite(t *testing.T) { - suite.Run(t, new(TreeAndCommitTestSuite)) +func TestTreeIOTestSuite(t *testing.T) { + suite.Run(t, new(TreeIOTestSuite)) }