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.
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 pushAfter 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Kuaishou E-commerce Frontend Team
Kuaishou E-commerce Frontend Team, welcome to join us
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
