NTL

Learning Go by building a git manager

Recently I decided to try Golang. I had been wanting to make a project to view all the git repositories on my computer and I figured this would be the perfect opportunity.

I started by defining some constants and structures to handle things

const MAX_DEPTH = 10
const MAX_SUB_DEPTH = 5

type GitFile struct {
  Mod string
  Name string
}

type Status struct {
  IsCurrent bool
  Files []*GitFile
}

type Repo struct {
  Path string
  IsRepoInRepo bool
  Depth int
  Status *Status
}

This method essentially just loops through all directories in my home directory and, while depth is less than the specified max depth, it will check if it contains the .git directory. The function will recursively check the repo for any submodules. I track the depth as well as sub depth for each call

func GetAllRepos(path string, repos []
Repo, depth int, sub_check bool) error {
  dirs, err := os.ReadDir(path)
  if err != nil {
    return err
  }
  for _, dir := range dirs {
    if dir.IsDir() && dir.Name() == ".git" {
      if sub_check {
        repos = append(
repos, newRepo(path, sub_check, depth))
      } else {
        repos = append(
repos, newRepo(path, sub_check, 0))
      }
      sub_check = true
    }
  }
  if sub_check && depth < MAX_SUB_DEPTH {
    for _, dir := range dirs {
      if dir.IsDir() && !strings.HasPrefix(dir.Name(), ".") {
        GetAllRepos(path+"/"+dir.Name(), repos, depth+1, sub_check)
      }
    }
  }
  if !sub_check && depth < MAX_DEPTH {
    for _, dir := range dirs {
      if dir.IsDir() && !strings.HasPrefix(dir.Name(), ".") {
        GetAllRepos(path+"/"+dir.Name(), repos, depth+1, sub_check)
      }
    }
  }
  return nil
}

Once a repo is found, I check the status with the following methods

func CheckRepoStatus(repo *Repo) error {
  bytes, err := exec.Command("git", "-C", repo.Path, "status", "--porcelain").Output()
  if err != nil {
    return err
  }
  if len(bytes) == 0 {
    repo.Status, err = ParseStatus("-")
    if err != nil {
      return err
    }
  } else {
    repo.Status, err = ParseStatus(fmt.Sprintf("%s", bytes))
    if err != nil {
      return err
    }
  }
  return nil
}

func ParseStatus(status string) (*Status, error) { 
  s := newStatus(true, nil)
  if status == "-" {
    return s, nil
  }
  files := []*GitFile{}
  lines := strings.Split(status, "\n")
  for _, line := range lines {
    file := ParseStatusLine(line)
    if file != nil {
      files = append(files, file)
    }
  }
  s.IsCurrent = false
  s.Files = files
  return s, nil
}

And printing

func PrintRepos(repos []
Repo) {
  if len(*repos) > 0 {
    for _, repo := range *repos {
      if repo.IsRepoInRepo {
        utils.Print(repo.Path, utils.Underline, utils.Magenta, utils.Bold)
        utils.Println(" - repo in a repo", utils.Purple)
      } else {
        utils.Println(repo.Path, utils.Underline, utils.Magenta, utils.Bold)
      }
      PrintStatus(repo)
      utils.Println("")
    }
  } else {
    utils.Println("No repos given", utils.Red)
  }
}

func PrintStatus(repo *Repo) {
  if repo.Status.IsCurrent {
    utils.Println("-Repo is up to date-", utils.Green)
  } else {
    for _, file := range repo.Status.Files {
      utils.Print(file.Mod, utils.Red)
      utils.Print(" ")
      utils.Println(file.Name, utils.Red)
    }
  }
}

This is how I use it

repos := []*git.Repo{}
home_dir, err := os.UserHomeDir()
if err != nil {
  utils.Println(err.Error(), utils.Red)
  log.Fatal(err)
}

git.GetAllRepos(home_dir, &repos, 0, false)
if len(repos) > 0 {
  for _, repo := range repos {
    err := git.CheckRepoStatus(repo)
    if err != nil {
      utils.Println(err.Error(), utils.Red)
      log.Fatal(err)
    }
  }
  git.PrintRepos(&repos)
} else {
  utils.Println("No repos found", utils.Red)
}

I set up a shortcut on my machine that will open a new terminal and run the executable, allowing for me to quickly see a list of all repos and their status

bindsym $mod+p exec xterm -hold -e "/home/nathaniel/go_tools/main"