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