diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..e8d2561
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,28 @@
+# This workflow will build a golang project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
+
+name: build
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: '1.21'
+
+ - name: Build
+ run: go build -v ./...
+
+ - name: Test
+ run: go test -v ./...
diff --git a/Makefile b/Makefile
index 3930e3a..f3f8465 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,6 @@
build:
- go build -o bin/tgit .
+ go build -o bin/tgit .;
+ cp bin/tgit ../tgit-test
clean:
rm -rf .tgit/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ad5efe6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+# tgit
+Tiny Git - Lightweight Version Control System
+
+
+
+[](https://github.com/mvdan/gofumpt)
+
+
+#### Installation
+```
+go install github.com/humanbeeng/tgit@latest
+```
+
+#### Commands
+- `init`: Initialises an empty tgit repository.
+- `branch`: Creates a new branch by having current branch as head.
+- `add`: Stage file(s)
+- `commit`: Commit staged files to branch.
+- `checkout`: DFS commit tree. Update file changes from all commits and checkouts to branch.
+- `help`: Displays a help message
+
+
+
+#### Features
+- Commits are made against the checked out branch.
+- Uncreated branch cannot be checked out to.
+- Cannot reinit a tgit repository.
+- Cannot add duplicate files to staging if they are unmodified. Only those files that are either modified or haven't been staged before can be added and the old one will be overwritten in staged area.
+- Invalid command checks.
+- Checkout will yield all committed files that we made upto branch-name HEAD.
+- Cannot commit an empty staged area.
+
+#### References
+[Git internals](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain)
diff --git a/add.go b/add.go
index 9ab272a..8c6575d 100644
--- a/add.go
+++ b/add.go
@@ -16,7 +16,7 @@ const (
Tree FileType = "tree"
)
-type Staged struct {
+type TreeItem struct {
Filename string
Hash string
FileType FileType
@@ -36,7 +36,7 @@ func add(fargs []string) error {
filesize := idxinfo.Size()
- staged := make(map[string]Staged)
+ staged := make(map[string]TreeItem)
// Read INDEX file
if filesize > 0 {
@@ -72,7 +72,7 @@ func add(fargs []string) error {
if canStage(farg, hash, staged) {
fmt.Printf("Added %v\n", farg)
- staged[farg] = Staged{Filename: farg, Hash: hash, FileType: Blob}
+ staged[farg] = TreeItem{Filename: farg, Hash: hash, FileType: Blob}
}
}
@@ -91,7 +91,7 @@ func add(fargs []string) error {
return nil
}
-func canStage(file string, fileHash string, staged map[string]Staged) bool {
+func canStage(file string, fileHash string, staged map[string]TreeItem) bool {
if _, err := os.Stat(".tgit/objects/" + fileHash); err == nil {
fmt.Printf("No latest change in %v\n", file)
return false
diff --git a/add_test.go b/add_test.go
deleted file mode 100644
index f6e6ec9..0000000
--- a/add_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "testing"
-)
-
-func TestSuccessfulAdd(t *testing.T) {
- files := []string{"asfasd.go"}
- err := add(files)
- if err != nil {
- t.Errorf("Unable to add main.go")
- }
-}
diff --git a/bin/tgit b/bin/tgit
index 146792d..ba8d2ce 100755
Binary files a/bin/tgit and b/bin/tgit differ
diff --git a/branch.go b/branch.go
new file mode 100644
index 0000000..a8398ae
--- /dev/null
+++ b/branch.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+)
+
+func branch(args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf("Branch name required\n")
+ }
+
+ if branchExists(args[0]) {
+ return fmt.Errorf("Branch %v already exists", args[0])
+ }
+
+ // Get the current branch
+ headInf, err := os.Stat(".tgit/refs/HEAD")
+ if err != nil {
+ return err
+ }
+
+ cb, err := currBranch(headInf)
+ if err != nil {
+ return err
+ }
+
+ cbFile, err := os.OpenFile(".tgit/refs/heads/"+cb, os.O_RDWR, fs.ModePerm)
+ if err != nil {
+ return fmt.Errorf("Unable to read curr branch file")
+ }
+ defer cbFile.Close()
+
+ // Get the latest commit hash from the curr branch
+ lch, err := os.ReadFile(".tgit/refs/heads/" + cb)
+ if err != nil {
+ return fmt.Errorf("Unable to get latest commit hash, %v", err)
+ }
+
+ // Create new branch
+ if _, err := os.Create(".tgit/refs/heads/" + args[0]); err != nil {
+ return fmt.Errorf("Unable to create main branch file")
+ }
+
+ // Write latest commit hash to new branch file
+ if err := os.WriteFile(".tgit/refs/heads/"+args[0], lch, fs.ModePerm); err != nil {
+ return fmt.Errorf("Unable to write latest commit hash")
+ }
+
+ // Reset HEAD
+ if err = os.Truncate(".tgit/refs/HEAD", 0); err != nil {
+ return err
+ }
+
+ // Update HEAD
+ if err := os.WriteFile(".tgit/refs/HEAD", []byte(args[0]), fs.ModePerm); err != nil {
+ return err
+ }
+
+ fmt.Printf("%v created \n", args[0])
+
+ return nil
+}
+
+func branchExists(branch string) bool {
+ _, err := os.Stat(".tgit/refs/heads/" + branch)
+ return !os.IsNotExist(err)
+}
diff --git a/checkout.go b/checkout.go
new file mode 100644
index 0000000..480aa60
--- /dev/null
+++ b/checkout.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "io/fs"
+ "os"
+)
+
+func checkout(args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf("No branch name found.")
+ }
+
+ if !branchExists(args[0]) {
+ fmt.Println(args[0], "does not exists")
+ return fmt.Errorf("Branch %v does not exists", args[0])
+ }
+
+ // Get the latest commit hash from the to checkout branch
+ lch, err := os.ReadFile(".tgit/refs/heads/" + args[0])
+ if err != nil {
+ return fmt.Errorf("Unable to get latest commit hash, %v", err)
+ }
+
+ if len(lch) > 0 {
+
+ q := make([]string, 0)
+ q = append(q, string(lch))
+ uniqueFiles := make(map[string]string, 0)
+
+ for len(q) != 0 {
+ currHash := q[0]
+ q = q[1:]
+
+ // Get the commit object
+ cFile, err := os.ReadFile(".tgit/objects/" + string(currHash))
+ if err != nil {
+ return err
+ }
+
+ if len(cFile) == 0 {
+ return fmt.Errorf("No commits found. Aborting")
+ }
+
+ var cobj CommitObject
+
+ buf := bytes.NewBuffer(cFile)
+ dec := gob.NewDecoder(buf)
+
+ if err := dec.Decode(&cobj); err != nil {
+ return fmt.Errorf("Unable to decode commit object , %v", err)
+ }
+
+ // Get staged tree obj which is a map[filename]TreeObj from the currHash
+ cTree, err := os.ReadFile(".tgit/objects/" + cobj.TreeHash)
+ if err != nil {
+ return err
+ }
+
+ var tree map[string]TreeItem
+
+ treeBuf := bytes.NewBuffer(cTree)
+ treeDec := gob.NewDecoder(treeBuf)
+
+ if err := treeDec.Decode(&tree); err != nil {
+ return fmt.Errorf("Unable to decode tree object map, %v", err)
+ }
+
+ // Check if the filename already exists in map. If not, add to map
+ for filename, t := range tree {
+ if _, ok := uniqueFiles[filename]; !ok {
+ uniqueFiles[filename] = t.Hash
+ }
+ }
+
+ if cobj.SubtreeHash != "" {
+ q = append(q, cobj.SubtreeHash)
+ }
+
+ }
+
+ for filename, hash := range uniqueFiles {
+ source, err := os.ReadFile(".tgit/objects/" + hash)
+ if err != nil {
+ return err
+ }
+
+ if err := os.WriteFile(filename, source, fs.ModePerm); err != nil {
+ return err
+ }
+
+ }
+ }
+
+ // Reset HEAD
+ if err = os.Truncate(".tgit/refs/HEAD", 0); err != nil {
+ return err
+ }
+ // Update HEAD
+ if err := os.WriteFile(".tgit/refs/HEAD", []byte(args[0]), fs.ModePerm); err != nil {
+ return err
+ }
+ fmt.Println("Checked out to ", args[0])
+
+ return nil
+}
diff --git a/commit.go b/commit.go
index 5a3895c..962302d 100644
--- a/commit.go
+++ b/commit.go
@@ -9,13 +9,6 @@ import (
"os"
)
-/*
-Git commit flow
-Check the branch head if it has any head
-
-if head exists, add that to the tree-object
-*/
-
type CommitObject struct {
Message string
TreeHash string
@@ -44,19 +37,9 @@ func commit(args []string) error {
return err
}
- headFile, err := os.OpenFile(".tgit/refs/HEAD", os.O_RDWR, fs.ModePerm)
+ currBranch, err := currBranch(headInf)
if err != nil {
- return fmt.Errorf("Unable to read HEAD file")
- }
- defer headFile.Close()
-
- // Get the curr branch name
- var currBranch string
- if headInf.Size() > 0 {
- sc := bufio.NewScanner(headFile)
- for sc.Scan() {
- currBranch = sc.Text()
- }
+ return err
}
if _, err := os.Stat(".tgit/refs/heads/" + currBranch); err != nil {
@@ -79,7 +62,7 @@ func commit(args []string) error {
}
}
- var staged map[string]Staged
+ var staged map[string]TreeItem
idxbuf := bytes.NewBuffer(idx)
idxdec := gob.NewDecoder(idxbuf)
@@ -98,8 +81,6 @@ func commit(args []string) error {
SubtreeHash: latestHash,
}
- fmt.Println("About to commit", co)
-
// Create hash for the commit struct
cmtHash := getSha1([]byte(cmtMsg + stageHash + latestHash))
@@ -120,13 +101,18 @@ func commit(args []string) error {
}
- // Write commit-object to a file
+ // Write tree-object to a file
+ if err := os.WriteFile(".tgit/objects/"+stageHash, idx, fs.ModePerm); err != nil {
+ return fmt.Errorf("Unable to create tree-object")
+ }
+
cmtFile, err := os.OpenFile(".tgit/objects/"+cmtHash, os.O_CREATE|os.O_RDWR, fs.ModePerm)
if err != nil {
return fmt.Errorf("Unable to open commit object, %v", err)
}
defer cmtFile.Close()
+ // Write commit-object to a file
cobuf := new(bytes.Buffer)
cog := gob.NewEncoder(cobuf)
diff --git a/commit_test.go b/commit_test.go
deleted file mode 100644
index dd9d48f..0000000
--- a/commit_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "testing"
-)
-
-func TestSuccessfulCommit(t *testing.T) {
- msg := "Test commit"
- err := commit(msg)
- if err != nil {
- t.Errorf("Commit failed")
- }
-}
diff --git a/main.go b/main.go
index 1ce39ee..3a3b843 100644
--- a/main.go
+++ b/main.go
@@ -24,9 +24,18 @@ func main() {
}
case "help":
{
+ if !repoExists() {
+ fmt.Println("Repository not initialized")
+ return
+ }
displayHelp()
}
+ case "checkout":
+ {
+ checkout(args[2:])
+ }
+
case "add":
{
if !repoExists() {
@@ -52,6 +61,18 @@ func main() {
}
}
+ case "branch":
+ {
+ if !repoExists() {
+ fmt.Println("Repository not initialized")
+ return
+ }
+
+ if err := branch(args[2:]); err != nil {
+ fmt.Println(err)
+ }
+ }
+
default:
{
fmt.Println("Invalid command", args[0])
diff --git a/utils.go b/utils.go
index 87a56c0..aa396e2 100644
--- a/utils.go
+++ b/utils.go
@@ -1,6 +1,7 @@
package main
import (
+ "bufio"
"crypto/sha1"
"encoding/hex"
"errors"
@@ -39,3 +40,21 @@ func repoExists() bool {
}
return true
}
+
+func currBranch(headInf fs.FileInfo) (string, error) {
+ headFile, err := os.OpenFile(".tgit/refs/HEAD", os.O_RDWR, fs.ModePerm)
+ if err != nil {
+ return "", fmt.Errorf("Unable to read HEAD file")
+ }
+ defer headFile.Close()
+
+ // Get the curr branch name
+ var currBranch string
+ if headInf.Size() > 0 {
+ sc := bufio.NewScanner(headFile)
+ for sc.Scan() {
+ currBranch = sc.Text()
+ }
+ }
+ return currBranch, nil
+}