Fundamentals 14 min read

Why Git Merge Can Produce Empty Commits and How to Prevent Them

This article analyzes a common Git issue where a merge creates an empty commit that drops master changes, explains the underlying cause involving `git merge --continue` and `--allow-empty`, demonstrates reproduction steps, and provides mitigation strategies such as pre‑commit hooks and proper merge handling.

Kuaishou E-commerce Frontend Team
Kuaishou E-commerce Frontend Team
Kuaishou E-commerce Frontend Team
Why Git Merge Can Produce Empty Commits and How to Prevent Them

Prologue

During a release, the main branch (master) had code that was lost after a merge, leading to a production issue. The problem is common and classic, and the following analysis explains it step by step.

Phenomenon

When preparing a release branch, the master branch had new changes.

The feature branch pulled master, merged, and committed. The git history was:

○ git pull origin master
○ git merge origin/master
○ git merge --continue
○ git merge origin/master
○ git pull origin master
○ git push

After these operations, the feature branch did not contain the latest master code. A merge request (MR) to the release branch was created, and the missing code caused the problem at release time.

No Interception on Release?

If the release branch does not contain the latest master commit, the release should be blocked. However, because the hash of the latest master commit existed in the release branch, the release was allowed.

Investigation

1. Investigation Process

• The merge commit showed no changes.

Checked the two parent nodes of the merge and repeated the same steps.

Multiple tests creating MRs from the two nodes showed no conflicts and could not reproduce the issue.

Since forward testing failed, a reverse investigation was attempted.

2. Empty Commit

An empty commit is a commit with no file changes. It can be useful for triggering CI builds or other purposes, but Git normally blocks such commits unless explicitly allowed. The command to create an empty commit is:

git commit --allow-empty (relevant source code excerpt shown below)

/*
  * Reject an attempt to record a non-merge empty commit without
  * explicit --allow-empty. In the cherry-pick case, it may be
  * empty due to conflict resolution, which the user should okay.
  */
    if (!committable && whence != FROM_MERGE && !allow_empty &&
        !(amend && is_a_merge(current_head))) {
        s->hints = advice_enabled(ADVICE_STATUS_HINTS);
        s->display_comment_prefix = old_display_comment_prefix;
        run_status(stdout, index_file, prefix, 0, s);
        if (amend)
            fputs(_(empty_amend_advice), stderr);
        else if (is_from_cherry_pick(whence) ||
             whence == FROM_REBASE_PICK) {
            fputs(_(empty_cherry_pick_advice), stderr);
            if (whence == FROM_CHERRY_PICK_SINGLE)
                fputs(_(empty_cherry_pick_advice_single), stderr);
            else if (whence == FROM_CHERRY_PICK_MULTI)
                fputs(_(empty_cherry_pick_advice_multi), stderr);
            else
                fputs(_(empty_rebase_pick_advice), stderr);
        }
        return 0;
    }

When --allow-empty is true or the command is invoked from a merge, the empty change does not raise an error and can be committed.

git merge --continue can also create an empty commit when invoked during a merge. The relevant source code is:

static struct option builtin_merge_options[] = {...
  OPT_BOOL(0, "continue", &continue_current_merge,
        N_("continue the current in‑progress merge")),
  OPT_END()
};
...
int cmd_merge(int argc, const char **argv, const char *prefix)
{
    ...
    if (continue_current_merge) {
        int nargc = 1;
        const char *nargv[] = {"commit", NULL};

        if (orig_argc != 2)
            usage_msg_opt(_("--continue expects no arguments"),
                  builtin_merge_usage, builtin_merge_options);

        if (!file_exists(git_path_merge_head(the_repository)))
            die(_("There is no merge in progress (MERGE_HEAD missing)."));

        /* Invoke 'git commit' */
        ret = cmd_commit(nargc, nargv, prefix);
        goto done;
    }
}

The code shows that when --continue is set, Git will invoke git commit if a merge is in progress, allowing an empty commit to be created.

3. Conclusion

Thus, after a merge is interrupted, using git merge --continue or git commit --allow-empty can produce an empty commit when there are no changes, effectively allowing the commit to go through.

Reproduction Steps

The following steps were performed in the zcb-test-js-test-22 repository (full permissions granted for testing).

1. Video Demonstration

2. Detailed Process

Create a feat_test branch from master.

Modify CHANGELOG on master to simulate a change.

Develop and commit on feat_test.

On feat_test, run git pull origin master to fetch the change (exit the merge editor with Ctrl+Z or close it).

Discard all changes using VSCode.

Run git merge --continue, which creates an empty commit 86e4142a.

Create an MR from feat_test to release; the master changes are missing.

The MR is not noticed, the release branch is merged, and the problem occurs.

3. VSCode Discard Log

Log excerpts showing discarded files:

2023-08-30 20:55:46.722 [info] > git branch [19ms]
2023-08-30 20:55:46.744 [info] > git reset -q HEAD -- . [21ms]
2023-08-30 20:55:46.763 [info] > git for-each-ref --format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)%00%(upstream:remotename)%00%(upstream:remoteref) --ignore-case refs/heads/feat_test refs/remotes/feat_test [18ms]
2023-08-30 20:55:46.845 [info] > git for-each-ref --format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)%00%(upstream:remotename)%00%(upstream:remoteref) --ignore-case refs/heads/feat_test refs/remotes/feat_test [17ms]
2023-08-30 20:55:49.585 [info] > git checkout -q -- /Users/a1/demo/zcb-test-js-test-22/CHANGELOG.md /Users/a1/demo/zcb-test-js-test-22/README.md /Users/a1/demo/zcb-test-js-test-22/build.sh [16ms]
2023-08-30 20:55:49.604 [info] > git for-each-ref --format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)%00%(upstream:remotename)%00%(upstream:remoteref) --ignore-case refs/heads/feat_test refs/remotes/feat_test [17ms]
2023-08-30 20:55:51.880 [info] > git for-each-ref --format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)%00%(upstream:remotename)%00%(upstream:remoteref) --ignore-case refs/heads/feat_test refs/remotes/feat_test [12ms]
2023-08-30 20:55:56.927 [info] > git for-each-ref --format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)%00%(upstream:remotename)%00%(upstream:remoteref) --ignore-case refs/heads/feat_test refs/remotes/feat_test [22ms]

VSCode also executed git checkout -q -- on the same files, discarding the master changes.

4. Verification

a. Log Investigation

Relevant logs include git reflog, bash history, local tool logs, and VSCode Git logs.

b. Conclusion

The user discarded the master changes via VSCode during the merge, and git merge --continue created an empty commit, causing the loss.

Mitigation

1. Avoid Empty Commits

Use a .git/hooks/pre-commit script to block commits with no changes, including merge commits invoked with git merge --continue:

#!/usr/bin/env node
const { execSync } = require("child_process");
try {
  const gitCommand = "git diff --cached --name-only";
  const gitStagedFiles = execSync(gitCommand, { encoding: "utf-8" })
    .trim()
    .split("
");
  // Prevent empty commits such as git merge --continue or git commit --allow-empty
  if (gitStagedFiles.length === 0) {
    console.error(`Error: 不允许空变更提交`);
    process.exit(1);
  }
} catch (error) {
  process.exit(1);
}

2. Mitigation Test

Testing the above solution shows that commits without changes are blocked.

Note: If a user partially reverts, the hook cannot block the commit because it is no longer empty; in such cases, merge guidelines and MR reviewer vigilance are required.

Git Commit & Merge Guidelines

1. Commit Guidelines

Do not allow empty changes or empty commit messages.

2. Git Merge Behavior Guidelines

When pulling changes without conflicts, if Git opens the merge editor:

Use ESC + :wq, ESC + q!, or ESC + Shift+zz to exit the editor and automatically merge the changes.

Prohibited actions:

When approving an MR, carefully check the changes and commits.

Extension

When investigating client‑side Git issues, examine git reflog, terminal Git command logs, and editor Git logs to obtain a complete picture of all Git operations.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GittroubleshootingmergeVersion Controlempty commitpre-commit hook
Kuaishou E-commerce Frontend Team
Written by

Kuaishou E-commerce Frontend Team

Kuaishou E-commerce Frontend Team, welcome to join us

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.