Summary
RepositoryService.ops.push() (the OperationsGitSubProvider.push implementation in the @gitlens/git-cli package) resolves successfully when git push is rejected with a non-fast-forward error whose output contains "tip of your current branch is behind its remote counterpart". The caller cannot tell a real push from a rejected one, and the remote is left unchanged.
This was found while debugging a downstream (Kepler) issue: pushing a branch that needs a force-push reported success, but nothing reached the remote.
Root cause
pushCore invokes this.git.run({ cwd: repoPath, ...runOptions }, ...params) without errors: 'throw', so a rejection falls through to the default handler. defaultExceptionHandler matches the message against GitWarnings.tipBehind (/tip of your current branch is behind/i), treats it as non-fatal, logs a warning, and returns without throwing. pushCore's catch block — which is written to build a PushError via getGitCommandError('push', ...) — therefore never runs, and run resolves with a success-shaped result ({ stdout: '', stderr: undefined, exitCode: 0 }).
The contradiction confirms this is an oversight, not intended:
errorToReasonMap['push'] already contains [GitWarnings.tipBehind, 'tipBehind']
PushErrorReason includes 'tipBehind', and PushError has a dedicated message for it
pushCore's catch already maps and builds the PushError
…but that mapping is unreachable for tipBehind because the default handler swallows the rejection first.
The bug is broader than tipBehind: because defaultExceptionHandler checks all GitWarnings regexes before pushCore's catch can map anything, a push that fails with a message matching GitWarnings.remoteConnectionError (Could not read from remote repository) or GitWarnings.noRemoteRepositorySpecified is also silently reported as success. Other push failures (e.g. remoteAhead from "remote contains work", stale info with --force-with-lease) are GitErrors (not warnings), so they already throw correctly.
The reported case is exactly the common force-push scenario: you rewrote your own history (amend/rebase) and nobody else touched the remote, so a normal git push is rejected as non-fast-forward — and GitLens (e.g. gitRepositoryService.ts which surfaces PushError to the user) never sees the failure.
Expected behavior
A rejected push should reject with a PushError (reason 'tipBehind' for the non-fast-forward case) so callers can surface the failure (and offer force-push where appropriate).
Fix
Have pushCore pass errors: 'throw' so its catch always runs and builds the correct PushError — matching what commit, merge, rebase, and cherryPick already do. (Moving the tipBehind regex out of GitWarnings would also work but is riskier, since the default handler uses it for every command.)
Affected versions
Present in the published @gitlens/git-cli 0.2.0 and 0.3.0 (where run was renamed to exec but execCore keeps the same logic and pushCore still omits errors: 'throw').
Summary
RepositoryService.ops.push()(theOperationsGitSubProvider.pushimplementation in the@gitlens/git-clipackage) resolves successfully whengit pushis rejected with a non-fast-forward error whose output contains "tip of your current branch is behind its remote counterpart". The caller cannot tell a real push from a rejected one, and the remote is left unchanged.This was found while debugging a downstream (Kepler) issue: pushing a branch that needs a force-push reported success, but nothing reached the remote.
Root cause
pushCoreinvokesthis.git.run({ cwd: repoPath, ...runOptions }, ...params)withouterrors: 'throw', so a rejection falls through to the default handler.defaultExceptionHandlermatches the message againstGitWarnings.tipBehind(/tip of your current branch is behind/i), treats it as non-fatal, logs a warning, and returns without throwing.pushCore'scatchblock — which is written to build aPushErrorviagetGitCommandError('push', ...)— therefore never runs, andrunresolves with a success-shaped result ({ stdout: '', stderr: undefined, exitCode: 0 }).The contradiction confirms this is an oversight, not intended:
errorToReasonMap['push']already contains[GitWarnings.tipBehind, 'tipBehind']PushErrorReasonincludes'tipBehind', andPushErrorhas a dedicated message for itpushCore'scatchalready maps and builds thePushError…but that mapping is unreachable for
tipBehindbecause the default handler swallows the rejection first.The bug is broader than
tipBehind: becausedefaultExceptionHandlerchecks allGitWarningsregexes beforepushCore'scatchcan map anything, a push that fails with a message matchingGitWarnings.remoteConnectionError(Could not read from remote repository) orGitWarnings.noRemoteRepositorySpecifiedis also silently reported as success. Other push failures (e.g.remoteAheadfrom "remote contains work",stale infowith--force-with-lease) areGitErrors(not warnings), so they already throw correctly.The reported case is exactly the common force-push scenario: you rewrote your own history (amend/rebase) and nobody else touched the remote, so a normal
git pushis rejected as non-fast-forward — and GitLens (e.g.gitRepositoryService.tswhich surfacesPushErrorto the user) never sees the failure.Expected behavior
A rejected push should reject with a
PushError(reason'tipBehind'for the non-fast-forward case) so callers can surface the failure (and offer force-push where appropriate).Fix
Have
pushCorepasserrors: 'throw'so itscatchalways runs and builds the correctPushError— matching whatcommit,merge,rebase, andcherryPickalready do. (Moving thetipBehindregex out ofGitWarningswould also work but is riskier, since the default handler uses it for every command.)Affected versions
Present in the published
@gitlens/git-cli0.2.0 and 0.3.0 (whererunwas renamed toexecbutexecCorekeeps the same logic andpushCorestill omitserrors: 'throw').