// Copyright 2017-2025 WIT.COM Inc. All rights reserved. // Use of this source code is governed by the GPL 3.0 package main import ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "go.wit.com/lib/fhelp" "go.wit.com/lib/protobuf/forgepb" "go.wit.com/lib/protobuf/gitpb" "go.wit.com/log" ) func isPatchingSafe() bool { if me.forge.Config.Mode == forgepb.ForgeMode_NORMAL { return true } log.Info("This patch command is not safe to run now") log.Info("you must reset the state of your git repositories. Run:") log.Info("") log.Info("forge normal (or use --force)") log.Info("") if argv.Force { return true } return false } func doPatch() (string, error) { if argv.Patch.Submit { return doPatchSubmit() } if argv.Patch.Show { curpatches := forgepb.NewPatches() curpatches.Filename = "/tmp/curpatches.pb" if err := curpatches.Load(); err != nil { return "fix curpatches.pb", err } footer := curpatches.PrintTable() return "all current patches: " + footer, nil } if argv.Patch.Get { psets := forgepb.NewSets() newpb, _, _ := psets.HttpPostVerbose(myServer(), "get") footer, err := doPatchGet(newpb) return footer, err } // forces patching to be done in 'NORMAL' mode // forge is too new to be able to handle anything else if !isPatchingSafe() { return "not safe", errors.New("not safe to work on patches") } s, err := doPatchProcess() return s, err } // submit's current working patches func doPatchSubmit() (string, error) { pset, err := me.forge.MakeDevelPatchSet("testing") if err != nil { return "MakeDevelPatchSet(testing)", err } if pset.Patches == nil { return "pset.Patches == nil", err } if pset.Patches.Len() == 0 { return "did not find any patches", nil } footer := pset.PrintTable() _, _, err = pset.HttpPost(myServer(), "new") return footer, err } func doPatchProcess() (string, error) { curpatches := forgepb.NewPatches() curpatches.Filename = "/tmp/curpatches.pb" if err := curpatches.Load(); err != nil { return "fix curpatches.pb", err } // footer := curpatches.PrintTable() // log.Info("START curpatches:", footer) var needfix int for patch := range curpatches.IterAll() { repo := me.forge.Repos.FindByNamespace(patch.Namespace) if repo == nil { // log.Info("no namespace", patch.PatchId, patch.Namespace, patch.Comment) patch.State = "no namespace" continue } if patch.NewHash == "na" { patch.State = "was na" needfix = 1 } newId, newHash, err := isPatchIdApplied(repo, patch) if errors.Is(err, ErrorGitPullOnDirty) { log.Info(patch.PatchId, newId, repo.Namespace, "repo dirty", patch.Comment) patch.State = "repo dirty" // log.Info("a patch with that comment couldn't be found in the repo") } else if err != nil { // log.Info(patch.PatchId, newId, repo.Namespace, err, patch.Comment) patch.State = "BAD applied err" patch.StateChange = "BAD applied err" // return "isPatchIdApplied() error", patch.Error(err) } if newId == "" { // new patch ! // log.Info(patch.PatchId, "newId==''", patch.Comment) } else { if (newId == patch.PatchId) && (newHash == patch.CommitHash) { // log.Info(patch.PatchId, newId, repo.Namespace, "patch made here", patch.Comment) patch.StateChange = "made here" patch.NewHash = "author" continue } if newId == patch.PatchId { patch.NewHash = patch.CommitHash // log.Info(patch.PatchId, newId, repo.Namespace, "patch already applied", patch.Comment) patch.StateChange = "did already" continue } if newId != patch.PatchId { // log.Info(patch.PatchId, newId, repo.Namespace, "probably duplicate subject? (mismatch)", patch.Comment) patch.StateChange = "dup subject?" // try this. it should compute every patch id in the repo // os.Chdir(repo.FullPath) // newNewId, err := searchAllCommits(targetPatchID string) (string, error) { } } // log.Info(patch.PatchId, newId, repo.Namespace, "new patch", patch.Comment) patch.State = "new patch" if !argv.Fix { needfix += 1 } else { log.Info(string(patch.Data)) log.Info("repo:", repo.FullPath, "patch header:", patch.Comment, patch.CommitHash) if fhelp.QuestionUser("apply this patch? (--force to autoapply)") { newhash, err := applyPatch(repo, patch) if err != nil { log.Info("apply results:", newhash, err) } if err != nil { return "git am problem. manually investigate or purge everything and start over", err } } } } // NOW, FINALLY, AFTER A LOT OF WORK, THE FUN PART newpatches := forgepb.NewPatches() for p := range curpatches.IterAll() { if p.NewHash == "author" { // this is your patch continue } if (p.NewHash != "") && p.StateChange == "did already" { // already applied continue } newpatches.Clone(p) } if newpatches.Len() == 0 { s := log.Sprintf("All (%d) current patches are appled. You are completely up to date!", curpatches.Len()) return s, nil } footer := newpatches.PrintTable() log.Info("BRAND NEW PATCHES:", footer) var s string s = log.Sprintf("There are %d new patches. Use --fix to apply them", needfix) curpatches.Save() return s, nil } func applyPatch(repo *gitpb.Repo, p *forgepb.Patch) (string, error) { _, filen := filepath.Split(p.Filename) tmpname := filepath.Join("/tmp", filen) log.Info("saving as", tmpname, p.Filename) raw, err := os.OpenFile(tmpname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return "", err } raw.Write(p.Data) raw.Close() // always run abort first cmd := []string{"git", "am", "--abort"} err = repo.RunVerbose(cmd) cmd = []string{"git", "am", tmpname} err = repo.RunVerbose(cmd) if err != nil { // always run abort after failure. todo: add --keep cmd := []string{"git", "am", "--abort"} err = repo.RunVerbose(cmd) log.Info("git am failed. run 'git am --abort' here") return "", log.Errorf("git am failed") } return "patch applied with git am", nil } func doPatchGet(newpb *forgepb.Sets) (string, error) { // var changed bool curpatches := forgepb.NewPatches() curpatches.Filename = "/tmp/curpatches.pb" if err := curpatches.Load(); err != nil { curpatches.Save() curpatches.Save() log.Info(err) panic("no file") // return // // THIS IS NEEDED? NOTSURE curpatches = forgepb.NewPatches() curpatches.Filename = "/tmp/curpatches.pb" curpatches.Save() } for pset := range newpb.IterAll() { if pset.Patches.Len() == 0 { log.Info("pset is empty", pset.Name) continue } for patch := range pset.Patches.IterAll() { if len(patch.Data) == 0 { continue } patchid, hash, err := gitpb.FindPatchIdFromGitAm(patch.Data) if err != nil { log.Info("git patchid exec err", err) continue } if hash != patch.CommitHash { log.Info("ERROR: patch commit hashes's didn't match", hash, patch.CommitHash) continue } if patchid != patch.PatchId { log.Info("ERROR: patchid's didn't match", patchid, patch.PatchId) continue } found := curpatches.FindByPatchId(patch.PatchId) if found != nil { // already have this patch continue } // gitpb.FindPatchIdFromGitAmBroken(patch.Data) // doesn't os.Exec() log.Info("adding new patch", patch.CommitHash, patch.PatchId, patch.Filename) curpatches.AppendByPatchId(patch) } } curpatches.Save() footer := curpatches.PrintTable() return footer, nil } var ErrorGitPullOnDirty error = errors.New("git comment is not there") func isPatchIdApplied(repo *gitpb.Repo, patch *forgepb.Patch) (string, string, error) { comment := cleanSubject(patch.Comment) os.Chdir(repo.GetFullPath()) newhash, err := findCommitBySubject(repo, comment, patch) if err != nil { return "", "", err } patchId, err := repo.FindPatchIdByHash(newhash) if err != nil { return "", "", err } // log.Infof("%s %s found hash by comment %s \n", patchId, newhash, patch.Comment) return patchId, newhash, nil } // Shows repos that are: // - git dirty repos // - repos with 'user' branch patches not in 'devel' branch // - repos with awaiting master branch verions // // return true if any are found func showWorkRepos() bool { // always run dirty first me.forge.CheckDirtyQuiet() // if no option is given to patch, list out the // repos that have patches ready in them found := findReposWithPatches() found.SortNamespace() if found.Len() == 0 { log.Info("you currently have no repos with patches") return false } else { footer := me.forge.PrintDefaultTB(found) log.Info("repos with patches or unsaved changes:", footer) } return true } func cleanSubject(line string) string { // Regular expression to remove "Subject:" and "[PATCH...]" patterns re := regexp.MustCompile(`(?i)^Subject:\s*(\[\s*PATCH[^\]]*\]\s*)?`) cleaned := re.ReplaceAllString(line, "") return strings.TrimSpace(cleaned) } func findCommitBySubject(repo *gitpb.Repo, subject string, newpatch *forgepb.Patch) (string, error) { parts := strings.Split(subject, " /") subject = parts[0] if subject == "" { return "", errors.New("subject blank. must brute force") } cmd := exec.Command("git", "log", "--pretty=format:%H %s", "--grep="+subject, "-i") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { return "", err } lines := strings.Split(out.String(), "\n") for _, line := range lines { if strings.Contains(strings.ToLower(line), strings.ToLower(subject)) { parts := strings.Fields(line) patchId, _ := repo.FindPatchIdByHash(parts[0]) if patchId == newpatch.PatchId { return strings.Fields(line)[0], nil // return the commit hash } } } return "", fmt.Errorf("no commit found for subject: %s", subject) }