Building Git Part 6

Building Git: Part VI

Git part VI

What’s up everyone, what interesting projects are you guys working on, do tell me. In our previous part we updated our code to make use of add command to store the desired files in the staging area(a.k.a index file), and then update the commit command to commit only those files that are present in our staging area. But there are some problem with our current implementation. That is when we call our add command it overwrites all the files that are already present in the index file and starts from new blank state. This is not helpfull at all, imagine you added some files to the staging area, and after that also edited some other files and then added them to the staging area and commited the staging files, but to your surprise only the files that you added using the second add command that you used. Not good right.

Incremental addition

So in this part we will be updating our add command so that it can add the files to the index or in the staging area. We want to support the incremental update support, i.e. we want to use multiple add commands to update our staging area. And come let’s do that.

Parsing the .git/index

Firstly, we will update our handler function to not create a new instance of index but call a function IndexHoldForUpdate that will create a new index instance for us and will load the index file that might be present in the .gitgo directory.

cmd/gitgo/cmdHandler.go

func cmdAddHandler(args []string) error {
-   index := gitgo.NewIndex()
+   _, index := gitgo.IndexHoldForUpdate()
    for _, path := range args {
        // ...
}

We will update our Index struct to store a new changed flag. It will help us in keep the track we changed the index file has been modified since loading it.

index.go

type Index struct {
    // ...
    changed bool
}

func NewIndex() *Index {
    return &Index {
        // ...
        changed: false,
    }
}
func IndexHoldForUpdate() (bool, *Index) {
    index := NewIndex()
    b, err := index.lockfile.holdForUpdate()
    if err != nil {
        return false, nil
    }
    if !b {
        return false, nil
    }

    err = index.Load()
    return true, index
}

We will get the lock in this function so that we do not have to get the lock for any further calls.

Now we will have to open the index file that might be present in the .gitgo directory.

func (i *Index) load() error {
    fileReader, err := os.Open(filepath.Join(GITPATH, "index"))
    if err != nil {
        return err
    }
    defer fileReader.Close()
    hash := new(bytes.Buffer)
    count := i.readHeader(fileReader, hash)
    i.readEntries(fileReader, count, hash)
    verifyChecksum(fileReader, hash)

    return nil
}

First we open the file, create a new hash variable, read the header and get the count of the entries that might be present in the index file and lastly we will verify the checksum.

const(
    HEADERSIZE   = 12
    HEADERFORMAT = "a4N2"
    SIGNATURE    = "DIRC"
    VERSION      = 2
)


func (i *Index) readHeader(f *os.File, h *bytes.Buffer) int {
    data, err := read(f, HEADERSIZE)
    if err != nil {
        log.Fatalf("%s\n", err)
    }
    signature, version, count := data[:4], data[4:8], data[8:12]
    if string(signature) != SIGNATURE {
        log.Fatalf("signature: expected %s got %s\n", SIGNATURE, signature)
    }
    if binary.BigEndian.Uint32(version) != VERSION {
        log.Fatalf("version: expected %d got %d\n", binary.BigEndian.Uint32(version), VERSION)
    }

    h.Write(data)
    return int(binary.BigEndian.Uint32(count))
}

Here we read the file, and then get the signature, version and count of the entries by splicing the data []byte. We know that signature, version and count all of these three occupy the 4 byte in the index file. Then we verify that signature and version are what we expect them to be. If they are then we write that data into our []byte slice named hash we created in the load method.

func read(f io.Reader, size int, h *bytes.Buffer) ([]byte, error) {
    data := make([]byte, size)
    _, err := f.Read(data)
    if err != nil {
        return nil, err
    }

    return data, nil
}

Now we will create the readEntries.

func (i *Index) readEntries(r io.Reader, count int, h *bytes.Buffer) {
    for range count {
        entry, err := read(r, ENTRYMINSIZE)
        if err != nil {
            log.Fatalln(err)
        }

        for entry[len(entry)-1] != byte(0) {
            e, err := read(r, ENTRYBLOCK)
            if err != nil {
                log.Fatalln(err)
            }

            entry = append(entry, e)
        }

        i.storeEntryByte(entry)
        _, err := h.Write(entry)
        if err != nil {
            log.Fatalf("err writing entry to hash: %s\n")
        }
    }
}

First, we read the data for ENTRYMINSIZE count, if we do not find the nil byte in that block, then we again read the next 8 bytes, until we find the nil byte. And store it in the our Index and write it in our hash byte slice.

func verifyChecksum(f io.Reader, h *bytes.Buffer) {
    checksum := make([]byte, 20)
    c := bytes.NewBuffer(checksum)
    _, err := f.Read(c.AvailableBuffer())
    if err != nil {
        log.Fatalln(err)
    }

    currChecksum := sha1.Sum(h.Bytes())
    currC := bytes.NewBuffer(currChecksum[:])

    if bytes.Equal(c.Bytes(), currC.Bytes()) {
        log.Fatalln("checksums not equal")
    }
}

You guys can, give better variable name then me, I am just going with the flow. We read the remaining data in the file, that will hopefully only be the SHA1 hash of the index file. We are just checking for the equality here.

Now lets create the storeEntryByte function that we used in our readEntries function. We read the bytes of the entries and now we want to deserialize them to the IndexEntry struct.

func (i *Index) storeEntryByte(entry []byte) {
    fNameInEntry := entry[62:]
    oidInEntry := entry[40:60]
    nullIdx := bytes.IndexByte(fNameInEntry, byte(0))

    fileName := ""
    if nullIdx != -1 {
        fileName = string(fNameInEntry[:nullIdx])
    } else {
        fileName = string(fNameInEntry[:])
    }

    stat, err := os.Stat(fileName)
    if err != nil {
        log.Fatalln(err)
    }

    i.Add(fileName, hex.EncodeToString(oidInEntry), stat)
}

We add them in the Index, and set the changed flag to true.

func (i *Index) Add(path, oid string, stat os.FileInfor) {
    entry := NewIndexEntry(path, oid, stat)
    i.storeEntry(entry)
    i.changed = true
}

Update the changed flag, tells us that we made the changes in the index file and should commit those changes in the file.

func (i *Index) storeEntry(e *IndexEntry) {
    i.keys.Add(e.Path)
    i.entries[e.Path] = *e
}

This is easy we are just storing the Entries in our Index structure.

Storing Updates

Let’s update update our index writing function.

index.go

func (i *Index) WriteUpdate() (bool, error) {
-	b, err := i.lockfile.holdForUpdate()
-	if err != nil {
-		return false, err
-	}
-	if !b {
-		return false, nil
-	}
+	if !i.changed {
+		return false, i.lockfile.rollback()
+	}
+
	buf := new(bytes.Buffer) // makes a new buffer and returns its pointer

    // ...

    i.lockfile.commit()
+   i.changed = false
    return true, nil

This checks if the changed flag is not true, then we will rollback any changes we made.

locfile.go

func (l *lockFile) rollback() error {
    l.mu.Lock()
    defer l.mu.Unlock()

    err := os.Remove(l.LockPath)
    if err != nil {
        return err
    }
    l.Lock = nil
    return nil
}

We created the rollback function to rollback any changes we might have made upto that point.

Committing from Index

Now that we are can incrementally update our index file, now it s time to update our commit command to commit the files that are in the index file.

cmd/gitgo/cmdHandler.go

func cmdCommitHandler(_ string) error {
-	rootPath := gitgo.ROOTPATH
-	// storing all the blobs first
-	entries, err := gitgo.StoreOnDisk(rootPath)
-	if err != nil {
-		return err
-	}
-	// build merkel tree, and store all the subdirectories tree file
+	index := gitgo.NewIndex()
+	index.Load()
+	tree := gitgo.BuildTree(index.Entries())
	e, err := gitgo.TraverseTree(tree)
    if err != nil {
        return err
    }

    // ...

+   // clear the file after the commit
+   err = os.Remove(filepath.Join(gitgo.GITPATH, "index"))
+   if err != nil {
+       return err
+   }
+
    return nil
}

Previously, we were building the tree from the whole files and directory in the root directory, but now we do not have to do that, we should now only build tree from the files that are provided to us in the index entry.

And after commit we are removing the index file so that all the index entries are removed.

We return the slice of Entries that is returned to us by index.Entries to our BuildTree function.

index.go

func (i *Index) Entries() []Entries {
    e := []Entries{}

    it := i.keys.Iterator()
    for it.Next() {
        path := it.Key()
        entry := i.entries[path]
        e = append(e, Entries{
            Path: path,
            OID: entry.Oid,
            Stat: strconv.Itoa(entry.Mode),
        })
    }

    return e
}

The function is pretty much straight forward, our key value in the Index structure uses the SortedSet that we implemented , and get traverse that set.

And, voilaa…

Now our gitgo can read from the index file to make a commit, how cool is that.

The problem

Well, our current implementation has a problem.

Let’s suppose we added file called file1.txt in our index file using the add command. And now we changed the file1.txt file to be a directory and added a new file in it like file1.txt/some.txt, now if we again use the add command, we can save this new change just fine, we do not have the problem there, but we want to update our file structure in the index file.

If you check your index file, you can see both the entries for the file1.txt and file1.txt/some.txt, we do not want that to happen. We want our index structure to remain consistent with the repository structure.

This can be true for the vice-versa of this condition, that a folder has been changed to the file.

But before go about updating the code to correct this error, we should do something else.

Test suites

Yes, till now we were not writing the test, not because I do not know how to write test, but because book never instructed me to do so. And know it did.

First let’s write a test case for adding a file to the index.

index_test.go

package gitgo

import (
    "crypto/rand"
    "encoding/hex"
    "os"
    "runtime"
    "testing"

	"github.com/stretchr/testify/assert"
)

// for testing purpose any random oid will do
func randomOID() string {
    b := make([]byte, 20)
    if _, err := rand.Read(b); err != nil {
        panic("failed to read random bytes: " + err)
    }

    return hex.EncodeToString(b)
}

// for testing purpose we will get the stats of this
// test file
func thisFileStat(t *testing.T) os.FileInfo {
    // locate this test file
    _, thisFile, _, ok := runtime.Caller(0)
    if !ok {
        t.Fatal("could not get current test filename")
    }
    fi, err := os.Stat(thisFile)
    if err != nil {
        t.Fatalf("stat test file: %v", err)
    }

    return fi
}

func TestAddSingleFile(t *testing.T) {
    // create a temp dir for this test and act the
    // repo root dir
    tmpDir := t.TempDir()
    ROOTPATH = tmpDir

    idx := NewIndex()
    oid := randomOID()
    stat := thisFileStat(t)

    idx.Add("alice.txt", oid, stat)

    entries := idx.Entires()

    var got []string
    for _, e := range entries {
        got = append(got, e.Path)
    }

    expected := []string{"alice.txt"}
    assert.Equal(t, expected, got)
}

I’m making use of the testify package, for asserts in testing, rather using the go’s table-driven tests because I like this way.

Run the test and it should pass(fingers crossed).

Replacing file with directory

Now let’s write the test case for the problem we described earlier.

index_test.go

func TestReplaceFileWithDir(t *testing.T) {
    index := NewIndex()

    index.Add("alice.txt", randomOID(), thisFileStat(t))
    index.Add("bob.txt", randomOID(), thisFileStat(t))

    index.Add("alice.txt/nested.txt", randomOID(), thisFileStat(t))

    expected := []string{"alice.txt/nested.txt", "bob.txt"}
    var got []string
    it := index.keys.Iterator()
    for it.Next() {
        got = append(got, it.Key())
    }

    assert.Equal(t, expected, got)
}

This test case should fail, because we did not have any mechanism in place to correct this bug. You can call that we are now doing table driven development(TDD) (uncle bob will be happy with our progress).

index.go

func (i *Index) Add(path, oid string, stat os.FileInfo) {
    entry := NewIndexEntry(path, oid, stat)
+   i.discardConflict(entry)
    i.storeEntry(entry)
    i.changed = true
}
func (i *Index) discardConflict(e *IndexEntry) {
    var dirPaths []string
    d := filepath.Dir(e.Path)
    dirPaths = append(dirPaths, d)
    for d != "." && d != ".." && d != string(filepath.Separator) {
        d = filepath.Dir(d)
        dirPaths = append(dirPaths, d)
    }

    // Remove files if they are now changed to dir
    for _, dirPath := range dirPaths {
        i.keys.Remove(dirPath)
        delete(i.entries, dirPath)
    }
}

Now run the test and it should pass.

That’s one problem fixed.

Replace directory with file

Let’s write a test case for vice-versa case of the problem we just solved above.

index_test.go

func TestReplaceDirWithFile(t *testing.T) {
    index := NewIndex()

    index.Add("alice.txt", randomOID(), thisFileStat(t))
    index.Add("nested/bob.txt", randomOID(), thisFileStat(t))
    index.Add("nested", randomOID(), thisFileStat(t))

	expected := []string{"alice.txt", "nested"}
	var got []string
	it := index.keys.Iterator()
	for it.Next() {
		got = append(got, it.Key())
	}

	assert.Equal(t, expected, got)
}

func TestReplaceNestedDirWithFile(t *testing.T) {
	index := NewIndex()

	index.Add("alice.txt", randomOID(), thisFileStat(t))
	index.Add("nested/bob.txt", randomOID(), thisFileStat(t))
	index.Add("nested/inner/claire.txt", randomOID(), thisFileStat(t))
	index.Add("nested", randomOID(), thisFileStat(t))

	expected := []string{"alice.txt", "nested"}
	var got []string
	it := index.keys.Iterator()
	for it.Next() {
		got = append(got, it.Key())
	}

	assert.Equal(t, expected, got)
}

In this case, we have a dir nested that has a file bob.txt but in the next add command, we changed the structure and nested became a file. And same for the nested with the nested files.

To update our code to correct this, thing we will have to change the our Index struct. Currently, we storing things like this

index.keys = SortedSet([
    "alice.txt",
    "nested/bob.txt",
    "nexted/inner/claire.txt",
])

index.entries = {
    "alice.txt"               => Entries{}
    "nested/bob.txt"          => Entries{}
    "nexted/inner/claire.txt" => Entries{}
}

To remove the nested directories, we will have to traverse all the parents of the file and check if any change in the structure happened for all the files, let’s not do that.

We will use a new parameter parents in our index structure that will be the map of map[string]Set that will store the directory as key and all its inner files in a Set.

Well, we do not have the Set in go, so let’s create one.

internal/datastr/set.go

package datastr

import (
    "iter"
    "slices"
)

type Set struct {
    arr []string
}

func NewSet() *Set {
    return &Set{}
}

// Add value in the slice if not found
func (s *Set) Add(val string) {
    found := slices.Contains(s.arr, val)
    if found {
        return
    }
    s.arr = append(s.arr, val)
}

// Remove value if found
func (s *Set) Remove(val string) {
    found := slices.Contains(s.arr, val)
    if !found {
        return
    }
    idx := slices.Index(s.arr, val)
    s.arr = slices.Delete(s.arr, idx, idx)
}

// Return the iterator on the set
func (s *Set) All() iter.Seq2[int, string] {
    return slices.All(s.arr)
}

func (s *Set) GetAll() []string {
    return s.arr
}

func (s *Set) IsEmpty() bool {
    return len(s.arr) == 0
}

Simple right.. ?

index.go

type Index struct {
	entries  map[string]IndexEntry
	keys     *datastr.SortedSet
	lockfile *lockFile
	changed  bool
+	parents  map[string]*datastr.Set
}

func NewIndex() *Index {
	return &Index{
		entries:  make(map[string]IndexEntry),
		keys:     datastr.NewSortedSet(),
		lockfile: lockInitialize(filepath.Join(GITPATH, "index")),
		changed:  false,
+		parents:  make(map[string]*datastr.Set),
	}
}

Now we store an entry, we iterator over its parent directories and add the entry’s path to each directories set in index.parent

func (i *Index) storeEntry(e *IndexEntry) {
    i.keys.Add(e.Path)
    i.entries[e.Path] = *e

    var parents []string
    p := filepath.Dir(e.Path)
    parents = append(parents, p)

    for p != "." && p != ".." && p != string(filepath.Separator) {
        p = filepath.Dir(p)
        parents = append(parents, p)
    }

    for _, p := range parents {
        pSet, ok := i.parents[p]
        if !ok {
            pSet = datastr.NewSet()
            i.parents[p] = pSet
        }
        pSet.Add(e.Path)
    }
}

With the parents added we can now extend our discardConflict function to remove the directories that conflict with the file.

func (i *Index) discardConflict(e *IndexEntry) {

+   // Remove dirs if they are now changed to file
+   i.removeChildren(e.Path)
}
func (i *Index) removeChildren(p string) {
    pSet, ok := i.parents[p]
    if !ok {
        return
    }
    original := pSet.GetAll()
    children := make([]string, len(original))
    copy(children, original)
    for _, child := range children {
        i.removeEntry(child)
    }
}

func (i *Index) removeEntry(path string) {
    entry, ok := i.entries[path]
    if !ok {
        return
    }
    i.keys.Remove(entry.Path)
    delete(i.entries, entry.Path)

    var dirPaths []string
    d := filepath.Dir(path)
    dirPaths = append(dirPaths, d)
    for d != "." && d != ".." && d != string(filepath.Separator) {
        d = filepath.Dir(d)
        dirPaths = append(dirPaths, d)
    }

    for _, d := range dirPaths {
        dir := d
        i.parents[dir].Remove(entry.Path)
        if i.parents[dir].IsEmpty() {
            delete(i.parents, dir)
        }
    }
}

Woooohhhh, with this we are now done, with the current updates, now run the tests and we should pass all the test cases. I am passing all my test cases.

Handling Bad inputs

Our current implementation of the add command works very great, but under an assumption that our user is intelligent and will always provide us with the correct file or directory input. Alas, that is not always the case. The user might accidently provide the wrong file name or a file name that might not even exists in the repository.

Handling non-existent file

Now, we want to handle the bad input that our user might throw at us. Let’s see how git handles this?

mkdir gitgo-test && cd gitgo-test
touch main.go
git init

Now we will add the main.go file and another file that does not exists in the repository.

❯ git add main.go no-such-file
fatal: pathspec 'no-such-file' did not match any files

We get a fatal error telling us that the file does not exists. And if you try to check the index file, it does not exists, the git did not added any file what so ever if one such file do not exists.

cat .git/index
cat: .git/index: No such file or directory

So, for replicating that we will have to update our cmdAddHandler first to add all the files in an slice using the ListFiles function, we want this function to return an error if the file do not exists in the actual repository. If it returns the error we will then exit the function early and return the error indicating failure. If error does not occur in the ListFiles function that we can iterate over the slice in which we added all the file paths and do the normal thing that we were doing.

In our cmdAddHandler function we will have to update the iteration block, as we did in the code snippet below.

cmd/gitgo/cmdHandler.go

// @@ -112,39 +112,46 @@ func cmdCatFileHandler(hash string) error {
 func cmdAddHandler(args []string) error {
 	// index := gitgo.NewIndex()
 	_, index := gitgo.IndexHoldForUpdate()
+	var filePaths []string
+
+	// add all the paths to a slice first
 	for _, path := range args {
 		absPath, err := filepath.Abs(path)
 		if err != nil {
 			return err
 		}
 		expandPaths, err := gitgo.ListFiles(absPath)
+		if err != nil {
+			index.Release()
+			return err
+		}
+		filePaths = append(filePaths, expandPaths...)
+	}
+
+	for _, p := range filePaths {
+		ap, err := filepath.Abs(p)
+		if err != nil {
+			return err
+		}
+
+		data, err := os.ReadFile(ap)
 		if err != nil {
 			return err
 		}
-		for _, p := range expandPaths {
-			ap, err := filepath.Abs(p)
-			if err != nil {
-				return err
-			}
-
-			data, err := os.ReadFile(ap)
-			if err != nil {
-				return err
-			}
-			stat, err := os.Stat(ap)
-			if err != nil {
-				return err
-			}
-
-			blob := gitgo.Blob{Data: data}.Init()
-			hash, err := blob.Store()
-			if err != nil {
-				return err
-			}
-
-			index.Add(p, hash, stat)
+		stat, err := os.Stat(ap)
+		if err != nil {
+			return err
 		}
+
+		blob := gitgo.Blob{Data: data}.Init()
+		hash, err := blob.Store()
+		if err != nil {
+			return err
+		}
+
+		index.Add(p, hash, stat)
 	}
+
 	res, err := index.WriteUpdate()
 	if err != nil {
 		return err

In our ListFiles function we are just now checking if our syscall to the Stat on the file or directory name returned any error, if it did that would possibly means(not neccessarily) that the file is not present in the actual repository.

files.go

// @@ -1,12 +1,15 @@
 package gitgo

 import (
+	"errors"
 	"fmt"
 	"io/fs"
 	"os"
 	"path/filepath"
 )

+var ErrMissingFile = errors.New("no file with the name")
+
 // Returns the flatten directory structure
 func ListFiles(dir string) ([]string, error) {
 	var workfiles []string
// @@ -18,6 +21,10 @@ func ListFiles(dir string) ([]string, error) {

 		// check if the given dir string is file or dir ?
 		s, err := os.Stat(path)
+		// if file is not present
+		if os.IsNotExist(err) {
+			return fmt.Errorf("%w '%s'", ErrMissingFile, dir)
+		}
 		if !s.IsDir() {
 			relPath, err := filepath.Rel(dir, path)
 			if err != nil {

Now, if we encounter the above mentioned error, we want to release the lock that we have on the index file so that when the next instance of the gitgo runs it does not file index.lock file.

index.go

// @@ -423,3 +423,5 @@ func writeIndexEntry(entry IndexEntry) ([]byte, error) {
 	}
 	return b.Bytes(), nil
 }
+
+func (i *Index) Release() error { return i.lockfile.rollback() }

Easy.

Unreadable File

Now, there might happen that the reader have given us a file name, that we cannot read(i.e. we do not have the appropriate permission to do so). Let’s see what happens in the git when we do this.

chmod -r main.go
❯ git add main.go
error: open("main.go"): Permission denied
error: unable to index file 'main.go'
fatal: adding files failed

I changed the permission our file by removing the reading permission and as we can see we cannot use the syscall open because we do that have the permission to do so. The git reported that error.

cmd/gitgo/cmdHandler.go

// @@ -136,6 +136,9 @@ func cmdAddHandler(args []string) error {

 		data, err := os.ReadFile(ap)
 		if err != nil {
+			if os.IsPermission(err) {
+				return fmt.Errorf("%w '%s'\nFatal: adding files failed", os.ErrPermission, p)
+			}
 			return err
 		}
 		stat, err := os.Stat(ap)

In our case the fix is very easy for us, we just want to give an better error message to our user.

Locked Index file

We can trigger the git’s error handler in case there is already a .git/index.lock file present.

touch .git/index.lock
❯ git add main.go
fatal: Unable to create '/home/viku/Workspace/personal/go/tests/gitgo-test/.git/index.lock': File exists.

Another git process seems to be running in this repository, e.g.
an editor opened by 'git commit'. Please make sure all processes
are terminated then try again. If it still fails, a git process
may have crashed in this repository earlier:
remove the file manually to continue.

In the book, it said that the message e.g an editor opened by git commit is now not true for the git as while the git opens the commit file at that time it does not have the lock for the index file. So we can just remove this example message in our implementation.

Now in our IndexHoldForUpdate we want to return an error, if the file is already present.

index.go

// @@ -60,20 +60,20 @@ func (i *Index) Entries() []Entries {
 	return e
 }

-func IndexHoldForUpdate() (bool, *Index) {
+func IndexHoldForUpdate() (bool, *Index, error) {
 	index := NewIndex()
 	b, err := index.lockfile.holdForUpdate()
 	if err != nil {
-		return false, nil
+		return false, index, err
 	}
 	if !b {
-		return false, nil
+		return false, index, nil
 	}

 	// load the index file
 	err = index.Load()

-	return true, index
+	return true, index, nil
 }

 func (i *Index) Load() error {

In our lockfile implementation we are now making a check in the error handling, if the file already exists(also I have updated how I am checking for the errors).

lockfile.go

// @@ -5,6 +5,7 @@ import (
 	"log"
 	"os"
 	"sync"
+	"syscall"
 )

 var (
@@ -31,18 +32,23 @@ func lockInitialize(path string) *lockFile {
 func (l *lockFile) holdForUpdate() (bool, error) {
 	l.mu.Lock()
 	defer l.mu.Unlock()
+
 	if l.Lock != nil {
		return true, nil // lock already aquired
 	}

 	file, err := os.OpenFile(l.LockPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
 	if err != nil {
-		if os.IsExist(err) {
-			return false, nil
-		} else if os.IsNotExist(err) {
-			return false, ErrMissingParent
-		} else if os.IsPermission(err) {
-			return false, ErrNoPermission
+		var pathErr *os.PathError
+		if errors.As(err, &pathErr) {
+			switch pathErr.Err {
+			case syscall.EEXIST:
+				return false, ErrLockDenied
+			case syscall.ENOENT:
+				return false, ErrMissingParent
+			case syscall.EACCES:
+				return false, ErrNoPermission
+			}
 		}
 		return false, err
 	}

Now we can update our ref file, to better reflect our update in the lockfile.

// @@ -2,7 +2,6 @@ package gitgo

 import (
 	"errors"
-	"fmt"
 	"os"
 	"path/filepath"
 )
@@ -23,8 +22,8 @@ func RefInitialize(pathname string) ref {
 func (r ref) UpdateHead(oid []byte) error {
 	lockfile := lockInitialize(r.headPath)

-	if lock, _ := lockfile.holdForUpdate(); !lock {
-		return fmt.Errorf("Err: %s\nCould not aquire lock on file: %s", ErrLockDenied, r.headPath)
+	if _, err := lockfile.holdForUpdate(); err != nil {
+		return err
 	}

 	oid = append(oid, '\n')

And lastly the cmdAddHandler, we are now returning a better error message.

cmd/gitgo/cmdHandler.go

// @@ -111,7 +111,16 @@ func cmdCatFileHandler(hash string) error {

 func cmdAddHandler(args []string) error {
 	// index := gitgo.NewIndex()
-	_, index := gitgo.IndexHoldForUpdate()
+	_, index, err := gitgo.IndexHoldForUpdate()
+	if err != nil {
+		return fmt.Errorf(`
+Fatal: %w
+
+Another gitgo process seems to be running in this repository.
+Please make sure all processes are terminated then try again.
+If it still fails, a gitgo process may have crashed in this
+repository earlier: remove the file manually to continue.`, err)
+	}
 	var filePaths []string

 	// add all the paths to a slice first

And now we are done with the implementation of the git’s add command.

Afterword

In this blog, we updated our add command to do the following

  • Parsing the index file.
  • Add the files in the incremental manner.
  • Commit from the index file.
  • Handling the bad input.

Code Link: Github

Just know this,

Reinvent the wheel, so that you can learn how to invent wheel

– a nobody

Share: X (Twitter) Facebook LinkedIn