From 76163e20202ab864262c89554f9fbcff91dccf0e Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Mon, 26 Jan 2026 23:56:49 +0000 Subject: [PATCH] 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 --- .../PackageManager/CommandLineArguments.zig | 14 +- .../PackageManager/PackageManagerOptions.zig | 6 +- src/install/PackageManager/patchPackage.zig | 10 ++ test/cli/install/bun-patch.test.ts | 142 ++++++++++++++++++ 4 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/install/PackageManager/CommandLineArguments.zig b/src/install/PackageManager/CommandLineArguments.zig index 6413f46349..7ecd9c30d5 100644 --- a/src/install/PackageManager/CommandLineArguments.zig +++ b/src/install/PackageManager/CommandLineArguments.zig @@ -119,11 +119,13 @@ pub const unlink_params: []const ParamType = &(shared_params ++ [_]ParamType{ const patch_params: []const ParamType = &(shared_params ++ [_]ParamType{ clap.parseParam(" ... \"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 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(" ... \"dir\" containing changes to a package") catch unreachable, + clap.parseParam("--preview Print the diff without saving it") catch unreachable, clap.parseParam("--patches-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 { \\ Generate a patch file for changes made to jquery \\ bun patch --commit 'node_modules/jquery' \\ + \\ Preview a patch without saving it + \\ bun patch --preview 'node_modules/jquery' + \\ \\ Generate a patch file in a custom directory for changes made to jquery \\ bun patch --patches-dir 'my-patches' 'node_modules/jquery' \\ @@ -408,6 +414,9 @@ pub fn printHelp(subcommand: Subcommand) void { \\ Generate a patch in the default "./patches" directory for changes in "./node_modules/jquery" \\ bun patch-commit 'node_modules/jquery' \\ + \\ Preview a patch without saving it + \\ bun patch-commit --preview 'node_modules/jquery' + \\ \\ Generate a patch in a custom directory ("./my-patches") \\ bun patch-commit --patches-dir 'my-patches' 'node_modules/jquery' \\ @@ -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"), }, }; } diff --git a/src/install/PackageManager/PackageManagerOptions.zig b/src/install/PackageManager/PackageManagerOptions.zig index 88fd29712b..48d12afd81 100644 --- a/src/install/PackageManager/PackageManagerOptions.zig +++ b/src/install/PackageManager/PackageManagerOptions.zig @@ -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, }, }; }, diff --git a/src/install/PackageManager/patchPackage.zig b/src/install/PackageManager/patchPackage.zig index b3986e334d..f81dfd8c6d 100644 --- a/src/install/PackageManager/patchPackage.zig +++ b/src/install/PackageManager/patchPackage.zig @@ -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()); diff --git a/test/cli/install/bun-patch.test.ts b/test/cli/install/bun-patch.test.ts index 7fef40559a..26bf3464b6 100644 --- a/test/cli/install/bun-patch.test.ts +++ b/test/cli/install/bun-patch.test.ts @@ -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 +* +* 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 +* +* 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"); + }); + }); });