Skip to content

fix(bump): skip commit step when there are no files to commit - #1969

Open
bearomorphism wants to merge 3 commits into
commitizen-tools:masterfrom
bearomorphism:fix/1530-skip-commit-when-clean
Open

fix(bump): skip commit step when there are no files to commit#1969
bearomorphism wants to merge 3 commits into
commitizen-tools:masterfrom
bearomorphism:fix/1530-skip-commit-when-clean

Conversation

@bearomorphism

@bearomorphism bearomorphism commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Closes #1530.

Why

cz bump unconditionally calls git commit -a after updating version files, then treats any non-zero exit code as a fatal BumpCommitFailedError. When version_provider = "scm" is configured with no version_files and 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 modify cz.toml during 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 enabling update_changelog_on_bump as a workaround.

What changed

File Change
commitizen/git.py Add has_pending_changes() helper after is_staging_clean (line 311): runs git status --porcelain --untracked-files=no and returns True when there are tracked-file changes that git commit -a would commit
commitizen/commands/bump.py Gate the git.commit + retry block (lines 383–399) behind if git.has_pending_changes(): …; emit an out.info message and skip straight to tag creation when the tree is clean
tests/commands/test_bump_command.py Add test_bump_skips_commit_when_no_files_changed: two consecutive cz bump --yes calls in a version_provider = "scm" repo both succeed and produce the expected v0.1.0 / v0.1.1 tags

How it works

  • git.has_pending_changes() (new function, commitizen/git.py after line 311) runs git status --porcelain --untracked-files=no. The --untracked-files=no flag restricts the query to tracked files — exactly the set that git commit -a would stage and commit. The function returns True if there is any non-empty output, False if the working tree is clean for tracked files.
  • Why not reuse the existing git.is_staging_clean() (commitizen/git.py:308–311)? is_staging_clean runs git diff --cached --name-only, which checks only the index (already-staged changes). git commit -a also 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=no covers both cases in a single call, making has_pending_changes the correct guard for git commit -a.
  • Why not treat a non-zero exit from git commit as 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.
  • The retry path is preserved: the pre-commit-reformatter retry (self.retry and c.return_code != 0 and self.changelog_flag) is moved inside the else branch and continues to work exactly as before — it only runs when there was something to commit and the first attempt failed.
  • When skipping the commit, out.info("No file changes; skipping bump commit and tagging HEAD.") is emitted so the user can see why no bump commit appears in git log.

Backward compatibility

  • Repos that have version_files or update_changelog_on_bump = true always have pending changes after the update step; has_pending_changes() returns True and the commit path runs exactly as before.
  • The retry-on-reformat path (self.retry) is preserved intact inside the else branch.
  • Pre-commit hook integration, GPG signing (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.
  • All pre-existing tests/commands/test_bump_command.py tests pass (132 tests; 4 GPG-fixture tests deselected on Windows — pre-existing, unrelated).

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce
  • Run uv run poe all locally to ensure this change passes linter check and tests
  • Manually test the changes (see "Steps to Test" below)
  • Update the documentation for the changes

Expected Behavior

Scenario Outcome
version_provider = "scm", no version_files, no changelog — first cz bump Tag v0.1.0 created on HEAD; no bump commit; exit 0
Same config — second cz bump after another conventional commit Tag v0.1.1 created; no error; exit 0
version_provider = "pep621" with version_files configured — cz bump Behaviour unchanged: version file updated → bump commit created → tag applied
version_provider = "scm" with update_changelog_on_bump = truecz bump Behaviour unchanged: changelog written → has_pending_changes() returns True → bump commit created → tag applied

Steps to Test This Pull Request

git fetch fork fix/1530-skip-commit-when-clean
git checkout fork/fix/1530-skip-commit-when-clean

# 1. Targeted regression test.
uv run pytest tests/commands/test_bump_command.py::test_bump_skips_commit_when_no_files_changed -v

# 2. Reproduce the bug, then verify the fix.
mkdir /tmp/cz-1530 && cd /tmp/cz-1530
git init -b main
git config user.name test && git config user.email test@example.com
printf '[tool.commitizen]\nversion_provider = "scm"\ntag_format = "v$version"\n' > cz.toml
git add cz.toml && git commit -m "feat: setup"

# First bump — previously failed with BumpCommitFailedError on master.
cz bump --yes
# Expected: "No file changes; skipping bump commit and tagging HEAD." + exit 0
git tag --list   # should show v0.1.0

# Second bump — the scenario from the original report.
touch foobar && git add foobar && git commit -m "feat: foobar"
cz bump --yes
# Expected: exit 0, no "nothing to commit" error
git tag --list   # should show v0.1.0 and v0.2.0

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 dedicated git.has_pending_changes() helper using git status --porcelain --untracked-files=no, which correctly covers both staged and unstaged modifications to tracked files (the full set that git commit -a acts on); see the "How it works" section for why the existing is_staging_clean() was insufficient. A prior fix attempt existed as PR #996 but was never merged.

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

codecov Bot commented May 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.24%. Comparing base (4b93a50) to head (c04048c).
⚠️ Report is 3 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 that git commit -a would 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.

File Description
commitizen/git.py Adds a helper to detect pending tracked changes before attempting git commit -a.
commitizen/commands/bump.py Skips the bump commit when there are no pending changes and continues to tag creation.
tests/commands/test_bump_command.py Adds regression coverage for SCM provider bumps with no file modifications.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commitizen/commands/bump.py Outdated
Comment thread commitizen/commands/bump.py Outdated
Comment thread commitizen/git.py Outdated
bearomorphism and others added 2 commits May 9, 2026 22:30
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment