| 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 |
| } |