package forgepb import ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/google/uuid" "go.wit.com/lib/hostname" "go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/httppb" "go.wit.com/log" timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) func (p *Patches) HttpPostVerbose(baseURL string, route string) (*Patches, *httppb.HttpRequest, error) { p.PrintTable() return p.HttpPost(baseURL, route) } func (p *Set) HttpPostVerbose(baseURL string, route string) (*Set, *httppb.HttpRequest, error) { p.PrintTable() return p.HttpPost(baseURL, route) } func (p *Sets) HttpPostVerbose(baseURL string, route string) (*Sets, *httppb.HttpRequest, error) { p.PrintTable() return p.HttpPost(baseURL, route) } func newPatchset(name string) *Set { pset := new(Set) pset.Name = name pset.Ctime = timestamppb.New(time.Now()) pset.Uuid = uuid.New().String() pset.Hostname, _ = hostname.Get() pset.Patches = NewPatches() return pset } // creates a patchset // works from the user branches against the devel branches func (f *Forge) MakeDevelPatchSet(name string) (*Set, error) { pset := newPatchset(name) if os.Getenv("GIT_AUTHOR_NAME") == "" { return nil, fmt.Errorf("GIT_AUTHOR_NAME not set") } else { pset.GitAuthorName = os.Getenv("GIT_AUTHOR_NAME") } if os.Getenv("GIT_AUTHOR_EMAIL") == "" { return nil, fmt.Errorf("GIT_AUTHOR_EMAIL not set") } else { pset.GitAuthorEmail = os.Getenv("GIT_AUTHOR_EMAIL") } dir, err := os.MkdirTemp("", "forge") if err != nil { return nil, err } // defer os.RemoveAll(dir) // clean up pset.TmpDir = dir all := f.Repos.SortByFullPath() for all.Scan() { repo := all.Next() if !repo.IsLocalBranch(repo.GetUserBranchName()) { // log.Info("repo doesn't have user branch", repo.GetGoPath()) continue } if !repo.IsLocalBranch(repo.GetDevelBranchName()) { // log.Info("repo doesn't have devel branch", repo.GetGoPath()) continue } if repo.ActualGetDevelHash() == repo.ActualGetUserHash() { continue } // make a patchset from user to devel // TODO: verify branches are otherwise exact pset.StartBranchName = repo.GetDevelBranchName() pset.EndBranchName = repo.GetUserBranchName() err := pset.makePatchSetNew(repo) if err != nil { return nil, err } } return pset, nil } func (pset *Set) makePatchSetNew(repo *gitpb.Repo) error { startBranch := pset.StartBranchName endBranch := pset.EndBranchName repoDir := filepath.Join(pset.TmpDir, repo.GetGoPath()) err := os.MkdirAll(repoDir, 0755) if err != nil { return err } // maybe better? maybe worse? // git format-patch -o patches --stdout > my-patch.mbox // git format-patch --stdout -5 > my-patch.mbox # last 5 patches // git am < my-patch.mbox // git format-patch branch1..branch2 // export GIT_COMMITTER_DATE="2024-01-01T12:00:00" // export GIT_AUTHOR_DATE="2024-01-01T12:00:00" // export GIT_COMMITTER_NAME="Your Name" // export GIT_COMMITTER_EMAIL="your.email@example.com" // export GIT_AUTHOR_NAME="Your Name" // export GIT_AUTHOR_EMAIL="your.email@example.com" // git am < patch.mbox cmd := []string{"git", "format-patch", "-o", repoDir, startBranch + ".." + endBranch} r := repo.Run(cmd) if r.Error != nil { log.Info("git format-patch", repo.FullPath) log.Info("git format-patch", cmd) log.Info("git format-patch error", r.Error) return r.Error } if r.Exit != 0 { log.Info("git format-patch", repo.FullPath) log.Info("git format-patch", cmd) log.Info("git format-patch exit", r.Exit) return errors.New(fmt.Sprintf("git returned %d", r.Exit)) } if len(r.Stdout) == 0 { log.Infof("No patches in %s (%s,%s)\n", repo.FullPath, repo.ActualGetDevelHash(), repo.ActualGetUserHash()) // git created no files to add return nil } err = pset.addPatchFiles(repo, repoDir) log.Infof("Added %d patches for %s len=%d\n", len(r.Stdout), repo.FullPath, pset.Patches.Len()) // pset.PrintTable() return err } // git show | git patch-id // git cat-file -p | grep tree // process each file in pDir/ func (p *Set) addPatchFiles(repo *gitpb.Repo, fullDir string) error { var baderr error // log.Info("ADD PATCH FILES ADDED DIR", fullDir) filepath.Walk(fullDir, func(path string, info os.FileInfo, err error) error { if err != nil { // Handle possible errors, like permission issues fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err) baderr = err return err } if info.IsDir() { return nil } data, err := os.ReadFile(path) if err != nil { log.Info("addPatchFile() failed", path) baderr = err return err } patch := new(Patch) patch.Filename, _ = filepath.Rel(p.TmpDir, path) patch.Data = data if err := patch.parseData(); err != nil { log.Info("parseData() failed", err) return err } if err := findPatchId(repo, patch); err != nil { log.Info("findPatchId() failed", err) return err } patch.StartHash = repo.ActualDevelHash() patch.NewHash = "na" patch.Namespace = repo.GetGoPath() if p.Patches == nil { log.Info("SHOULD NOT HAVE HAPPENED. p.Patches == nil") p.Patches = new(Patches) } p.Patches.Append(patch) // log.Info("ADDED PATCH FILE", path) return nil }) return baderr } // looks at the git format-patch output // saves the commit Hash // saves the diff lines func (p *Patch) parseData() error { lines := strings.Split(string(p.Data), "\n") for _, line := range lines { fields := strings.Fields(line) if len(fields) < 2 { continue } switch fields[0] { case "From": p.CommitHash = fields[1] case "Subject:": p.Comment = line case "diff": p.Files = append(p.Files, line) } } return nil } // just an example of how to walk only directories func onlyWalkDirs(pDir string) error { log.Info("DIR", pDir) // var all []string var baderr error filepath.WalkDir(pDir, func(path string, d os.DirEntry, err error) error { if err != nil { // Handle possible errors, like permission issues fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err) baderr = err return err } log.Info("TESTING DIR", path) if d.IsDir() { return filepath.SkipDir } log.Info("NEVER GETS HERE? WHAT IS THIS?", path) return nil }) return baderr } // func runPipe() error { func findPatchId(repo *gitpb.Repo, p *Patch) error { if p.CommitHash == "" { return log.Errorf("%s commit hash not found", p.Filename) } // 1. Create the command to get the diff for the commit. // "git show" is the perfect tool for this. cmdShow := exec.Command("git", "show", p.CommitHash) cmdShow.Dir = repo.GetFullPath() // 2. Create the command to calculate the patch-id from stdin. cmdPipeID := exec.Command("git", "patch-id", "--stable") cmdPipeID.Dir = repo.GetFullPath() // 3. Connect the output of "git show" to the input of "git patch-id". // This is the Go equivalent of the shell pipe `|`. pipe, err := cmdShow.StdoutPipe() if err != nil { return fmt.Errorf("failed to create pipe: %w", err) } cmdPipeID.Stdin = pipe // 4. We need a buffer to capture the final output from git patch-id. var output bytes.Buffer cmdPipeID.Stdout = &output // 5. Start the reading command (patch-id) first. if err := cmdPipeID.Start(); err != nil { return fmt.Errorf("failed to start git-patch-id: %w", err) } // 6. Run the writing command (show). This will block until it's done. if err := cmdShow.Run(); err != nil { return fmt.Errorf("failed to run git-show: %w", err) } // 7. Wait for the reading command to finish. if err := cmdPipeID.Wait(); err != nil { return fmt.Errorf("failed to wait for git-patch-id: %w", err) } fields := strings.Fields(output.String()) if len(fields) != 2 { return fmt.Errorf("git-patch-id produced empty output") } if fields[1] != p.CommitHash { return fmt.Errorf("patchid did not match %s != %v", p.CommitHash, fields) } p.PatchId = fields[1] return nil }