// Copyright 2017-2025 WIT.COM Inc. All rights reserved. // Use of this source code is governed by the GPL 3.0 package main import ( "errors" "fmt" "path/filepath" "go.wit.com/lib/env" "go.wit.com/lib/fhelp" "go.wit.com/lib/protobuf/gitpb" "go.wit.com/log" ) func doResetRepo(repo *gitpb.Repo) error { if repo.GetCurrentBranchName() != repo.GetMasterBranchName() { return log.Errorf("not on master branch") } if repo.IsDirty() { return log.Errorf("repo is dirty") } // when publishing, clean out the details of that if it's still there if repo.GetTargetVersion() != "" { repo.SetTargetVersion("") } // try to delete user if err := doRepoCleanUser(repo); err != nil { return err } // try to delete devel err := doRepoCleanDevel(repo) return err } // reverts all repos back to the original master branches // automatically deletes local devel and user branches func doClean() (string, error) { var s string var err error var allerr error if me.forge.IsModeNormal() { s := fmt.Sprintf("Reset all (%d) git repos to the original state (non-destructive)?", me.forge.Repos.Len()) if !fhelp.QuestionUser(s) { s, err = doModeMaster() allerr = errors.Join(allerr, err) } } me.forge.SetCleaningMode() // fix this to work, then delete all the other options for "forge clean' if err := me.forge.DoAllCheckoutMaster(); err != nil { // badExit(err) } me.forge.RescanRepos() // looks for new dirs, checks existing repos for changes notclean := gitpb.NewRepos() all := me.forge.Repos.SortByFullPath() for all.Scan() { repo := all.Next() err := doResetRepo(repo) if err != nil { newr := notclean.Clone(repo) newr.State = fmt.Sprintf("%v", err) allerr = errors.Join(allerr, err) } } if notclean.Len() > 0 { if env.True("--force") { // try to force the issue for r := range notclean.IterAll() { log.Info("FORCE THE ISSUE", r.FullPath) } } else { log.Info("NOT FORCEING THE ISSUE") } footer := me.forge.NormalCheckTB(notclean) s = fmt.Sprintf("(%d) not clean repos %s", notclean.Len(), footer) allerr = errors.Join(allerr, errors.New(s)) return s, allerr } return "all repos are clean", nil } func doRepoCleanDevel(repo *gitpb.Repo) error { if !repo.IsLocalBranch(repo.GetDevelBranchName()) { // there is no local branch named 'devel' return nil } if repo.GetCurrentBranchName() != repo.GetMasterBranchName() { return log.Errorf("%s not on master branch:", repo.GetFullPath()) } if repo.IsDirty() { return log.Errorf("%s is dirty:", repo.GetFullPath()) } if err := justDeleteTheDevelBranchAlready(repo); err != nil { log.Info("justDeleteTheDevel() err", repo.Namespace, err) return err } return nil } // removes all local user branches func doRepoCleanUser(repo *gitpb.Repo) error { if repo.IsDirty() { return nil } bruser := repo.GetUserBranchName() brdevel := repo.GetDevelBranchName() brmaster := repo.GetMasterBranchName() if repo.IsBranchRemote(bruser) { b2, err := repo.CountDiffObjects("refs/heads/"+bruser, "refs/heads/"+brmaster) // should be zero if err != nil { log.Info(repo.FullPath, "doRepoCleanUser() bad branches") return err } if b2 == 0 { log.Info("Local remote branch is safe to delete (is completely in master)", bruser) cmd := []string{"git", "branch", "-D", bruser} s := log.Sprintf("Run: %v in %s", cmd, repo.FullPath) if !argv.Force { if fhelp.QuestionUser(s) { repo.RunVerbose([]string{"git", "branch", "-D", bruser}) // repo.RunVerbose([]string{"git", "checkout", bname}) } } else { repo.RunVerbose([]string{"git", "branch", "-D", bruser}) } } log.Info("forge is designed to always have local only user branches", bruser) return fmt.Errorf("forge is designed to always have local only user branches") } if !repo.IsLocalBranch(bruser) { // there is no local user branch return nil } // will you loose work if you delete your user branch? // if DevelBranchExists() // then if UserBranchCommits exist in DevelBranch // DeleteUserBranch is safe if repo.IsLocalBranch(brdevel) { b1, err := repo.CountDiffObjects(bruser, "refs/heads/"+brdevel) // should be zero if err != nil { log.Info(repo.FullPath, "doRepoCleanUser() bad branches") return err } if b1 == 0 { // every user branch exists in devel. delete user branch cmd := []string{"git", "branch", "-D", bruser} // log.Info("USER IS IN DEVEL", repo.Namespace, cmd) _, err := repo.RunVerboseOnError(cmd) return err } } // will you loose work if you delete your user branch? // if master branch exists() // then if all user commits exist in master // delete user branch is safe if repo.IsLocalBranch(brmaster) { b1, err := repo.CountDiffObjects(bruser, "refs/heads/"+brmaster) // should be zero if err != nil { log.Info(repo.FullPath, "doRepoCleanUser() bad branches") return err } if b1 == 0 { cmd := []string{"git", "branch", "-D", bruser} // log.Info("USER IS IN DEVEL", repo.Namespace, cmd) _, err := repo.RunVerboseOnError(cmd) return err } } return ErrorBranchUnique } var ErrorBranchUnique error = errors.New("branch has unique commits") // if you call this, there is no going back. no checks anymore. nothing // it deletes the 'devel' branch. git branch -D "devel". END OF STORY func justDeleteTheDevelBranchAlready(repo *gitpb.Repo) error { branch := repo.GetDevelBranchName() remote := filepath.Join("origin", branch) if me.forge.Config.IsReadOnly(repo.Namespace) { if repo.IsDevelRemote() { // just make sure the remote & local branches are the same return repo.DeleteLocalDevelBranch() } } // check against remote if it exists if repo.IsDevelRemote() { b1, err := repo.CountDiffObjects(branch, remote) // should be zero if err != nil { log.Info(repo.FullPath, "doRepoCleanUser() bad branches") return err } if b1 == 0 { cmd := []string{"git", "branch", "-D", repo.GetDevelBranchName()} // log.Info("DEVEL IS IN REMOTE", repo.Namespace, cmd) _, err := repo.RunVerboseOnError(cmd) return err } cmd := []string{"git", "push"} log.Info("DEVEL LOCAL NEEDS GIT PUSH TO REMOTE", repo.Namespace, cmd) err = repo.RunVerbose(cmd) return err } // remote doesn't exist, check against master master := repo.GetMasterBranchName() b1, err := repo.CountDiffObjects(branch, "refs/heads/"+master) // should be zero if err != nil { log.Info(repo.FullPath, "doRepoCleanUser() bad branches") return err } if b1 == 0 { cmd := []string{"git", "branch", "-D", repo.GetDevelBranchName()} // log.Info("DEVEL IS IN REMOTE", repo.Namespace, cmd) _, err := repo.RunVerboseOnError(cmd) return err } cmd := []string{"git", "merge something somehow"} log.Info("devel local, remote and master branches are wrong", repo.Namespace, cmd, b1) // _, err := repo.RunVerbose(cmd) return nil } func doGitReset() { all := me.forge.Repos.SortByFullPath() for all.Scan() { repo := all.Next() if me.forge.Config.IsReadOnly(repo.Namespace) { // log.Info("is readonly", repo.Namespace) if repo.CheckDirty() { log.Info("is readonly and dirty", repo.Namespace) cmd := []string{"git", "reset", "--hard"} repo.RunRealtime(cmd) } } else { // log.Info("is not readonly", repo.Namespace) } } } func checkRemoteBranches(repo *gitpb.Repo) error { if err := repo.ReloadCheck(); err != nil { log.Info("need to reload", repo.FullPath) } if repo.VerifyRemoteAndLocalBranches(repo.GetDevelBranchName()) { } else { return log.Errorf("remote devel is out of sync with local: todo: git pull or git fetch") } if repo.VerifyRemoteAndLocalBranches(repo.GetMasterBranchName()) { } else { return log.Errorf("remote master is out of sync with local: todo: git pull or git fetch") } return nil } func checkPatchIds(repo *gitpb.Repo, b1 string, b2 string) error { var safe bool = true var baderr error ids := make(map[string]string) s1 := fmt.Sprintf("%s..%s", b1, b2) s2 := fmt.Sprintf("%s..%s", b2, b1) if stats, err := repo.RunStrict([]string{"git", "rev-list", s1}); err != nil { return err } else { for _, hash := range stats.Stdout { patchId, err := repo.FindPatchIdByHash(hash) if err != nil { baderr = err safe = false continue } ids[patchId] = hash } } if stats, err := repo.RunStrict([]string{"git", "rev-list", s2}); err != nil { return err } else { for _, hash := range stats.Stdout { patchId, err := repo.FindPatchIdByHash(hash) if err != nil { baderr = err safe = false continue } if val, ok := ids[patchId]; ok { log.Info(patchId, hash, "already here as", val) } else { log.Info(hash, "this probably means this branch is not safe to delete") safe = false ids[patchId] = hash } } } for key, val := range ids { log.Info(key, val) } if safe { log.Info("branch is probably safe to delete", b1, b2) } else { log.Info("branch is probably not safe to delete", b1, b2, baderr) } return baderr }