package main import ( "fmt" "path/filepath" "slices" "go.wit.com/lib/protobuf/gitpb" "go.wit.com/log" ) var ErrorReposHasLocalBranches error = fmt.Errorf("repo still has local branches") var ErrorMergeBranch error = fmt.Errorf("trunk has things not in the branch") var ErrorMergeTrunk error = fmt.Errorf("branch has things not in trunk") func doClean() error { if argv.Clean.Devel != nil { if err := doCleanDevel(); err != nil { badExit(err) } log.Info("finished attempt at cleaning devel branches") return nil } if argv.Clean.User != nil { if err := doCleanUser(); err != nil { log.Info(err) okExit("") } return nil } return nil } func doCleanUser() error { if _, count, _, err := IsEverythingOnMaster(); err != nil { if count == 0 { log.Info("No repos are on the master branch") return nil } log.Info("Not all repos are on the master branch") // return err } all := me.forge.Repos.SortByFullPath() for all.Scan() { repo := all.Next() if err := doCleanUserRepo(repo); err != nil { log.Info(repo.GetGoPath(), err) return err } } return nil } func doCleanDevel() error { var total int var count int all := me.forge.Repos.SortByFullPath() for all.Scan() { repo := all.Next() if argv.Verbose { // log.Info("Cleaning:", repo.GetGoPath()) } total += 1 if repo.GetCurrentBranchName() != repo.GetDevelBranchName() { // only process branches in devel // return nil } if repo.IsDirty() { return nil } count += 1 if err := doCleanDevelRepo(repo); err != nil { log.Info(repo.GetGoPath(), err) return err } } log.Printf("attempted cleaning %d branches of %d total branches\n", count, total) return nil } func exactDevelRepo(repo *gitpb.Repo) error { devel := repo.GetDevelBranchName() master := repo.GetMasterBranchName() err := isBranchSubsetOfTrunk(repo, devel, master) if err != nil { return err } return nil } func checkhashes(repo *gitpb.Repo, hashes []string, refpath string) ([]string, error) { if !repo.Exists(refpath) { return hashes, nil } r, err := repo.RunStrictNew([]string{"cat", refpath}) if err != nil { return hashes, err } newhash := r.Stdout[0] for _, hash := range hashes { if newhash != hash { return hashes, fmt.Errorf("%s hash broke %s %s", repo.GetGoPath(), newhash, hash) } } hashes = append(hashes, newhash) return hashes, nil } func doCleanDevelRepo(repo *gitpb.Repo) error { var hashes []string devel := repo.GetDevelBranchName() // log.Printf("%s Start verify devel branch: %s\n", repo.GetGoPath(), devel) // check if devel branch exists in remote repo if repo.Exists(filepath.Join(".git/refs/remotes/origin", devel)) { if argv.Verbose { var err error if hashes, err = checkhashes(repo, hashes, filepath.Join(".git/refs/remotes/origin", devel)); err != nil { return err } } remote := filepath.Join("origin", devel) if err := isBranchSubsetOfTrunk(repo, devel, remote); err != nil { if err == ErrorMergeBranch { log.Info("can not do this yet. need push to upstream", repo.GetGoPath()) if argv.Force { return nil } return err } return err } // log.Info("todo: verify against remote devel branch", repo.GetGoPath()) } // verify devel branch is subset of master branch master := repo.GetMasterBranchName() if argv.Verbose { var err error if hashes, err = checkhashes(repo, hashes, filepath.Join(".git/refs/heads", devel)); err != nil { return err } } if argv.Verbose { var err error if hashes, err = checkhashes(repo, hashes, filepath.Join(".git/refs/heads", devel)); err != nil { return err } } if argv.Verbose { log.Info("repo hashes all match. incredible", hashes, repo.GetGoPath()) } if err := isBranchSubsetOfTrunk(repo, devel, master); err != nil { if err == ErrorMergeBranch { if argv.Force { if repo.GetCurrentBranchName() == devel { cmd := []string{"git", "merge", master} // only run this if branch is local _, err := repo.RunVerbose(cmd) return err } else { cmd := []string{"git", "merge", master} log.Info("can't run. on wrong branch.", cmd, repo.GetGoPath(), "current branch =", repo.GetCurrentBranchName()) } return nil } return err } return err } // log.Info("todo: verify against remote devel branch", repo.GetGoPath()) return nil } // removes all local branches func doCleanUserRepo(repo *gitpb.Repo) error { var hasLocal bool if repo.GitConfig == nil { return fmt.Errorf("GitConfig == nil") } if repo.GetCurrentBranchName() != repo.GetMasterBranchName() { // skip this while in devel return nil } if repo.IsDirty() { return nil } for _, l := range repo.GitConfig.Local { log.Info("\tlocal branch name:", l.Name) } if argv.Clean.User != nil { base := repo.GetUserBranchName() if repo.Exists(filepath.Join(".git/refs/heads", base)) { log.Info("Delete local branch:", base, repo.GetGoPath()) err := forceDeleteUserBranch(repo, base) repo.Reload() if err != nil { log.Info("Delete local branch ERROR:", err) return err } // return fmt.Errorf("todo") } return nil } for name, b := range repo.GitConfig.Branches { if b.Name == "" { b.Name = name } if name == repo.GetMasterBranchName() { // never delete the master branch // todo: make sure the master branch is in sync with remote master continue } if name == repo.GetUserBranchName() { hasLocal = true if err := doCleanUserBranch(repo, b); err != nil { log.Info("\tLOCAL BRANCH ERROR user =", name) return err } log.Info("\tLOCAL BRANCH user =", name) continue } if name == repo.GetDevelBranchName() { continue } if name == repo.GetMasterBranchName() { continue } if err := verifyLocalBranchIsMerged(repo, b); err != nil { return err } } if argv.Clean.Force == nil { if hasLocal { return ErrorReposHasLocalBranches } } return nil } func verifyLocalBranchIsMerged(repo *gitpb.Repo, branch *gitpb.GitBranch) error { base := filepath.Base(branch.Name) log.Info("local branch name unknown:", base, branch.Name, branch.Merge, branch.Remote, repo.GetGoPath()) // check if it exists in the origin if repo.Exists(filepath.Join(".git/refs/remotes/origin", base)) { err := fmt.Errorf("repo %s ERROR. branch is also remote %s", repo.GetGoPath(), branch.Name) log.Info(err) if err := isSafeToDelete(repo, base); err != nil { log.Info(err) return err } if err := requiresGitPush(repo, base); err != nil { log.Info(err) return err } err = fmt.Errorf("repo %s BRANCH AND REMOTE CAN BE DELETED %s", repo.GetGoPath(), branch.Name) log.Info(err) if argv.Clean.Force != nil { err = BADforceDeleteBranch(repo, base) repo.Reload() } return err } if !repo.Exists(filepath.Join(".git/refs/heads", base)) { log.Info("GitConfig() parse logic error. not really a local branch", base) me.forge.Repos.Delete(repo) configSave = true return nil } // this is only a local branch if err := isSafeToDelete(repo, base); err != nil { log.Info(err) return err } err := fmt.Errorf("repo %s BRANCH CAN PROBABLY BE DELETED base=%s fullname=%s", repo.GetGoPath(), base, branch.Name) log.Info(err) if argv.Clean.Force != nil { err = BADforceDeleteBranch(repo, base) repo.Reload() } return err } func doCleanUserBranch(repo *gitpb.Repo, branch *gitpb.GitBranch) error { if branch.Name != repo.GetUserBranchName() { return fmt.Errorf("repo %s was not user branch %s", repo.GetGoPath(), branch.Name) } /* if bran == repo.GetUserBranchName() { log.Info("The user branch also has a remote branch", repo.CurrentTag.Refname) log.Info("TODO: verify the remote branch is out of date", repo.CurrentTag.Refname) log.Info("TODO: delete the remote branch", repo.CurrentTag.Refname) return nil } */ if !repo.Exists(filepath.Join(".git/refs/heads", branch.Name)) { err := fmt.Errorf("BAD FORGE LOGIC. DELETE REPO %s", repo.GetGoPath()) log.Warn(err) me.forge.Repos.Repos = slices.DeleteFunc(me.forge.Repos.Repos, func(r *gitpb.Repo) bool { if repo.GetGoPath() == r.GetGoPath() { return true } return false }) // me.forge.Repos.Delete(repo) me.forge.ConfigSave() // os.Exit(0) return nil } if repo.Exists(filepath.Join(".git/refs/remotes/origin", branch.Name)) { log.Info("why is this non-localonly branch a problem?", branch.Name) repo.RunVerbose([]string{"ls", "-l", ".git/refs/remotes/origin"}) repo.RunVerbose([]string{"cat", filepath.Join(".git/refs/remotes/origin", branch.Name)}) repo.RunVerbose([]string{"cat", filepath.Join(".git/refs/heads", branch.Name)}) if err := userToDevelRequiresGitPush(repo, branch.Name); err != nil { log.Info(err) log.Info("THIS USER BRANCH MUST BE PUSHED TO DEVEL", branch.Name) return err } log.Info("THIS USER BRANCH IS CLEAN TO DELETE", branch.Name) if argv.Clean.Force != nil { err := BADforceDeleteBranch(repo, branch.Name) repo.Reload() if err != nil { return err } } } else { log.Info("why is this local only branch a problem?", branch.Name) repo.RunVerbose([]string{"ls", "-l", ".git/refs/remotes/origin"}) r, err := repo.RunVerbose([]string{"cat", filepath.Join(".git/refs/heads", branch.Name)}) if err == nil { cmd := []string{"git", "show", "-s", "--format=\"%H %ae %as %s\"", r.Stdout[0]} repo.RunVerbose(cmd) log.Info(cmd) } if err := userToDevelRequiresGitPush(repo, branch.Name); err != nil { log.Info(err) log.Info("THIS USER BRANCH MUST BE PUSHED TO DEVEL", branch.Name) return err } log.Info("THIS USER BRANCH IS CLEAN TO DELETE", branch.Name) if argv.Clean.Force != nil { cmd := []string{"git", "branch", "-D", branch.Name} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) return err } return nil } } // return fmt.Errorf("%s repo.CurrentTag is not local: %s. Don't proceed yet", repo.GetGoPath(), repo.CurrentTag.Refname) log.Printf("Do something %s on branch name:%s merge:%s remote:%s\n", repo.GetGoPath(), branch.Name, branch.Merge, branch.Remote) return nil } func userToDevelRequiresGitPush(repo *gitpb.Repo, branchName string) error { devel := repo.GetDevelBranchName() missing := countDiffObjects(repo, branchName, "origin/"+devel) b2 := countDiffObjects(repo, "origin/"+devel, branchName) log.Info("user vs devel count", missing, b2) if missing == 0 && b2 == 0 { return nil } if missing != 0 { log.Info("user vs devel count missing != 0, b2 ==", missing, b2) log.Info("THIS MEANS THE LOCAL BRANCH NEEDS GIT PUSH TO ORIGIN BRANCH ==", missing) // if argv.Examine.Fix != nil { // return gitPushStrict(repo, branchName) // } return fmt.Errorf("user branch not clean to delete %d %d", missing, b2) } if missing == 0 { // log.Info("THIS MEANS THE LOCAL BRANCH IS OK TO DELETE missing =", missing) log.Info("THIS IS REALLY BAD RIGHT NOW. must to git push / git merge missing =", missing, b2) } return fmt.Errorf("user branch not clean to delete. maybe it is? devel might be ahead of user branch. %d %d", missing, b2) } // checks against upstream master func isSafeToDelete(repo *gitpb.Repo, old string) error { var head string head = filepath.Join("origin", repo.GetMasterBranchName()) if !repo.Exists(filepath.Join(".git/refs/remotes/", head)) { head = filepath.Join("origin", "HEAD") } b1 := countDiffObjects(repo, old, head) b2 := countDiffObjects(repo, head, old) log.Info(old, "vs origin count", b1, b2) if b1 == 0 && b2 == 0 { log.Info("isSafeToDelete() SAFE TO DELETE ==", old, b1, head, b2) return nil } if b1 == 0 { log.Info("isSafeToDelete() SAFE TO DELETE ==", old, b1, head, b2) return nil } if b1 != 0 { log.Info(old, "vs", head, " count b1 != 0, b2 ==", b2, "b1 =", b1) log.Info("THIS MEANS THE LOCAL BRANCH NEEDS GIT PUSH TO ORIGIN BRANCH ==", b1) cmd := repo.ConstructGitDiffLog(old, head) log.Info("cmd", cmd) cmd = repo.ConstructGitDiffLog(head, old) log.Info("cmd", cmd) if argv.Clean.Force != nil { // return gitPushStrict(repo, branchName) } return fmt.Errorf("user branch not clean to delete. needs git push %d %d", b1, b2) } return fmt.Errorf("user branch not clean to delete. needs git push %d %d", b1, b2) } // literally ignore all errors. delete everthing with no checks for now func forceDeleteUserBranch(repo *gitpb.Repo, branch string) error { if repo.IsDirty() { log.Info(repo.GetGoPath(), "is dirty") return nil } if repo.GetUserBranchName() != branch { log.Info(repo.GetGoPath(), branch, "is not the user branch", repo.GetUserBranchName()) return nil } if err := isSafeToDelete(repo, branch); err != nil { log.Info(err) return err } if repo.Exists(filepath.Join(".git/refs/remotes/origin/", branch)) { if err := requiresGitPush(repo, branch); err != nil { log.Info(err) return err } } configSave = true cmd := []string{"git", "branch", "-D", branch} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) // return err } log.Info("THIS USER REMOTE BRANCH MUST BE DELETED HERE", branch) if repo.Exists(filepath.Join(".git/refs/remote/origin", branch)) { // git push origin --delete jcarr cmd = []string{"git", "push", "origin", "--delete", branch} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) // return err } } cmd = []string{"git", "branch", "-D", "--remote", "origin/" + branch} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) // return err } // return fmt.Errorf("one at a time %s", repo.GetGoPath()) return nil } // literally ignore all errors. delete everthing with no checks for now func BADforceDeleteBranch(repo *gitpb.Repo, branch string) error { if repo.IsDirty() { log.Info(repo.GetGoPath(), "is dirty") return nil } configSave = true cmd := []string{"git", "branch", "-D", branch} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) // return err } log.Info("THIS USER REMOTE BRANCH MUST BE DELETED HERE", branch) if repo.Exists(filepath.Join(".git/refs/remote/origin", branch)) { // git push origin --delete jcarr cmd = []string{"git", "push", "origin", "--delete", branch} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) // return err } } cmd = []string{"git", "branch", "-D", "--remote", "origin/" + branch} if _, err := repo.RunVerbose(cmd); err != nil { log.Info("THE GIT BRANCH DELETE ERROR IS:", err) // return err } // return fmt.Errorf("one at a time %s", repo.GetGoPath()) return nil } // verifies that the branch is a pure subset of the other branch // sorry about the 'master' 'slave' nameing thing. I guess that isn't // 'cool' to use anymore. I can't think of other terms that aren't reserved words. func isBranchSubsetOfTrunk(repo *gitpb.Repo, branch string, trunk string) error { b1 := countGitDiffLog(repo, branch, trunk) // should be zero b2 := countGitDiffLog(repo, trunk, branch) // can be greater than 1 // log.Info(branch, "vs", trunk, "count", b1, b2) if b1 == 0 && b2 == 0 { // log.Info("branch and trunk are identical ==", branch, b1, trunk, b2) return nil } if argv.Verbose { log.Printf("%-40s NOT EXACT %s %s (%d) (%d)\n", repo.GetGoPath(), branch, trunk, b1, b2) } if b1 == 0 { cmd := []string{"git", "merge", trunk} log.Printf("%-40s branch %s needs merge with trunk %s len(%d) %s\n", repo.GetGoPath(), branch, trunk, b2, cmd) return ErrorMergeBranch } if b2 == 0 { log.Printf("%-40s trunk %s needs merge with branch %s len(%d)\n", repo.GetGoPath(), branch, trunk, b2) return ErrorMergeTrunk } return fmt.Errorf("branch not clean to delete. needs merge %d %d", b1, b2) } // count all objects only in branch1 // if zero, that means branch1 is entirely contained in branch2 and can be safely deleted func countGitDiffLog(repo *gitpb.Repo, branch1, branch2 string) int { cmd := repo.ConstructGitDiffLog(branch1, branch2) r, err := repo.RunStrictNew(cmd) if err != nil { return -1 } // log.Info("countDiffObjects()", cmd, len(r.Stdout), strings.Join(r.Stdout, " ")) return len(r.Stdout) }