Go and Git

Go and Git

Creating a Simple Version Control System from Scratch

Git is a popular version control system widely used in software development to manage code changes. It is a system that allows developers to keep track of all changes made to the code. This blog will explore implementing basic Git functionality using the Go programming language.

The program we will build has four commands: “init”, “add”, “commit”, and “log”.

“Init” command:

func initRepo() {
 err := os.Mkdir(".git", 0755)
 if err != nil {
  log.Fatalf("Error creating .git directory: %v", err)
 }
 err = os.Mkdir(filepath.Join(".git", "objects"), 0755)
 if err != nil {
  log.Fatalf("Error creating objects directory: %v", err)
 }
 err = os.Mkdir(filepath.Join(".git", "commits"), 0755)
 if err != nil {
  log.Fatalf("Error creating commits directory: %v", err)
 }
 err = ioutil.WriteFile(filepath.Join(".git", "HEAD"), []byte("ref: refs/heads/master\n"), 0644)
 if err != nil {
  log.Fatalf("Error creating HEAD file: %v", err)
 }
 fmt.Println("Initialized empty Git repository in .git/")
}

The “init” function creates a .git directory in the current directory, along with the objects and commits directories inside it. It also creates a HEAD file that points to the master branch. When you run this function, it will print out a message saying that the repository has been initialized.

“Add” command:

Before we work on the “add” function, we need two helper methods. One to generate a sha1 hash, and another to create an object path to store the contents to

func getObjectPath(content []byte) string {
 sha1 := sha1sum(content)
 return filepath.Join(objectsDir, sha1[:])
}

func sha1sum(content []byte) string {
 h := sha1.New()
 h.Write(content)
 return fmt.Sprintf("%x", h.Sum(nil))
}

The “add” command takes a list of file paths as arguments, reads the contents of each file, calculates an SHA-1 hash of the content, and writes the content to the object database directory with the hash as the filename. If the object already exists, it will not be written again. This is similar to Git’s “object” storage system, which stores all files and their contents as objects with unique SHA-1 hashes.

func add(files []string) {
 if len(files) == 0 {
  fmt.Println("Nothing specified, nothing added.")
  return
 }

 for _, file := range files {
  content, err := ioutil.ReadFile(file)
  if err != nil {
   log.Fatalf("Error reading file %s: %v", file, err)
  }

  objectPath := getObjectPath(content)

  if _, err := os.Stat(objectPath); os.IsNotExist(err) {
   err := ioutil.WriteFile(objectPath, content, 0644)
   if err != nil {
    log.Fatalf("Error writing object file %s: %v", objectPath, err)
   }
  }
 }
}

“Commit” command:

In the “commit” command, we first get the list of objects in the object database directory using ioutil.ReadDir(). We then create a commit object that references the current state of the object database, by calculating the SHA-1 hash of the object list and using it as the tree object. We also include the commit message in the commit object. Finally, we write the commit object to the commits directory with the timestamp as the filename.

func commit(args []string) {
 if len(args) == 0 {
  fmt.Println("Please provide a commit message.")
  os.Exit(1)
 }

 message := strings.Join(args, " ")

 // Get the list of objects to include in the commit
 objects, err := ioutil.ReadDir(objectsDir)
 if err != nil {
  log.Fatalf("Error reading objects directory %s: %v", objectsDir, err)
 }

 // Create a new commit object
 var objectLines []string
 for _, object := range objects {
  content, err := ioutil.ReadFile(filepath.Join(objectsDir, object.Name()))
  if err != nil {
   log.Fatalf("Error reading object file %s: %v", object.Name(), err)
  }
  objectLines = append(objectLines, object.Name()+" "+sha1sum(content))
 }
 commitContent := []byte(fmt.Sprintf("tree %s\n\n%s\n", sha1sum([]byte(strings.Join(objectLines, "\n"))), message))
 commitPath := filepath.Join(commitsDir, time.Now().Format("2006-01-02T15-04-05"))
 if err := ioutil.WriteFile(commitPath, commitContent, 0644); err != nil {
  log.Fatalf("Error writing commit file %s: %v", commitPath, err)
 }
 fmt.Printf("Created commit %s\n", commitPath)
}

“Log” command:

In the “log” command, we get the list of commits in the commits directory using ioutil.ReadDir(), and loop through them in reverse order using a for loop. For each commit, we read the commit file's content using ioutil.ReadFile() and print the SHA-1 hash and commit message to the console using fmt.Printf() and strings.TrimSpace().

func logCommand() {
 commits, err := ioutil.ReadDir(commitsDir)
 if err != nil {
  log.Fatalf("Error reading commits directory %s: %v", commitsDir, err)
 }
 for i := len(commits) - 1; i >= 0; i-- {
  commit := commits[i]
  content, err := ioutil.ReadFile(filepath.Join(commitsDir, commit.Name()))
  if err != nil {
   log.Fatalf("Error reading commit file %s: %v", commit.Name(), err)
  }
  fmt.Printf("\n%s\n\n", strings.TrimSpace(string(content)))
 }
}

Conclusion

You can find the code for this program and test it out using the repository at https://github.com/SaratAngajalaoffl/go-git.

This is a very bare-bones implementation of git that highlights some basic concepts. This blog is to kick-start your understanding of git and to push you to explore the git protocol further.


This is the end of today’s article. Any feedback is appreciated.

You can find my socials below if you want to connect.

Twitter - https://twitter.com/sarat_angajala

Linkedin- https://linkedin.com/in/saratangajala

Did you find this article valuable?

Support Sarat Angajala by becoming a sponsor. Any amount is appreciated!