diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index 6be28c8a4e..489a9d5fd0 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -240,11 +240,14 @@ remove it anyway.
Like `--delete-merged`, but also delete a branch whose changes
the upstream already has even though its commits are not there,
as happens after a squash or rebase merge. Such a branch is
- deleted when some upstream commit since the fork point has its
- version of every file it changed, even if those changes were
- later reverted, and the message names the first such commit.
- It does not matter who made that commit. The same rules for
- which branches are kept apply as for `--delete-merged`.
+ deleted when some upstream commit since the fork point has all
+ of its changes, even if they were later reverted, and the
+ message names the first such commit. It does not matter who made
+ that commit. A file counts when the commit has the branch's
+ version of it, or when merging the branch's version into the
+ commit's leaves it unchanged, which reads the file and may
+ download blobs in a partial clone. The same rules for which
+ branches are kept apply as for `--delete-merged`.
`--dry-run`::
With `--delete-merged` or `--delete-integrated`, print which
diff --git a/builtin/branch.c b/builtin/branch.c
index 818c916791..477b4a92c4 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -33,8 +33,10 @@
#include "diff.h"
#include "diffcore.h"
#include "hex.h"
+#include "merge-ll.h"
#include "revision.h"
#include "tree-walk.h"
+#include "xdiff-interface.h"
static const char * const builtin_branch_usage[] = {
N_("git branch [<options>] [-r | -a] [--merged] [--no-merged] [(--forked <branch>)...]"),@@ -853,10 +855,17 @@ static int branch_pushes_to_upstream(struct branch *branch,
return ret;
}
+enum change_state {
+ CHANGE_MISSING,
+ CHANGE_LANDED,
+ CHANGE_NEEDS_MERGE,
+};
+
struct branch_change {
char *path;
- struct object_id branch_oid;
+ struct object_id base_oid, branch_oid, upstream_oid;
unsigned short branch_mode;
+ enum change_state state;
};
struct landing_search {@@ -888,6 +897,8 @@ static void collect_branch_changes(struct landing_search *search,
search->changes_alloc);
change = &search->changes[search->changes_nr++];
change->path = xstrdup(p->two->path);
+ oidcpy(&change->base_oid, DIFF_FILE_VALID(p->one) ?
+ &p->one->oid : null_oid(the_hash_algo));
oidcpy(&change->branch_oid, DIFF_FILE_VALID(p->two) ?
&p->two->oid : null_oid(the_hash_algo));
change->branch_mode = p->two->mode;
@@ -895,25 +906,66 @@ static void collect_branch_changes(struct landing_search *search,
diff_flush(&diffopt);
}
-static int change_landed(const struct branch_change *change,
- struct commit *commit)
+static int merge_keeps_upstream(const struct branch_change *change)
+{
+ mmfile_t base, upstream, branch;
+ mmfile_t result = { 0 };
+ int ret;
+
+ repo_read_index(the_repository);
+ read_mmblob(&base, the_repository->objects, &change->base_oid);
+ read_mmblob(&upstream, the_repository->objects, &change->upstream_oid);
+ read_mmblob(&branch, the_repository->objects, &change->branch_oid);
+ ret = ll_merge(&result, change->path, &base, "base",
+ &upstream, "upstream", &branch, "branch",
+ the_repository->index, NULL) == LL_MERGE_OK &&
+ result.size == upstream.size &&
+ !memcmp(result.ptr, upstream.ptr, upstream.size);
+
+ free(base.ptr);
+ free(upstream.ptr);
+ free(branch.ptr);
+ free(result.ptr);
+ return ret;
+}
+
+static enum change_state change_state_at(struct branch_change *change,
+ struct commit *commit)
{
- struct object_id oid;
unsigned short mode;
if (get_tree_entry(the_repository, get_commit_tree_oid(commit),
- change->path, &oid, &mode))
- return is_null_oid(&change->branch_oid);
- return oideq(&oid, &change->branch_oid) &&
- mode == change->branch_mode;
+ change->path, &change->upstream_oid, &mode))
+ return is_null_oid(&change->branch_oid) ?
+ CHANGE_LANDED : CHANGE_MISSING;
+ if (oideq(&change->upstream_oid, &change->branch_oid))
+ return mode == change->branch_mode ?
+ CHANGE_LANDED : CHANGE_MISSING;
+ if (is_null_oid(&change->base_oid) ||
+ is_null_oid(&change->branch_oid) ||
+ oideq(&change->upstream_oid, &change->base_oid) ||
+ mode != change->branch_mode || !S_ISREG(mode))
+ return CHANGE_MISSING;
+ return CHANGE_NEEDS_MERGE;
}
static int changes_landed(struct landing_search *search,
struct commit *commit)
{
- for (size_t i = 0; i < search->changes_nr; i++)
- if (!change_landed(&search->changes[i], commit))
+ for (size_t i = 0; i < search->changes_nr; i++) {
+ struct branch_change *change = &search->changes[i];
+
+ change->state = change_state_at(change, commit);
+ if (change->state == CHANGE_MISSING)
return 0;
+ }
+ for (size_t i = 0; i < search->changes_nr; i++) {
+ struct branch_change *change = &search->changes[i];
+
+ if (change->state == CHANGE_NEEDS_MERGE &&
+ !merge_keeps_upstream(change))
+ return 0;
+ }
return 1;
}
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 1fbb173ad4..d4ebbd0ae9 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -2122,6 +2122,48 @@ test_expect_success '--delete-integrated deletes a squash merged branch that was
)
'
+test_expect_success '--delete-integrated deletes a squash merged branch after upstream changed the same file' '
+ (
+ cd upstream &&
+ git checkout next &&
+ test_write_lines 1 2 3 4 5 6 7 8 9 10 >shared &&
+ git add shared &&
+ git commit -m "add shared" &&
+ git checkout main
+ ) &&
+ setup_repo_for_delete_merged &&
+ (
+ cd repo &&
+ git checkout -b edits-shared --track origin/next &&
+ test_write_lines 1 two 3 4 5 6 7 8 9 10 >shared &&
+ git commit -a -m "edit line 2" &&
+ git push origin edits-shared &&
+ git checkout --detach
+ ) &&
+ (
+ cd upstream &&
+ git checkout next &&
+ test_write_lines 1 2 3 4 five 6 7 8 9 10 >shared &&
+ git commit -a -m "edit line 5" &&
+ git checkout main
+ ) &&
+ squash_merge_upstream edits-shared &&
+ squash=$(git -C upstream rev-parse --short next) &&
+ (
+ cd repo &&
+ git fetch origin &&
+ sha=$(git rev-parse --short edits-shared) &&
+
+ git branch --delete-integrated origin/next >actual 2>&1 &&
+ echo "Deleted branch edits-shared (was $sha, landed as $squash)." >expect &&
+ test_cmp expect actual &&
+
+ check_branches <<-\EOF
+ main
+ EOF
+ )
+'
+
test_expect_success '--delete-integrated reports a merge plainly next to an unmerged branch' '
setup_repo_for_delete_merged &&
push_topic a-unmerged unmerged-work &&
--
gitgitgadget