blob: b1982768e9104e2bd8dcc51d26e2ca479644f740 [file]
package kernel
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
)
// PatchAppliesCleanly checks whether or not a patch applies cleanly to the current directory.
//
// This returns an error if the patch file can't be read. Any nonzero exit code of the
// patch command is interpreted as the patch not applying cleanly, but is not considered an
// error.
// Logs the output of the patch command to the provided writer.
func PatchAppliesCleanly(path string, writer io.Writer) (bool, error) {
patchFp, err := os.Open(path)
if err != nil {
return false, fmt.Errorf("could not read patch file %s: %w", path, err)
}
defer patchFp.Close()
cmd := exec.Command("patch", "-p1", "--dry-run", "--force", "--no-backup-if-mismatch")
cmd.Stdin = patchFp
cmd.Stdout = writer
cmd.Stderr = writer
if err := cmd.Run(); err != nil {
return false, nil
}
return true, nil
}
// FilesModifiedByPatch returns the list of files that a patch would modify.
//
// This returns an error if the patch can't be read or does not apply cleanly.
func FilesModifiedByPatch(path string) ([]string, error) {
patchFp, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not read patch file %s: %w", path, err)
}
defer patchFp.Close()
cmd := exec.Command("patch", "-p1", "--dry-run", "--force", "--no-backup-if-mismatch")
cmd.Stdin = patchFp
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("could not get list of files modified by patch %s: %w", path, err)
}
var modified []string
outLines := strings.Split(string(out), "\n")
for _, line := range outLines {
if strings.HasPrefix(line, "checking file ") {
trimmed := strings.TrimPrefix(line, "checking file ")
// Dealing with renames is surprisingly tricky. lsdiff does not output rename info.
// `git apply --numstat --summary` does output rename info, but in a format which
// is tricky to parse. While relying on the precise output format from `patch` is
// not great, it works for now.
//
// The patch output looks like:
//
// checking patch <x> (renamed from <y>)
parts := strings.SplitN(trimmed, " (renamed from ", 2)
if len(parts) > 0 {
// First part after "checking file " should be the path name.
modified = append(modified, parts[0])
}
if len(parts) > 1 {
// Second part after " (renamed from " should be the original path name.
renamedFrom := strings.TrimSuffix(parts[1], ")")
modified = append(modified, renamedFrom)
}
}
}
return modified, nil
}
// ApplyPatch applies a patch in the current directory.
//
// This returns an error if the patch can't be read or does not apply cleanly.
// Logs the output of the patch command to the provided writer.
func ApplyPatch(path string, writer io.Writer) error {
patchFp, err := os.Open(path)
if err != nil {
return fmt.Errorf("could not read patch file %s: %w", path, err)
}
defer patchFp.Close()
cmd := exec.Command("patch", "-p1", "--no-backup-if-mismatch")
cmd.Stdin = patchFp
cmd.Stdout = writer
cmd.Stderr = writer
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to apply patch %s: %w", path, err)
}
return nil
}
// UnapplyPatch unapplies a patch in the current directory by applying it in reverse.
//
// This returns an error if the patch can't be read or does not unapply cleanly.
// Logs the output of the patch command to the provided writer.
func UnapplyPatch(path string, writer io.Writer) error {
patchFp, err := os.Open(path)
if err != nil {
return fmt.Errorf("could not read patch file %s: %w", path, err)
}
defer patchFp.Close()
cmd := exec.Command("patch", "-p1", "--reverse", "--no-backup-if-mismatch")
cmd.Stdin = patchFp
cmd.Stdout = writer
cmd.Stderr = writer
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to unapply patch %s: %w", path, err)
}
return nil
}
// ApplyPatches applies a list of patches in the current directory.
//
// For each patch, this first checks if the patch would apply cleanly. If not,
// then the patch is not applied, and the function exits.
//
// The first return value is the list of patches which applied cleanly, and the
// second return value is the full list of paths modified by the applied patches.
// Both of these values are populated correctly even if this function returns an
// error. This is so that callers can commit or roll back changes to the affected
// files.
//
// Returns an error immediately if any patch cannot be applied.
func ApplyPatches(paths []string, writer io.Writer) ([]string, []string, error) {
var applied, modified []string
for _, path := range paths {
fmt.Fprintf(writer, "Applying patch %s\n", path)
appliesCleanly, err := PatchAppliesCleanly(path, writer)
if err != nil {
return applied, modified, fmt.Errorf("could not apply all patches: %w", err)
}
if !appliesCleanly {
return applied, modified, fmt.Errorf("could not apply all patches; patch %s would not apply cleanly", path)
}
modifiedByPatch, err := FilesModifiedByPatch(path)
if err != nil {
return applied, nil, fmt.Errorf("could not apply all patches: %w", err)
}
if err := ApplyPatch(path, writer); err != nil {
return applied, modified, fmt.Errorf("could not apply all patches: %w", err)
}
modified = append(modified, modifiedByPatch...)
applied = append(applied, path)
}
return paths, modified, nil
}
// UnapplyPatches unapplies a list of patches in the current directory.
//
// Returns an error immediately if any patch fails to be unapplied.
func UnapplyPatches(paths []string, writer io.Writer) error {
for _, path := range paths {
fmt.Fprintf(writer, "Unapplying patch %s\n", path)
if err := UnapplyPatch(path, writer); err != nil {
return fmt.Errorf("could not unapply all patches: %w", err)
}
}
return nil
}