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"