From patchwork Tue Feb 25 23:39:23 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Justin Tobler X-Patchwork-Id: 13991148 Received: from mail-ot1-f43.google.com (mail-ot1-f43.google.com [209.85.210.43]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 47F9B235BF4 for ; Tue, 25 Feb 2025 23:42:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.43 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740526974; cv=none; b=l7DCJOMUjoqYNAtBhu6ECfnFlgvsG3XgCiJUkkpTEDWQzILPAzmEpIVk4PqqrPq4SKkLZZMBMpVqbkzf/GQPMdYwV0w0B9lLtHFkZPtDLnQetUV1rXtS1dKXlJebwxqjs8oYjn/QnysepzVVgZYSzBQeKpN71hm0aC5yZcuvOU0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740526974; c=relaxed/simple; bh=s5pmylBMPrgv8R6XXgZI0FuwefJC9j9tKO48NCXyWaE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=IcifTKvGRxsZss4NVWrMO8BDUtlkkaXaa98OG2OT9jd2AKsKFr1yvP5IYv4uFQeJDl81GU24FP97CyVWNmh8Lc9bR0EiceqTAeRDna/4dNrL+uruNK+2VcDpnFHarGlImfOQK12IHbuPccWHiKK/LKi9U0l7YI86VcxHcSTgGDQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=OI33AyDL; arc=none smtp.client-ip=209.85.210.43 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="OI33AyDL" Received: by mail-ot1-f43.google.com with SMTP id 46e09a7af769-7272f3477e9so1901772a34.3 for ; Tue, 25 Feb 2025 15:42:52 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1740526972; x=1741131772; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=D3RWgs1kZQK6RfOE4QBpVa0M0RoOUlUV1dcBc6a6OJk=; b=OI33AyDL6vbpZCEUSSOzOAuYcggLhFo4BmjV6NoyCk+fnGd5gIdwd0H0aNGXI6QuwG 07VvKJ2kXb3OUGIvncVinzxJlJykmQVbsuEjKwt9stUz0muN4NOv7fioIpUgam/zDvti bRlrz2rdaQUGfysL0KrusMnwBSzscO/cveVBC3Meeb+OKDC0PRLbkwczBIhM9qLlRtgj euR6Fp41gKkPmQNoLqPqW0+qOkp+kxr/fR5qi7E/vKVwN3DtJNt3W6Rx4ZCHFNTYTmD6 QcVnNp9ZBT6+qg4ph8BaRjPtdLsz8EI65vpZYBryqiA5ijo1MTUUsfcmDSf3Ge6gV6VG YHaQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1740526972; x=1741131772; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=D3RWgs1kZQK6RfOE4QBpVa0M0RoOUlUV1dcBc6a6OJk=; b=PVpt8HBbIVWxYhH9yZt9CUvbHsx0ivYLqUJ4XqB1aiuH5AmI4EC3i9XE/WNpM//BIa 80zjHe1AF+TOTnwXhCoXvugqGAUTdMk1QkFkC1ERc/YSE2fcGSM2axY1TXRW67fRPK0w QrRpsOvLB7GZeatoVpMvrkkqIltwh23j4R8NKDWadAAQh4KKZLZu5lOeiwqT3BV3iVvE QalgttMwhfDupgsmOcd3AUCR9jdo6ZM9OIbOUEvK95B/gT1RTl77JPqNRQDgU7wSlzit /Sg0/zLoEiG5vH4pOXKBBGfZjut5zt/Ll9f1m1XA0Yd40ThEzpgnt0aJIxLqZ/SnnOSW 0PDA== X-Gm-Message-State: AOJu0YwcRLS0IO49EG1sr/5/eQ8Y+0QDFv585zWRVEArF4OYeTQyr5/t c+LL+DYAIUf16iBYEadaUupB71zDQ/S3h3kqHOLKxnNhT56yERtaGVRq3V0d X-Gm-Gg: ASbGncs2zXUtzPMN+ZaeXRWU4yVj/7+doGawj8l9BByUv5TKHhJfDhLu2/HbZtZAZT8 /GuuwOrVxrK7oqDNJF/SdEtJMz7gO5FZggMNCH37DFyoXMOKje4KgtJC5BL15Xf7G++8P/Mo5D3 nqyROANlqnGy0KJ/MkGPcsgHdsZyidFOClNOiOcPBIgis+L2K6eYJP9coIPG1NJQsg76h7XV9vx Cxgupp7a3ABpw8QZV94X1bxA+YL+PKbrCKZ0fVojRgDu0H6TvcWlfLhejeBiJRiiGbJa/EWKtuK OXycv25FgLTp1OkRpJeIKR6I3hV9JvkqaQ== X-Google-Smtp-Source: AGHT+IETRXLhbjtMe47+9U7OaerpvkJT2t+M3A0j+rdvZc8upsW9Qb2Uj5v2wPOMnUYTdcBf2OOheA== X-Received: by 2002:a05:6830:6e0f:b0:727:2751:6b9d with SMTP id 46e09a7af769-7289d0fad72mr3738921a34.9.1740526971668; Tue, 25 Feb 2025 15:42:51 -0800 (PST) Received: from denethor.localdomain ([136.50.74.45]) by smtp.gmail.com with ESMTPSA id 586e51a60fabf-2c1113f5bc5sm609308fac.21.2025.02.25.15.42.50 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 25 Feb 2025 15:42:50 -0800 (PST) From: Justin Tobler To: git@vger.kernel.org Cc: ps@pks.im, karthik.188@gmail.com, phillip.wood123@gmail.com, Justin Tobler Subject: [PATCH v3 1/3] diff: return diff_filepair from diff queue helpers Date: Tue, 25 Feb 2025 17:39:23 -0600 Message-ID: <20250225233925.1345086-2-jltobler@gmail.com> X-Mailer: git-send-email 2.48.1 In-Reply-To: <20250225233925.1345086-1-jltobler@gmail.com> References: <20250212041825.2455031-1-jltobler@gmail.com> <20250225233925.1345086-1-jltobler@gmail.com> Precedence: bulk X-Mailing-List: git@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 The `diff_addremove()` and `diff_change()` functions set up and queue diffs, but do not return the `diff_filepair` added to the queue. In a subsequent commit, modifications to `diff_filepair` need to occur in certain cases after being queued. Since the existing `diff_addremove()` and `diff_change()` are also used for callbacks in `diff_options` as types `add_remove_fn_t` and `change_fn_t`, modifying the existing function signatures requires further changes. The diff options for pruning use `file_add_remove()` and `file_change()` where file pairs do not even get queued. Thus, separate functions are implemented instead. Split out the queuing operations into `diff_queue_addremove()` and `diff_queue_change()` which also return a handle to the queued `diff_filepair`. Signed-off-by: Justin Tobler --- diff.c | 70 +++++++++++++++++++++++++++++++++++++++++----------------- diff.h | 25 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/diff.c b/diff.c index 019fb893a7..b5a779f997 100644 --- a/diff.c +++ b/diff.c @@ -7157,16 +7157,19 @@ void compute_diffstat(struct diff_options *options, options->found_changes = !!diffstat->nr; } -void diff_addremove(struct diff_options *options, - int addremove, unsigned mode, - const struct object_id *oid, - int oid_valid, - const char *concatpath, unsigned dirty_submodule) +struct diff_filepair *diff_queue_addremove(struct diff_queue_struct *queue, + struct diff_options *options, + int addremove, unsigned mode, + const struct object_id *oid, + int oid_valid, + const char *concatpath, + unsigned dirty_submodule) { struct diff_filespec *one, *two; + struct diff_filepair *pair; if (S_ISGITLINK(mode) && is_submodule_ignored(concatpath, options)) - return; + return NULL; /* This may look odd, but it is a preparation for * feeding "there are unchanged files which should @@ -7186,7 +7189,7 @@ void diff_addremove(struct diff_options *options, if (options->prefix && strncmp(concatpath, options->prefix, options->prefix_length)) - return; + return NULL; one = alloc_filespec(concatpath); two = alloc_filespec(concatpath); @@ -7198,25 +7201,29 @@ void diff_addremove(struct diff_options *options, two->dirty_submodule = dirty_submodule; } - diff_queue(&diff_queued_diff, one, two); + pair = diff_queue(queue, one, two); if (!options->flags.diff_from_contents) options->flags.has_changes = 1; + + return pair; } -void diff_change(struct diff_options *options, - unsigned old_mode, unsigned new_mode, - const struct object_id *old_oid, - const struct object_id *new_oid, - int old_oid_valid, int new_oid_valid, - const char *concatpath, - unsigned old_dirty_submodule, unsigned new_dirty_submodule) +struct diff_filepair *diff_queue_change(struct diff_queue_struct *queue, + struct diff_options *options, + unsigned old_mode, unsigned new_mode, + const struct object_id *old_oid, + const struct object_id *new_oid, + int old_oid_valid, int new_oid_valid, + const char *concatpath, + unsigned old_dirty_submodule, + unsigned new_dirty_submodule) { struct diff_filespec *one, *two; struct diff_filepair *p; if (S_ISGITLINK(old_mode) && S_ISGITLINK(new_mode) && is_submodule_ignored(concatpath, options)) - return; + return NULL; if (options->flags.reverse_diff) { SWAP(old_mode, new_mode); @@ -7227,7 +7234,7 @@ void diff_change(struct diff_options *options, if (options->prefix && strncmp(concatpath, options->prefix, options->prefix_length)) - return; + return NULL; one = alloc_filespec(concatpath); two = alloc_filespec(concatpath); @@ -7235,19 +7242,42 @@ void diff_change(struct diff_options *options, fill_filespec(two, new_oid, new_oid_valid, new_mode); one->dirty_submodule = old_dirty_submodule; two->dirty_submodule = new_dirty_submodule; - p = diff_queue(&diff_queued_diff, one, two); + p = diff_queue(queue, one, two); if (options->flags.diff_from_contents) - return; + return p; if (options->flags.quick && options->skip_stat_unmatch && !diff_filespec_check_stat_unmatch(options->repo, p)) { diff_free_filespec_data(p->one); diff_free_filespec_data(p->two); - return; + return p; } options->flags.has_changes = 1; + + return p; +} + +void diff_addremove(struct diff_options *options, int addremove, unsigned mode, + const struct object_id *oid, int oid_valid, + const char *concatpath, unsigned dirty_submodule) +{ + diff_queue_addremove(&diff_queued_diff, options, addremove, mode, oid, + oid_valid, concatpath, dirty_submodule); +} + +void diff_change(struct diff_options *options, + unsigned old_mode, unsigned new_mode, + const struct object_id *old_oid, + const struct object_id *new_oid, + int old_oid_valid, int new_oid_valid, + const char *concatpath, + unsigned old_dirty_submodule, unsigned new_dirty_submodule) +{ + diff_queue_change(&diff_queued_diff, options, old_mode, new_mode, + old_oid, new_oid, old_oid_valid, new_oid_valid, + concatpath, old_dirty_submodule, new_dirty_submodule); } struct diff_filepair *diff_unmerge(struct diff_options *options, const char *path) diff --git a/diff.h b/diff.h index 0a566f5531..63afa17e84 100644 --- a/diff.h +++ b/diff.h @@ -508,6 +508,31 @@ void diff_set_default_prefix(struct diff_options *options); int diff_can_quit_early(struct diff_options *); +/* + * Stages changes in the provided diff queue for file additions and deletions. + * If a file pair gets queued, it is returned. + */ +struct diff_filepair *diff_queue_addremove(struct diff_queue_struct *queue, + struct diff_options *, + int addremove, unsigned mode, + const struct object_id *oid, + int oid_valid, const char *fullpath, + unsigned dirty_submodule); + +/* + * Stages changes in the provided diff queue for file modifications. + * If a file pair gets queued, it is returned. + */ +struct diff_filepair *diff_queue_change(struct diff_queue_struct *queue, + struct diff_options *, + unsigned mode1, unsigned mode2, + const struct object_id *old_oid, + const struct object_id *new_oid, + int old_oid_valid, int new_oid_valid, + const char *fullpath, + unsigned dirty_submodule1, + unsigned dirty_submodule2); + void diff_addremove(struct diff_options *, int addremove, unsigned mode, From patchwork Tue Feb 25 23:39:24 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Justin Tobler X-Patchwork-Id: 13991149 Received: from mail-oa1-f53.google.com (mail-oa1-f53.google.com [209.85.160.53]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 80E0923A981 for ; Tue, 25 Feb 2025 23:42:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.160.53 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740526976; cv=none; b=BGQ1nEh9HfOg3zUxfcFJA6dytQjNJ9A8cRttDcyJWKHymA9m1abOvHxQsOre9eftmTzPSBlyPzcH+OPuaz1ZF9SZMH9rNmTIC4N0wrN9qrS+EHSPLnQrnfw9slAaEeQL6loovSmYhymMd+AOSS1hrrUu7+TsocBVxH1q07E8d8A= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740526976; c=relaxed/simple; bh=7B9i44bSistVhySabwy6KWfftJcd7OXiezgJZ4TFRws=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Q2ye0Oywonv8YGVM0gahGXNoFjo93fwOvNT4Z8WEsJEAEJHmfJlQqDRm8NL1rxRVNyNSKbI6j7ev2k3FI01k50p7oDptY4dnvmQuJ4UD/lVstev8TbTPJxXvnnObc+enNnzVihZGwGJSJK7/YKOsgDtvwYpOwHFPvRyDYIVldAE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=HwzlLEf2; arc=none smtp.client-ip=209.85.160.53 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="HwzlLEf2" Received: by mail-oa1-f53.google.com with SMTP id 586e51a60fabf-2c11ddc865eso270136fac.3 for ; Tue, 25 Feb 2025 15:42:54 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1740526973; x=1741131773; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=vGn6nf6jT0na+wUhQtCKWimm97GE+9muoJZ6n+4FfKQ=; b=HwzlLEf2v9m9JiXAQOWf3aOmG4ajE6HfmtEeQyipNFNgMtJQ4GTeJLr97LJIjv/Gbp ILL5YBYLFeIVNcMilPaB1uXGreDn1vWJf/ud/qJvpFB7+v1bUOpVJ5Ce4hcHvn3DBnSM NaxjbdS1wXYFDTICM6D8aweJysoTZYXng73SZJlQz8bkYA9j+c2n8qotoNWMEpxybePa wfdPWDs+6X/97fvyhsjQyCTjEGCGovnsIGuKtw4xz6sFWZ4niaA1AuJmuvPzZ9SDnDRT LNSqun1eBrWxQlfd+0bDZ89Ur0qJ30cf8w02FhSuj/vd5Hl82bdzcAFS6fQnWd1GRxdI 4ZFA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1740526973; x=1741131773; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=vGn6nf6jT0na+wUhQtCKWimm97GE+9muoJZ6n+4FfKQ=; b=kmhSZEJLnE/mCNXUt09TeOAtqIF1D+NbOWZ2jl/YtDZK4mo14EC++eKHYToyUcXTL+ QQOCQEe1RiM/A8b3Crlws5jJmeYdp5HxfDErprN79xG6xRZ5nR5UAW75dVdOwi8x0Zhq h/kUIbf4vk87m1CKEypUxfpBBufkFmd5W4z44H5QD1ABNe6MotbQ1LwAnx5lDJlByeut ccpUnsLyq8P31D88SEo2LA0+T1n7qjc6oOj1kyduQMfmWxDchg6V3B2m1K5OL/DzK4LJ YZ6gmvZzpgiZwuw2mFLDA/ZaIgY53vIzj2Ww7tcSZWmQ+dRnEbMT1KkgZ1CFg2e+Lk9u APzg== X-Gm-Message-State: AOJu0YxVxkGy9168eicGI/4T/Iq9aQfm5m2EidwneUImrrjf6DCqY9PR DX9xCUpoG+JB0573zITyCUce6ZkfawuMLfslC02Mv8tpRPXhSqriSwGgAQoG X-Gm-Gg: ASbGncvihVEDYTZG4Zsw6w/aMJAUpUT0Ajx5+GAdwTQl+pxBzDzIHpRB/U9XV9594Kx kNMDniJrys0xeOF1qpxscqEjD9tmut/gcwfTih6U4W/tvihmbreYudMHA3w3Uuhq1DB1Mx27UB7 bcdtZ2B1FNPBZFoDhi8gTSzvF9hwFSB3GjIRWNUxaP3mKBLZX/gBoRbmQJnhozeeyqIeR2mKuLY xzFL6Gl2a/++asrAr2y4ly/8y2gSTlyne/9rEHyM32o9BP0tWlpAd3krSTwu7yDtQFAcsTOOBI6 uxAp4O+6QgrrpaZgiqsbghcTlJu2brfC0w== X-Google-Smtp-Source: AGHT+IHDRuqZXYQq+hA7VlUUOKlXGQUG+QomGYTrwVGWoPzlrHgcrnv3zw2JZfSFqWupPPuhiC2LWQ== X-Received: by 2002:a05:6871:53ca:b0:29d:c624:7cad with SMTP id 586e51a60fabf-2c10f1c7570mr3362543fac.3.1740526972883; Tue, 25 Feb 2025 15:42:52 -0800 (PST) Received: from denethor.localdomain ([136.50.74.45]) by smtp.gmail.com with ESMTPSA id 586e51a60fabf-2c1113f5bc5sm609308fac.21.2025.02.25.15.42.51 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 25 Feb 2025 15:42:52 -0800 (PST) From: Justin Tobler To: git@vger.kernel.org Cc: ps@pks.im, karthik.188@gmail.com, phillip.wood123@gmail.com, Justin Tobler , Jeff King Subject: [PATCH v3 2/3] builtin: introduce diff-pairs command Date: Tue, 25 Feb 2025 17:39:24 -0600 Message-ID: <20250225233925.1345086-3-jltobler@gmail.com> X-Mailer: git-send-email 2.48.1 In-Reply-To: <20250225233925.1345086-1-jltobler@gmail.com> References: <20250212041825.2455031-1-jltobler@gmail.com> <20250225233925.1345086-1-jltobler@gmail.com> Precedence: bulk X-Mailing-List: git@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Through git-diff(1), a single diff can be generated from a pair of blob revisions directly. Unfortunately, there is not a mechanism to compute batches of specific file pair diffs in a single process. Such a feature is particularly useful on the server-side where diffing between a large set of changes is not feasible all at once due to timeout concerns. To facilitate this, introduce git-diff-pairs(1) which acts as a backend passing its NUL-terminated raw diff format input from stdin through diff machinery to produce various forms of output such as patch or raw. The raw format was originally designed as an interchange format and represents the contents of the diff_queue_diff list making it possible to break the diff pipeline into separate stages. For example, git-diff-tree(1) can be used as a frontend to compute file pairs to queue and feed its raw output to git-diff-pairs(1) to compute patches. With this, batches of diffs can be progessively generated without having to recompute rename detection or retrieve object context. Something like the following: git diff-tree -r -z -M $old $new | git diff-pairs -p -z should generate the same output as `git diff-tree -p -M`. Furthermore, each line of raw diff formatted input can also be individually fed to a separate git-diff-pairs(1) process and still produce the same output. Based-on-patch-by: Jeff King Signed-off-by: Justin Tobler --- .gitignore | 1 + Documentation/git-diff-pairs.adoc | 56 +++++++++ Documentation/meson.build | 1 + Makefile | 1 + builtin.h | 1 + builtin/diff-pairs.c | 193 ++++++++++++++++++++++++++++++ command-list.txt | 1 + git.c | 1 + meson.build | 1 + t/meson.build | 1 + t/t4070-diff-pairs.sh | 74 ++++++++++++ 11 files changed, 331 insertions(+) create mode 100644 Documentation/git-diff-pairs.adoc create mode 100644 builtin/diff-pairs.c create mode 100755 t/t4070-diff-pairs.sh diff --git a/.gitignore b/.gitignore index 08a66ca508..04c444404e 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ /git-diff /git-diff-files /git-diff-index +/git-diff-pairs /git-diff-tree /git-difftool /git-difftool--helper diff --git a/Documentation/git-diff-pairs.adoc b/Documentation/git-diff-pairs.adoc new file mode 100644 index 0000000000..e31f2e2fbb --- /dev/null +++ b/Documentation/git-diff-pairs.adoc @@ -0,0 +1,56 @@ +git-diff-pairs(1) +================= + +NAME +---- +git-diff-pairs - Compare the content and mode of provided blob pairs + +SYNOPSIS +-------- +[synopsis] +git diff-pairs -z [] + +DESCRIPTION +----------- +Show changes for file pairs provided on stdin. Input for this command must be +in the NUL-terminated raw output format as generated by commands such as `git +diff-tree -z -r --raw`. By default, the outputted diffs are computed and shown +in the patch format when stdin closes. + +Usage of this command enables the traditional diff pipeline to be broken up +into separate stages where `diff-pairs` acts as the output phase. Other +commands, such as `diff-tree`, may serve as a frontend to compute the raw +diff format used as input. + +Instead of computing diffs via `git diff-tree -p -M` in one step, `diff-tree` +can compute the file pairs and rename information without the blob diffs. This +output can be fed to `diff-pairs` to generate the underlying blob diffs as done +in the following example: + +----------------------------- +git diff-tree -z -r -M $a $b | +git diff-pairs -z +----------------------------- + +Computing the tree diff upfront with rename information allows patch output +from `diff-pairs` to be progressively computed over the course of potentially +multiple invocations. + +Pathspecs are not currently supported by `diff-pairs`. Pathspec limiting should +be performed by the upstream command generating the raw diffs used as input. + +Tree objects are not currently supported as input and are rejected. + +Abbreviated object IDs in the `diff-pairs` input are not supported. Outputted +object IDs can be abbreviated using the `--abbrev` option. + +OPTIONS +------- + +include::diff-options.adoc[] + +include::diff-generate-patch.adoc[] + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/meson.build b/Documentation/meson.build index 1129ce4c85..ce990e9fe5 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -42,6 +42,7 @@ manpages = { 'git-diagnose.adoc' : 1, 'git-diff-files.adoc' : 1, 'git-diff-index.adoc' : 1, + 'git-diff-pairs.adoc' : 1, 'git-difftool.adoc' : 1, 'git-diff-tree.adoc' : 1, 'git-diff.adoc' : 1, diff --git a/Makefile b/Makefile index bcf5ed3f85..56df7aed3f 100644 --- a/Makefile +++ b/Makefile @@ -1242,6 +1242,7 @@ BUILTIN_OBJS += builtin/describe.o BUILTIN_OBJS += builtin/diagnose.o BUILTIN_OBJS += builtin/diff-files.o BUILTIN_OBJS += builtin/diff-index.o +BUILTIN_OBJS += builtin/diff-pairs.o BUILTIN_OBJS += builtin/diff-tree.o BUILTIN_OBJS += builtin/diff.o BUILTIN_OBJS += builtin/difftool.o diff --git a/builtin.h b/builtin.h index 89928ccf92..e6aad3a6a1 100644 --- a/builtin.h +++ b/builtin.h @@ -153,6 +153,7 @@ int cmd_diagnose(int argc, const char **argv, const char *prefix, struct reposit int cmd_diff_files(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_diff_index(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_diff(int argc, const char **argv, const char *prefix, struct repository *repo); +int cmd_diff_pairs(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_diff_tree(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_difftool(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_env__helper(int argc, const char **argv, const char *prefix, struct repository *repo); diff --git a/builtin/diff-pairs.c b/builtin/diff-pairs.c new file mode 100644 index 0000000000..9472b10461 --- /dev/null +++ b/builtin/diff-pairs.c @@ -0,0 +1,193 @@ +#include "builtin.h" +#include "commit.h" +#include "config.h" +#include "diff.h" +#include "diffcore.h" +#include "gettext.h" +#include "hex.h" +#include "object.h" +#include "parse-options.h" +#include "revision.h" +#include "strbuf.h" + +static unsigned parse_mode_or_die(const char *mode, const char **endp) +{ + uint16_t ret; + + *endp = parse_mode(mode, &ret); + if (!*endp) + die(_("unable to parse mode: %s"), mode); + return ret; +} + +static void parse_oid_or_die(const char *p, struct object_id *oid, + const char **endp, const struct git_hash_algo *algop) +{ + if (parse_oid_hex_algop(p, oid, endp, algop) || *(*endp)++ != ' ') + die(_("unable to parse object id: %s"), p); +} + +static void flush_diff_queue(struct diff_options *options) +{ + /* + * If rename detection is not requested, use rename information from the + * raw diff formatted input. Setting found_follow ensures diffcore_std() + * does not mess with rename information already present in queued + * filepairs. + */ + if (!options->detect_rename) + options->found_follow = 1; + diffcore_std(options); + diff_flush(options); +} + +int cmd_diff_pairs(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + struct strbuf path_dst = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + struct strbuf meta = STRBUF_INIT; + struct rev_info revs; + int ret; + + const char * const usage[] = { + N_("git diff-pairs -z []"), + NULL + }; + struct option options[] = { + OPT_END() + }; + struct option *parseopts = add_diff_options(options, &revs.diffopt); + + show_usage_with_options_if_asked(argc, argv, usage, parseopts); + + repo_init_revisions(repo, &revs, prefix); + repo_config(repo, git_diff_basic_config, NULL); + revs.disable_stdin = 1; + revs.abbrev = 0; + revs.diff = 1; + + if (setup_revisions(argc, argv, &revs, NULL) > 1) + usage_with_options(usage, parseopts); + + /* + * With the -z option, both command input and raw output are + * NUL-delimited (this mode does not effect patch output). At present + * only NUL-delimited raw diff formatted input is supported. + */ + if (revs.diffopt.line_termination) { + error(_("working without -z is not supported")); + usage_with_options(usage, parseopts); + } + + if (revs.prune_data.nr) { + error(_("pathspec arguments not supported")); + usage_with_options(usage, parseopts); + } + + if (revs.pending.nr || revs.max_count != -1 || + revs.min_age != (timestamp_t)-1 || + revs.max_age != (timestamp_t)-1) { + error(_("revision arguments not allowed")); + usage_with_options(usage, parseopts); + } + + if (!revs.diffopt.output_format) + revs.diffopt.output_format = DIFF_FORMAT_PATCH; + + while (1) { + struct object_id oid_a, oid_b; + struct diff_filepair *pair; + unsigned mode_a, mode_b; + const char *p; + char status; + + if (strbuf_getline_nul(&meta, stdin) == EOF) + break; + + p = meta.buf; + if (*p != ':') + die(_("invalid raw diff input")); + p++; + + mode_a = parse_mode_or_die(p, &p); + mode_b = parse_mode_or_die(p, &p); + + if (S_ISDIR(mode_a) || S_ISDIR(mode_b)) + die(_("tree objects not supported")); + + parse_oid_or_die(p, &oid_a, &p, repo->hash_algo); + parse_oid_or_die(p, &oid_b, &p, repo->hash_algo); + + status = *p++; + + if (strbuf_getline_nul(&path, stdin) == EOF) + die(_("got EOF while reading path")); + + switch (status) { + case DIFF_STATUS_ADDED: + pair = diff_queue_addremove(&diff_queued_diff, + &revs.diffopt, '+', mode_b, + &oid_b, 1, path.buf, 0); + if (pair) + pair->status = status; + break; + + case DIFF_STATUS_DELETED: + pair = diff_queue_addremove(&diff_queued_diff, + &revs.diffopt, '-', mode_a, + &oid_a, 1, path.buf, 0); + if (pair) + pair->status = status; + break; + + case DIFF_STATUS_TYPE_CHANGED: + case DIFF_STATUS_MODIFIED: + pair = diff_queue_change(&diff_queued_diff, &revs.diffopt, + mode_a, mode_b, &oid_a, &oid_b, + 1, 1, path.buf, 0, 0); + if (pair) + pair->status = status; + break; + + case DIFF_STATUS_RENAMED: + case DIFF_STATUS_COPIED: + { + struct diff_filespec *a, *b; + unsigned int score; + + if (strbuf_getline_nul(&path_dst, stdin) == EOF) + die(_("got EOF while reading destination path")); + + a = alloc_filespec(path.buf); + b = alloc_filespec(path_dst.buf); + fill_filespec(a, &oid_a, 1, mode_a); + fill_filespec(b, &oid_b, 1, mode_b); + + pair = diff_queue(&diff_queued_diff, a, b); + + if (strtoul_ui(p, 10, &score)) + die(_("unable to parse rename/copy score: %s"), p); + + pair->score = score * MAX_SCORE / 100; + pair->status = status; + pair->renamed_pair = 1; + } + break; + + default: + die(_("unknown diff status: %c"), status); + } + } + + flush_diff_queue(&revs.diffopt); + ret = diff_result_code(&revs); + + strbuf_release(&path_dst); + strbuf_release(&path); + strbuf_release(&meta); + release_revisions(&revs); + FREE_AND_NULL(parseopts); + + return ret; +} diff --git a/command-list.txt b/command-list.txt index c537114b46..b7ade3ab9f 100644 --- a/command-list.txt +++ b/command-list.txt @@ -96,6 +96,7 @@ git-diagnose ancillaryinterrogators git-diff mainporcelain info git-diff-files plumbinginterrogators git-diff-index plumbinginterrogators +git-diff-pairs plumbinginterrogators git-diff-tree plumbinginterrogators git-difftool ancillaryinterrogators complete git-fast-export ancillarymanipulators diff --git a/git.c b/git.c index 450d6aaa86..77c4359522 100644 --- a/git.c +++ b/git.c @@ -541,6 +541,7 @@ static struct cmd_struct commands[] = { { "diff", cmd_diff, NO_PARSEOPT }, { "diff-files", cmd_diff_files, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT }, { "diff-index", cmd_diff_index, RUN_SETUP | NO_PARSEOPT }, + { "diff-pairs", cmd_diff_pairs, RUN_SETUP | NO_PARSEOPT }, { "diff-tree", cmd_diff_tree, RUN_SETUP | NO_PARSEOPT }, { "difftool", cmd_difftool, RUN_SETUP_GENTLY }, { "fast-export", cmd_fast_export, RUN_SETUP }, diff --git a/meson.build b/meson.build index bf95576f83..9e8b365d2a 100644 --- a/meson.build +++ b/meson.build @@ -540,6 +540,7 @@ builtin_sources = [ 'builtin/diagnose.c', 'builtin/diff-files.c', 'builtin/diff-index.c', + 'builtin/diff-pairs.c', 'builtin/diff-tree.c', 'builtin/diff.c', 'builtin/difftool.c', diff --git a/t/meson.build b/t/meson.build index 780939d49f..09c7bc2fad 100644 --- a/t/meson.build +++ b/t/meson.build @@ -500,6 +500,7 @@ integration_tests = [ 't4067-diff-partial-clone.sh', 't4068-diff-symmetric-merge-base.sh', 't4069-remerge-diff.sh', + 't4070-diff-pairs.sh', 't4100-apply-stat.sh', 't4101-apply-nonl.sh', 't4102-apply-rename.sh', diff --git a/t/t4070-diff-pairs.sh b/t/t4070-diff-pairs.sh new file mode 100755 index 0000000000..2f511cc9c9 --- /dev/null +++ b/t/t4070-diff-pairs.sh @@ -0,0 +1,74 @@ +#!/bin/sh + +test_description='basic diff-pairs tests' +. ./test-lib.sh + +# This creates a diff with added, modified, deleted, renamed, copied, and +# typechange entries. That includes one in a subdirectory for non-recursive +# tests, and both exact and inexact similarity scores. +test_expect_success 'setup' ' + echo to-be-gone >deleted && + echo original >modified && + echo now-a-file >symlink && + test_seq 200 >two-hundred && + test_seq 201 500 >five-hundred && + git add . && + test_tick && + git commit -m base && + git tag base && + + echo now-here >added && + echo new >modified && + rm deleted && + mkdir subdir && + echo content >subdir/file && + mv two-hundred renamed && + test_seq 201 500 | sed s/300/modified/ >copied && + rm symlink && + git add -A . && + test_ln_s_add dest symlink && + test_tick && + git commit -m new && + git tag new +' + +test_expect_success 'diff-pairs recreates --raw' ' + git diff-tree -r -M -C -C -z base new >expect && + git diff-tree -r -M -C -C -z base new | + git diff-pairs --raw -z >actual && + test_cmp expect actual +' + +test_expect_success 'diff-pairs can create -p output' ' + git diff-tree -p -M -C -C base new >expect && + git diff-tree -r -M -C -C -z base new | + git diff-pairs -p -z >actual && + test_cmp expect actual +' + +test_expect_success 'diff-pairs does not support normal raw diff input' ' + git diff-tree -r base new | + test_must_fail git diff-pairs >out 2>err && + + test_must_be_empty out && + grep "error: working without -z is not supported" err +' + +test_expect_success 'diff-pairs does not support tree objects as input' ' + git diff-tree -z base new | + test_must_fail git diff-pairs -z >out 2>err && + + echo "fatal: tree objects not supported" >expect && + test_must_be_empty out && + test_cmp expect err +' + +test_expect_success 'diff-pairs does not support pathspec arguments' ' + git diff-tree -r -z base new | + test_must_fail git diff-pairs -z -- new >out 2>err && + + test_must_be_empty out && + grep "error: pathspec arguments not supported" err +' + +test_done From patchwork Tue Feb 25 23:39:25 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Justin Tobler X-Patchwork-Id: 13991150 Received: from mail-oa1-f50.google.com (mail-oa1-f50.google.com [209.85.160.50]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C2E4823A998 for ; Tue, 25 Feb 2025 23:42:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.160.50 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740526977; cv=none; b=pfGs8HyjpnNp0v9s+MKrguGKBX2Y/1z6JuvClpM90PsDEF9F3XxH2Gfnf/DwIn0BXq22jWtaIn4lhm3GcOjTms71smKnoBsFGgXEXKuU+gK7oY8Exb3aIajt55joMROAZPrBcx1SZ23VG1PqX1XYKYaFUU7EqmZGjJWIXNh789k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740526977; c=relaxed/simple; bh=Fe8UlSWWkoonBqR+S/zRRS8hJukSOWHkcwr34p7o3as=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=tvVpLwkXVXc1425Sep61I1NqDVGEd3F0tsK9SCUhDT7r9maeEYCmlDXEKxZb6QO9Snc+RyJjGhXs1w929iqpOZ8ZfYvWppd1INaSzNcvMhowNnEF+ualcpolLqRZHDoeU2kIQ5iFB3cZ24uuk0hD86td6FkkivA2yF1LUndx/H4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=M98PxF1u; arc=none smtp.client-ip=209.85.160.50 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="M98PxF1u" Received: by mail-oa1-f50.google.com with SMTP id 586e51a60fabf-2b1a9cbfc8dso1250477fac.2 for ; Tue, 25 Feb 2025 15:42:55 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1740526974; x=1741131774; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=7n/Sn2nmoW9FpfuveghA+kkkeOeEao55dwiuqFRSGTs=; b=M98PxF1uAaFmPBNAnoqJpRs58nuYjLKIlDBAZVdmlYBebpacle6w+zUKYd0LV08nHH Em91Nlu0mtwIIe5253IKtt08rv7AvS/Saj4Dzs/NTtE4bTnQosB5V4yYGXOfWfn+CV3X 3iZrpiR2hWg/tQksrGMFXMf+ItQXHgJV3tOBGnAa9lWHuNyEybJY9tbLs9wQ72/TSUQO ehUNN2tRptLK5UEXp4jZJo9LYxftDlznR+EzbusjCWxIVC5rbQGKD+yXtAQSaYVCmnYp NcF2RJMoe0dsZOsxAfiXIwZgeuyYhxggSj1I8+pvugvWhZXBVwiBLEBJRp/K3WTvXb+5 9fwg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1740526974; x=1741131774; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=7n/Sn2nmoW9FpfuveghA+kkkeOeEao55dwiuqFRSGTs=; b=tagsZyhpaGvGgsgIvH7WXT1Yp2todCiA5u45M0nXqgVdWVp0i/P1sZj7aXoZ+PU07A JokrtRIHRCqqF6e5yIhfwW6eyLQw3j02DCllqLsHgul5J5Rnhaf4mem50ESJSsrejhBK kqlV0G+1NbqRvZThwwybEqxkP/dXxXzIhH8lzEG6mRtPGfKcCqTgr8CBdFdfQXnL0e/F Ou7rFJfTh8uX4fsFtAbsdtwT2NYEfIa5IKfneIB+Uj2wfgT/L2ygaElMj/RRoHu1+zvJ VCr1365eCPPxc0czm1cHt4nVHq1u9imnHhaX1pRmryZ2B5J7R+Z9mSvt8UtGhfOXUpDf 7LvQ== X-Gm-Message-State: AOJu0YxiOsEe+3MWhFtT+pElHyjzp2i/YaX1lvGBgL2nuTPjMt03PFkg HQEYv7ui7FA3sP/8tBvnPyHsjvNRiwDwSNOxNglvCEjhL4KHhHMxUv8bQt7r X-Gm-Gg: ASbGncsUxGh1QMpyzKB92MPKVwauqMtRBauclNVi541kRre3+IHkkOJNMtQv6dLN4LD q3HeGomc96sZEI9PPO+JER+8q8s39qOYQc6f+QqJ3Jk1EzCucn0bArgo2WdIZd7cERo3tvExTNi IyBxc1c4bRJPWqmqPJjgDk9hHcp1wFtrJ0tOwjJjjZIRw38EkcQ0LadJxO5MXMP3JNyToojl9se XioORdKSvPnM/0NpZngiO3jF4LnUAr1ct593MGZoVYAjFGdD18UR1RB339QirhLIUdynlkm+EdK vI5Fz9+LwFp000YQSGmkGMFpBhlP2mwuWA== X-Google-Smtp-Source: AGHT+IFEEeciHy6IBEw00WkQP4d08pG/mKsxuW/mn0X+ULbTwLOPmcudPtgyUZrh2PHuzKttKjNeNQ== X-Received: by 2002:a05:6870:3929:b0:296:df26:8a6e with SMTP id 586e51a60fabf-2bd5187a062mr15087798fac.35.1740526974360; Tue, 25 Feb 2025 15:42:54 -0800 (PST) Received: from denethor.localdomain ([136.50.74.45]) by smtp.gmail.com with ESMTPSA id 586e51a60fabf-2c1113f5bc5sm609308fac.21.2025.02.25.15.42.53 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 25 Feb 2025 15:42:53 -0800 (PST) From: Justin Tobler To: git@vger.kernel.org Cc: ps@pks.im, karthik.188@gmail.com, phillip.wood123@gmail.com, Justin Tobler Subject: [PATCH v3 3/3] builtin/diff-pairs: allow explicit diff queue flush Date: Tue, 25 Feb 2025 17:39:25 -0600 Message-ID: <20250225233925.1345086-4-jltobler@gmail.com> X-Mailer: git-send-email 2.48.1 In-Reply-To: <20250225233925.1345086-1-jltobler@gmail.com> References: <20250212041825.2455031-1-jltobler@gmail.com> <20250225233925.1345086-1-jltobler@gmail.com> Precedence: bulk X-Mailing-List: git@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 The diffs queued from git-diff-pairs(1) are flushed when stdin is closed. To enable greater flexibility, allow control over when the diff queue is flushed by writing a single NUL byte on stdin between input file pairs. Diff output between flushes is separated by a single NUL byte. Signed-off-by: Justin Tobler --- Documentation/git-diff-pairs.adoc | 4 ++++ builtin/diff-pairs.c | 13 +++++++++++++ t/t4070-diff-pairs.sh | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/Documentation/git-diff-pairs.adoc b/Documentation/git-diff-pairs.adoc index e31f2e2fbb..f99fcd1ead 100644 --- a/Documentation/git-diff-pairs.adoc +++ b/Documentation/git-diff-pairs.adoc @@ -17,6 +17,10 @@ in the NUL-terminated raw output format as generated by commands such as `git diff-tree -z -r --raw`. By default, the outputted diffs are computed and shown in the patch format when stdin closes. +A single NUL byte may be written to stdin between raw input lines to compute +file pair diffs up to that point instead of waiting for stdin to close. A NUL +byte is also written to the output to delimit between these batches of diffs. + Usage of this command enables the traditional diff pipeline to be broken up into separate stages where `diff-pairs` acts as the output phase. Other commands, such as `diff-tree`, may serve as a frontend to compute the raw diff --git a/builtin/diff-pairs.c b/builtin/diff-pairs.c index 9472b10461..7130569332 100644 --- a/builtin/diff-pairs.c +++ b/builtin/diff-pairs.c @@ -63,6 +63,7 @@ int cmd_diff_pairs(int argc, const char **argv, const char *prefix, repo_init_revisions(repo, &revs, prefix); repo_config(repo, git_diff_basic_config, NULL); + revs.diffopt.no_free = 1; revs.disable_stdin = 1; revs.abbrev = 0; revs.diff = 1; @@ -106,6 +107,17 @@ int cmd_diff_pairs(int argc, const char **argv, const char *prefix, break; p = meta.buf; + if (!*p) { + flush_diff_queue(&revs.diffopt); + /* + * When the diff queue is explicitly flushed, append a + * NUL byte to separate batches of diffs. + */ + fputc('\0', revs.diffopt.file); + fflush(revs.diffopt.file); + continue; + } + if (*p != ':') die(_("invalid raw diff input")); p++; @@ -180,6 +192,7 @@ int cmd_diff_pairs(int argc, const char **argv, const char *prefix, } } + revs.diffopt.no_free = 0; flush_diff_queue(&revs.diffopt); ret = diff_result_code(&revs); diff --git a/t/t4070-diff-pairs.sh b/t/t4070-diff-pairs.sh index 2f511cc9c9..3352bfe0b9 100755 --- a/t/t4070-diff-pairs.sh +++ b/t/t4070-diff-pairs.sh @@ -71,4 +71,13 @@ test_expect_success 'diff-pairs does not support pathspec arguments' ' grep "error: pathspec arguments not supported" err ' +test_expect_success 'diff-pairs explicit queue flush' ' + git diff-tree -r -M -C -C -z base new >expect && + printf "\0" >>expect && + git diff-tree -r -M -C -C -z base new >>expect && + + git diff-pairs --raw -z actual && + test_cmp expect actual +' + test_done