forge/doClean.go

538 lines
16 KiB
Go
Raw Normal View History

2025-01-18 15:50:06 -06:00
package main
import (
"fmt"
2025-01-19 02:37:04 -06:00
"path/filepath"
"slices"
2025-01-18 15:50:06 -06:00
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
2025-01-18 23:25:55 -06:00
var ErrorReposHasLocalBranches error = fmt.Errorf("repo still has local branches")
2025-01-19 16:07:17 -06:00
var ErrorMergeBranch error = fmt.Errorf("trunk has things not in the branch")
var ErrorMergeTrunk error = fmt.Errorf("branch has things not in trunk")
2025-01-18 23:25:55 -06:00
2025-01-18 15:50:06 -06:00
func doClean() error {
2025-01-19 16:07:17 -06:00
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
2025-01-18 23:25:55 -06:00
}
2025-01-19 16:07:17 -06:00
return nil
}
2025-01-18 23:25:55 -06:00
2025-01-19 16:07:17 -06:00
func doCleanUser() error {
2025-01-20 01:39:59 -06:00
if _, count, _, err := IsEverythingOnMaster(); err != nil {
2025-01-19 18:02:24 -06:00
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
}
2025-01-18 15:50:06 -06:00
all := me.forge.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
2025-01-19 16:07:17 -06:00
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())
2025-01-19 16:07:17 -06:00
}
total += 1
if repo.GetCurrentBranchName() != repo.GetMasterBranchName() {
// repos must be in the master branch to clean the devel branch
return nil
2025-01-18 23:25:55 -06:00
}
2025-01-19 03:39:35 -06:00
if repo.IsDirty() {
2025-01-19 16:07:17 -06:00
return nil
2025-01-19 03:39:35 -06:00
}
2025-01-19 16:07:17 -06:00
count += 1
if err := doCleanDevelRepo(repo); err != nil {
2025-01-19 07:05:45 -06:00
log.Info(repo.GetGoPath(), err)
2025-01-19 16:07:17 -06:00
return err
2025-01-18 15:50:06 -06:00
}
}
2025-01-19 16:07:17 -06:00
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
}
2025-01-19 16:07:17 -06:00
func doCleanDevelRepo(repo *gitpb.Repo) error {
var hashes []string
2025-01-19 16:07:17 -06:00
devel := repo.GetDevelBranchName()
if argv.Verbose {
log.Printf("Start clean devel branch: %s %s\n", repo.GetGoPath(), devel)
}
2025-01-19 16:07:17 -06:00
// 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
}
}
2025-01-19 16:07:17 -06:00
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
2025-01-19 16:07:17 -06:00
}
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())
}
2025-01-19 16:07:17 -06:00
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
2025-01-19 16:07:17 -06:00
}
return err
2025-01-19 16:07:17 -06:00
}
return err
}
// log.Info("todo: verify against remote devel branch", repo.GetGoPath())
2025-01-18 15:50:06 -06:00
return nil
}
2025-01-18 23:25:55 -06:00
// removes all local branches
2025-01-19 16:07:17 -06:00
func doCleanUserRepo(repo *gitpb.Repo) error {
2025-01-18 23:25:55 -06:00
var hasLocal bool
2025-01-18 15:50:06 -06:00
if repo.GitConfig == nil {
return fmt.Errorf("GitConfig == nil")
}
2025-01-19 16:07:17 -06:00
if repo.GetCurrentBranchName() != repo.GetMasterBranchName() {
// skip this while in devel
return nil
}
if repo.IsDirty() {
return nil
}
2025-01-18 15:50:06 -06:00
for _, l := range repo.GitConfig.Local {
log.Info("\tlocal branch name:", l.Name)
}
2025-01-19 08:48:17 -06:00
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
}
2025-01-18 15:50:06 -06:00
for name, b := range repo.GitConfig.Branches {
2025-01-19 00:35:58 -06:00
if b.Name == "" {
b.Name = name
}
2025-01-18 23:25:55 -06:00
if name == repo.GetMasterBranchName() {
2025-01-19 00:35:58 -06:00
// never delete the master branch
// todo: make sure the master branch is in sync with remote master
2025-01-18 23:25:55 -06:00
continue
}
2025-01-19 00:35:58 -06:00
if name == repo.GetUserBranchName() {
2025-01-19 02:56:48 -06:00
hasLocal = true
2025-01-19 00:35:58 -06:00
if err := doCleanUserBranch(repo, b); err != nil {
2025-01-19 07:05:45 -06:00
log.Info("\tLOCAL BRANCH ERROR user =", name)
2025-01-19 00:35:58 -06:00
return err
}
2025-01-19 07:05:45 -06:00
log.Info("\tLOCAL BRANCH user =", name)
2025-01-19 00:35:58 -06:00
continue
}
if name == repo.GetDevelBranchName() {
2025-01-19 16:07:17 -06:00
continue
}
if name == repo.GetMasterBranchName() {
2025-01-19 00:35:58 -06:00
continue
}
2025-01-19 07:05:45 -06:00
if err := verifyLocalBranchIsMerged(repo, b); err != nil {
return err
}
2025-01-18 15:50:06 -06:00
}
2025-01-19 03:39:35 -06:00
if argv.Clean.Force == nil {
if hasLocal {
return ErrorReposHasLocalBranches
}
2025-01-18 23:25:55 -06:00
}
return nil
}
func verifyLocalBranchIsMerged(repo *gitpb.Repo, branch *gitpb.GitBranch) error {
2025-01-19 07:05:45 -06:00
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 {
2025-01-19 08:48:17 -06:00
err = BADforceDeleteBranch(repo, base)
2025-01-19 07:05:45 -06:00
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 {
2025-01-19 08:48:17 -06:00
err = BADforceDeleteBranch(repo, base)
2025-01-19 07:05:45 -06:00
repo.Reload()
}
return err
2025-01-18 15:50:06 -06:00
}
2025-01-19 00:35:58 -06:00
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)
}
2025-01-19 02:37:04 -06:00
/*
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()
2025-01-19 03:39:35 -06:00
// os.Exit(0)
return nil
2025-01-19 02:37:04 -06:00
}
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)})
2025-01-19 02:56:48 -06:00
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 {
2025-01-19 08:48:17 -06:00
err := BADforceDeleteBranch(repo, branch.Name)
2025-01-19 07:05:45 -06:00
repo.Reload()
if err != nil {
return err
}
2025-01-19 02:56:48 -06:00
}
2025-01-19 02:37:04 -06:00
} 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 {
2025-01-19 02:56:48 -06:00
log.Info(err)
log.Info("THIS USER BRANCH MUST BE PUSHED TO DEVEL", branch.Name)
2025-01-19 02:37:04 -06:00
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}
2025-01-19 07:05:45 -06:00
if _, err := repo.RunVerbose(cmd); err != nil {
log.Info("THE GIT BRANCH DELETE ERROR IS:", err)
return err
}
return nil
2025-01-19 02:37:04 -06:00
}
}
// 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)
2025-01-19 00:35:58 -06:00
return nil
}
2025-01-19 02:37:04 -06:00
func userToDevelRequiresGitPush(repo *gitpb.Repo, branchName string) error {
devel := repo.GetDevelBranchName()
2025-01-19 03:39:35 -06:00
missing := countDiffObjects(repo, branchName, "origin/"+devel)
2025-01-19 02:37:04 -06:00
b2 := countDiffObjects(repo, "origin/"+devel, branchName)
2025-01-19 03:39:35 -06:00
log.Info("user vs devel count", missing, b2)
if missing == 0 && b2 == 0 {
2025-01-19 02:37:04 -06:00
return nil
}
2025-01-19 03:39:35 -06:00
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)
2025-01-19 02:37:04 -06:00
// if argv.Examine.Fix != nil {
// return gitPushStrict(repo, branchName)
// }
2025-01-19 03:39:35 -06:00
return fmt.Errorf("user branch not clean to delete %d %d", missing, b2)
}
if missing == 0 {
2025-01-19 03:45:10 -06:00
// 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)
2025-01-19 02:37:04 -06:00
}
2025-01-19 03:39:35 -06:00
return fmt.Errorf("user branch not clean to delete. maybe it is? devel might be ahead of user branch. %d %d", missing, b2)
2025-01-19 02:37:04 -06:00
}
2025-01-19 07:05:45 -06:00
// 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
2025-01-19 08:48:17 -06:00
func forceDeleteUserBranch(repo *gitpb.Repo, branch string) error {
2025-01-19 07:05:45 -06:00
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
}
2025-01-19 08:48:17 -06:00
if err := isSafeToDelete(repo, branch); err != nil {
log.Info(err)
return err
}
2025-01-19 09:25:25 -06:00
if repo.Exists(filepath.Join(".git/refs/remotes/origin/", branch)) {
if err := requiresGitPush(repo, branch); err != nil {
log.Info(err)
return err
}
2025-01-19 08:48:17 -06:00
}
2025-01-19 07:05:45 -06:00
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)
2025-01-19 08:48:17 -06:00
if repo.Exists(filepath.Join(".git/refs/remote/origin", branch)) {
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}
2025-01-19 07:05:45 -06:00
if _, err := repo.RunVerbose(cmd); err != nil {
log.Info("THE GIT BRANCH DELETE ERROR IS:", err)
// return err
}
2025-01-19 08:48:17 -06:00
// 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)) {
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
}
}
2025-01-19 07:05:45 -06:00
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
}
2025-01-19 16:07:17 -06:00
// 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
}
2025-01-20 04:11:53 -06:00
if argv.Verbose {
log.Printf("%-40s NOT EXACT %s %s (%d) (%d)\n", repo.GetGoPath(), branch, trunk, b1, b2)
}
2025-01-19 16:07:17 -06:00
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)
}