forgepb/patchset.Make.go

294 lines
7.8 KiB
Go

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 <commit-range> > 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 <original_commit_hash> | git patch-id
// git cat-file -p <commit_hash> | 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
}