From 9e0ac5045c32461b098ad145de49d3dff2c0ddfa Mon Sep 17 00:00:00 2001 From: Derek Cormier Date: Mon, 16 Mar 2026 18:41:34 -0700 Subject: [PATCH] fix: correct patch commands for recursive diffs --- diff/defs.bzl | 10 ++++++++++ diff/private/diff.bzl | 25 ++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/diff/defs.bzl b/diff/defs.bzl index ddaaba0..569c752 100644 --- a/diff/defs.bzl +++ b/diff/defs.bzl @@ -70,10 +70,20 @@ def diff(name, srcs, args = ["--unified"], patch = None, **kwargs): partial.call(srcs[i], name = target, out = target + ".in") srcs[i] = target + # Determine whether patchable inputs are all source directories. + # Bazel rules cannot detect source directories. This information is + # needed to produce the correct patch command. + source_directories = False + if len(srcs) == 2: # TODO: support multiple directory inputs with --to-file + all_sources = set(native.glob([srcs[0]], exclude_directories = 0, allow_empty = True)) + file_sources = set(native.glob([srcs[0]], exclude_directories = 1, allow_empty = True)) + source_directories = len(all_sources.difference(file_sources)) > 0 + diff_rule( name = name, args = args, srcs = srcs, patch = patch or name + ".patch", + source_directories = source_directories, **kwargs ) diff --git a/diff/private/diff.bzl b/diff/private/diff.bzl index 16e324a..e11d253 100644 --- a/diff/private/diff.bzl +++ b/diff/private/diff.bzl @@ -35,11 +35,21 @@ def _determine_patch_type(args): return "normal" -def _patch_cmd(type, source_file, patch_file): +def _is_recursive(args): + for arg in args: + arg = arg.lstrip(" ") + if arg.startswith("-r") or arg.startswith("--recursive"): + return True + return False + +def _patch_cmd(type, source_file, patch_file, recursive, source_directories): if type == "normal": return "(cd \\$(bazel info workspace); patch -p0 {} < {})".format(source_file, patch_file) elif type == "context" or type == "unified": - return "(cd \\$(bazel info workspace); patch -p0 < {})".format(patch_file) + if recursive and source_directories: + return "(cd \\$(bazel info workspace); patch --directory {} -p{} < {})".format(source_file, source_file.count("/") + 1, patch_file) + else: + return "(cd \\$(bazel info workspace); patch -p0 < {})".format(patch_file) return None def _detect_multifile(args): @@ -137,6 +147,7 @@ def _diff_rule_impl(ctx): fail("error: srcs attr of diff rule must contain exactly two targets unless --from-file or --to-file are specified") type = _determine_patch_type(ctx.attr.args) + recursive = _is_recursive(ctx.attr.args) command = _build_command( ctx.bin_dir.path, @@ -182,7 +193,7 @@ def _diff_rule_impl(ctx): if patchable: # Show a command to patch the source file if it's a (bazel) source file. # NB: the error message we print here allows the user to be in any working directory. - patch_cmd = _patch_cmd(type, ctx.files.srcs[0].path, ctx.outputs.patch.path) + patch_cmd = _patch_cmd(type, ctx.files.srcs[0].path, ctx.outputs.patch.path, recursive, ctx.attr.source_directories) if patch_cmd != None: patch_msg = """ To accept the diff, run: @@ -211,6 +222,14 @@ diff_rule = rule( """, default = [], ), + "source_directories": attr.bool( + doc = """\ + Whether all of the patchable source inputs in `srcs` are directories. This is expected + to be passed in by a wrapper macro as File.is_directory does not detect source directory + inputs. + """, + default = False, + ), "srcs": attr.label_list(allow_files = True), "patch": attr.output( doc = """\