feat(patch): add --preview flag to show diff without saving

Adds a `--preview` flag to `bun patch` and `bun patch-commit` commands that
prints the patch diff to stdout without saving the patch file or modifying
package.json.

This allows users to preview what a patch will contain before committing to
it, making it easier to create minimal patches when porting bug fixes from
upstream repositories.

Usage:
  bun patch --preview node_modules/package
  bun patch-commit --preview node_modules/package

Closes #26463

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-26 23:56:49 +00:00
parent bfe40e8760
commit 76163e2020
4 changed files with 169 additions and 3 deletions

View File

@@ -119,11 +119,13 @@ pub const unlink_params: []const ParamType = &(shared_params ++ [_]ParamType{
const patch_params: []const ParamType = &(shared_params ++ [_]ParamType{
clap.parseParam("<POS> ... \"name\" of the package to patch") catch unreachable,
clap.parseParam("--commit Install a package containing modifications in `dir`") catch unreachable,
clap.parseParam("--preview Print the diff without saving it (implies --commit)") catch unreachable,
clap.parseParam("--patches-dir <dir> The directory to put the patch file in (only if --commit is used)") catch unreachable,
});
const patch_commit_params: []const ParamType = &(shared_params ++ [_]ParamType{
clap.parseParam("<POS> ... \"dir\" containing changes to a package") catch unreachable,
clap.parseParam("--preview Print the diff without saving it") catch unreachable,
clap.parseParam("--patches-dir <dir> The directory to put the patch file") catch unreachable,
});
@@ -281,6 +283,7 @@ const PatchOpts = union(enum) {
patch: struct {},
commit: struct {
patches_dir: []const u8 = "patches",
preview: bool = false,
},
};
@@ -380,6 +383,9 @@ pub fn printHelp(subcommand: Subcommand) void {
\\ <d>Generate a patch file for changes made to jquery<r>
\\ <b><green>bun patch --commit 'node_modules/jquery'<r>
\\
\\ <d>Preview a patch without saving it<r>
\\ <b><green>bun patch --preview 'node_modules/jquery'<r>
\\
\\ <d>Generate a patch file in a custom directory for changes made to jquery<r>
\\ <b><green>bun patch --patches-dir 'my-patches' 'node_modules/jquery'<r>
\\
@@ -408,6 +414,9 @@ pub fn printHelp(subcommand: Subcommand) void {
\\ <d>Generate a patch in the default "./patches" directory for changes in "./node_modules/jquery"<r>
\\ <b><green>bun patch-commit 'node_modules/jquery'<r>
\\
\\ <d>Preview a patch without saving it<r>
\\ <b><green>bun patch-commit --preview 'node_modules/jquery'<r>
\\
\\ <d>Generate a patch in a custom directory ("./my-patches")<r>
\\ <b><green>bun patch-commit --patches-dir 'my-patches' 'node_modules/jquery'<r>
\\
@@ -934,10 +943,12 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
if (subcommand == .patch) {
const patch_commit = args.flag("--commit");
if (patch_commit) {
const patch_preview = args.flag("--preview");
if (patch_commit or patch_preview) {
cli.patch = .{
.commit = .{
.patches_dir = args.option("--patches-dir") orelse "patches",
.preview = patch_preview,
},
};
} else {
@@ -950,6 +961,7 @@ pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !Com
cli.patch = .{
.commit = .{
.patches_dir = args.option("--patches-dir") orelse "patches",
.preview = args.flag("--preview"),
},
};
}

View File

@@ -33,6 +33,7 @@ patch_features: union(enum) {
patch: struct {},
commit: struct {
patches_dir: string,
preview: bool = false,
},
} = .{ .nothing = .{} },
@@ -648,10 +649,11 @@ pub fn load(
.patch => {
this.patch_features = .{ .patch = .{} };
},
.commit => {
.commit => |commit_opts| {
this.patch_features = .{
.commit = .{
.patches_dir = cli.patch.commit.patches_dir,
.patches_dir = commit_opts.patches_dir,
.preview = commit_opts.preview,
},
};
},

View File

@@ -391,6 +391,16 @@ pub fn doPatchCommit(
};
defer patchfile_contents.deinit();
// In preview mode, print the diff to stdout and return without modifying any files
if (manager.options.patch_features.commit.preview) {
Output.writer().writeAll(patchfile_contents.items) catch |e| {
Output.err(e, "failed to write patch to stdout", .{});
Global.crash();
};
Output.flush();
return null;
}
// write the patch contents to temp file then rename
var tmpname_buf: [1024]u8 = undefined;
const tempfile_name = try bun.fs.FileSystem.tmpname("tmp", &tmpname_buf, bun.fastRandom());

View File

@@ -816,4 +816,146 @@ module.exports = function isOdd() {
expect(stdout.toString()).toBe("true\n");
}
});
describe("--preview flag", async () => {
test("should print diff without saving patch file", async () => {
const tempdir = tempDirWithFiles("preview", {
"package.json": JSON.stringify({
name: "bun-patch-preview-test",
module: "index.ts",
type: "module",
dependencies: {
"is-even": "1.0.0",
},
}),
"index.ts": /* ts */ `import isEven from 'is-even'; console.log(isEven(420))`,
});
// Install dependencies
expectNoError(await $`${bunExe()} i`.env(bunEnv).cwd(tempdir));
// Prepare the package for patching
expectNoError(await $`${bunExe()} patch is-even@1.0.0`.env(bunEnv).cwd(tempdir));
// Make a change to the package
const patchedCode = /* ts */ `/*!
* is-even <https://github.com/jonschlinkert/is-even>
*
* Copyright (c) 2015, 2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
var isOdd = require('is-odd');
module.exports = function isEven(i) {
console.log("Preview test!")
return !isOdd(i);
};
`;
await $`echo ${patchedCode} > node_modules/is-even/index.js`.env(bunEnv).cwd(tempdir);
// Run bun patch --preview
const { stdout, stderr } = await $`${bunExe()} patch --preview node_modules/is-even`.env(bunEnv).cwd(tempdir);
expect(stderr.toString()).not.toContain("error");
// The stdout should contain the diff
expect(stdout.toString()).toContain("diff --git");
expect(stdout.toString()).toContain("Preview test!");
expect(stdout.toString()).toContain("index.js");
// The patches directory should NOT exist (no file saved)
const { exitCode } = await $`test -d patches`.env(bunEnv).cwd(tempdir).throws(false);
expect(exitCode).not.toBe(0);
// package.json should NOT have patchedDependencies
const packageJson = await $`cat package.json`.cwd(tempdir).env(bunEnv).json();
expect(packageJson.patchedDependencies).toBeUndefined();
});
test("should work with bun patch-commit --preview", async () => {
const tempdir = tempDirWithFiles("preview2", {
"package.json": JSON.stringify({
name: "bun-patch-preview-test-2",
module: "index.ts",
type: "module",
dependencies: {
"is-even": "1.0.0",
},
}),
"index.ts": /* ts */ `import isEven from 'is-even'; console.log(isEven(420))`,
});
// Install dependencies
expectNoError(await $`${bunExe()} i`.env(bunEnv).cwd(tempdir));
// Prepare the package for patching
expectNoError(await $`${bunExe()} patch is-even@1.0.0`.env(bunEnv).cwd(tempdir));
// Make a change to the package
const patchedCode = /* ts */ `/*!
* is-even <https://github.com/jonschlinkert/is-even>
*
* Copyright (c) 2015, 2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
var isOdd = require('is-odd');
module.exports = function isEven(i) {
console.log("patch-commit preview!")
return !isOdd(i);
};
`;
await $`echo ${patchedCode} > node_modules/is-even/index.js`.env(bunEnv).cwd(tempdir);
// Run bun patch-commit --preview
const { stdout, stderr } = await $`${bunExe()} patch-commit --preview node_modules/is-even`
.env(bunEnv)
.cwd(tempdir);
expect(stderr.toString()).not.toContain("error");
// The stdout should contain the diff
expect(stdout.toString()).toContain("diff --git");
expect(stdout.toString()).toContain("patch-commit preview!");
// The patches directory should NOT exist (no file saved)
const { exitCode } = await $`test -d patches`.env(bunEnv).cwd(tempdir).throws(false);
expect(exitCode).not.toBe(0);
// package.json should NOT have patchedDependencies
const packageJson = await $`cat package.json`.cwd(tempdir).env(bunEnv).json();
expect(packageJson.patchedDependencies).toBeUndefined();
});
test("--preview should show no changes when package is unmodified", async () => {
const tempdir = tempDirWithFiles("preview3", {
"package.json": JSON.stringify({
name: "bun-patch-preview-test-3",
module: "index.ts",
type: "module",
dependencies: {
"is-even": "1.0.0",
},
}),
"index.ts": /* ts */ `import isEven from 'is-even'; console.log(isEven(420))`,
});
// Install dependencies
expectNoError(await $`${bunExe()} i`.env(bunEnv).cwd(tempdir));
// Prepare the package for patching (but don't make any changes)
expectNoError(await $`${bunExe()} patch is-even@1.0.0`.env(bunEnv).cwd(tempdir));
// Run bun patch --preview without making changes
const { stdout, stderr } = await $`${bunExe()} patch --preview node_modules/is-even`.env(bunEnv).cwd(tempdir);
expect(stderr.toString()).not.toContain("error");
// Should indicate no changes
expect(stdout.toString()).toContain("No changes detected");
});
});
});