fix(bump): skip commit step when there are no files to commit - #1969
fix(bump): skip commit step when there are no files to commit#1969bearomorphism wants to merge 3 commits into
Conversation
When using `version_provider = "scm"` with no `version_files` and no `--changelog` / `update_changelog_on_bump`, the bump has nothing to commit. `cz bump` would then call `git commit -a`, which exits with `nothing to commit, working tree clean` and the bump fails with `BumpCommitFailedError`. Add a `git.has_pending_changes()` helper that returns `True` only if `git commit -a` would actually commit something, and use it to decide whether to issue the bump commit. When there is nothing to commit, the tag is created on `HEAD` directly. This is the typical flow for SCM- driven projects that derive the version from the latest tag. Closes commitizen-tools#1530 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1969 +/- ##
=======================================
Coverage 98.23% 98.24%
=======================================
Files 61 61
Lines 2779 2790 +11
=======================================
+ Hits 2730 2741 +11
Misses 49 49 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Pull request overview
This PR fixes cz bump failing in SCM-based versioning setups where a bump produces no file changes (no version files and no changelog), by skipping the bump commit step when there is nothing to commit and proceeding directly to tagging.
Changes:
- Add
git.has_pending_changes()to detect tracked-file changes thatgit commit -awould commit. - Gate the bump commit + retry logic behind a “pending changes” check and skip straight to tagging when clean.
- Add a regression test covering two consecutive bumps in a
version_provider = "scm"repo.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
* skip git.add when updated_files is empty * remove now-misleading FIXME on has_pending_changes guard * use consistent double-backtick reST formatting in has_pending_changes docstring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The git.add return-code check added in the previous reviewer-feedback commit was not exercised by tests. This adds a regression test that mocks git.add returning non-zero and asserts BumpCommitFailedError is raised with a useful message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Description
Closes #1530.
Why
cz bumpunconditionally callsgit commit -aafter updating version files, then treats any non-zero exit code as a fatalBumpCommitFailedError. Whenversion_provider = "scm"is configured with noversion_filesand no changelog generation, there are no files for the bump to modify — so git exits with "nothing to commit, working tree clean" and the error is raised before any tag is ever created.Reported by @loelkes on commitizen 4.8.2 / Python 3.13 / macOS (#1530): the exact error is
2nd git.commit error: "On branch main\nnothing to commit, working tree clean\n". Maintainer @Lee-W confirmed the report ("this is a valid bug"), and @woile linked a prior fix attempt (PR #996) that was never merged. A triage note from the open-issues audit (2026-05-09) found the bug is worse on master (v4.15.1) than the original report described: even the first bump now fails, because newer commitizen versions no longer modifycz.tomlduring a bump.The
version_provider = "scm"workflow is fully documented — the version is derived entirely from git tags and no file is ever written — yet there was no way to use it without also enablingupdate_changelog_on_bumpas a workaround.What changed
commitizen/git.pyhas_pending_changes()helper afteris_staging_clean(line 311): runsgit status --porcelain --untracked-files=noand returnsTruewhen there are tracked-file changes thatgit commit -awould commitcommitizen/commands/bump.pygit.commit+ retry block (lines 383–399) behindif git.has_pending_changes(): …; emit anout.infomessage and skip straight to tag creation when the tree is cleantests/commands/test_bump_command.pytest_bump_skips_commit_when_no_files_changed: two consecutivecz bump --yescalls in aversion_provider = "scm"repo both succeed and produce the expectedv0.1.0/v0.1.1tagsHow it works
git.has_pending_changes()(new function,commitizen/git.pyafter line 311) runsgit status --porcelain --untracked-files=no. The--untracked-files=noflag restricts the query to tracked files — exactly the set thatgit commit -awould stage and commit. The function returnsTrueif there is any non-empty output,Falseif the working tree is clean for tracked files.git.is_staging_clean()(commitizen/git.py:308–311)?is_staging_cleanrunsgit diff --cached --name-only, which checks only the index (already-staged changes).git commit -aalso commits unstaged modifications to tracked files — for example a file reformatted by a pre-commit hook that wasn't re-staged.git status --porcelain --untracked-files=nocovers both cases in a single call, makinghas_pending_changesthe correct guard forgit commit -a.git commitas non-fatal? The existing error path also surfaces genuine commit failures — bad GPG key, locked.git/index, pre-commit hook rejection — as non-zero exit codes. Silently swallowing those would mask real problems. Checking before attempting the commit avoids the ambiguity entirely.self.retry and c.return_code != 0 and self.changelog_flag) is moved inside theelsebranch and continues to work exactly as before — it only runs when there was something to commit and the first attempt failed.out.info("No file changes; skipping bump commit and tagging HEAD.")is emitted so the user can see why no bump commit appears ingit log.Backward compatibility
version_filesorupdate_changelog_on_bump = truealways have pending changes after the update step;has_pending_changes()returnsTrueand the commit path runs exactly as before.self.retry) is preserved intact inside theelsebranch.gpg_sign), and annotated-tag options are unaffected — those code paths all execute after the commit block.git.is_staging_clean()is untouched; nothing that calls it is changed.tests/commands/test_bump_command.pytests pass (132 tests; 4 GPG-fixture tests deselected on Windows — pre-existing, unrelated).Checklist
Was generative AI tooling used to co-author this PR?
Generated-by: Claude following the guidelines
Code Changes
uv run poe alllocally to ensure this change passes linter check and testsExpected Behavior
version_provider = "scm", noversion_files, no changelog — firstcz bumpv0.1.0created onHEAD; no bump commit; exit 0cz bumpafter another conventional commitv0.1.1created; no error; exit 0version_provider = "pep621"withversion_filesconfigured —cz bumpversion_provider = "scm"withupdate_changelog_on_bump = true—cz bumphas_pending_changes()returnsTrue→ bump commit created → tag appliedSteps to Test This Pull Request
Additional Context
This fix was identified during the open-issues audit tracked in #1964. A triage note (@bearomorphism, 2026-05-09) confirmed the bug reproduces on master (v4.15.1) — and is worse than the original report, failing on the very first bump — and suggested checking
is_staging_clean()before the commit. The implementation instead introduces a dedicatedgit.has_pending_changes()helper usinggit status --porcelain --untracked-files=no, which correctly covers both staged and unstaged modifications to tracked files (the full set thatgit commit -aacts on); see the "How it works" section for why the existingis_staging_clean()was insufficient. A prior fix attempt existed as PR #996 but was never merged.