Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/docs/how-to/pushing-to-remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Only force push when you [rebased](/learn/concepts/git/merge-vs-rebase) your own
git push --force-with-lease origin treq/your-branch
```

Use `--force-with-lease` instead of `--force`. It fails if someone else pushed, preventing accidental overwrites.
Use [`--force-with-lease`](/learn/concepts/git/force-with-lease) instead of `--force`. It fails if someone else pushed, preventing accidental overwrites.

## Best Practices

Expand Down
3 changes: 2 additions & 1 deletion web/learn/concepts/git/cherry-pick-vs-rebase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Interactive rebase lets you reorder, combine, edit, or remove commits before sha
git rebase -i origin/main
```

If the rebased branch already exists on a remote, updating it usually requires a force push. Use `--force-with-lease`, which refuses the update when the remote ref differs from the value your local repository expects:
If the rebased branch already exists on a remote, updating it usually requires a force push. Use [`--force-with-lease`](./force-with-lease), which refuses the update when the remote ref differs from the value your local repository expects:

```bash
git push --force-with-lease origin feature/my-work
Expand Down Expand Up @@ -110,5 +110,6 @@ git range-diff origin/main...feature/my-work@{1} origin/main...feature/my-work
## Next Steps

- [Merge vs Rebase](./merge-vs-rebase): compare two ways to integrate diverged branches
- [Force-with-lease](./force-with-lease): update a rewritten remote branch without clobbering concurrent pushes
- [What are Stacked PRs?](./stacked-prs): see how dependent branches create rebase cascades
- [Stacked PR Workflow](/learn/workflows/git/stacked-pr): maintain a stack in practice
109 changes: 109 additions & 0 deletions web/learn/concepts/git/force-with-lease.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
sidebar_position: 6
---

import DefinitionCard from "@site/src/components/DefinitionCard";

# Force-with-lease

_Refuse a force push when the remote tip has moved past what you expect._

After you rewrite local history with [rebase](./merge-vs-rebase) or amend, a normal push is rejected because the remote tip is not an ancestor of your new tip. A [force push](/docs/how-to/pushing-to-remote) overwrites that remote tip. `--force-with-lease` still overwrites, but only when the remote matches the tip your repository already expects.

## Introduction

`--force` updates a remote branch to your local tip with no check against concurrent work. If a collaborator pushed while you rebased, those commits disappear from the branch.

`--force-with-lease` treats the update as conditional. Git compares the live remote tip to an expected value, usually the tip stored in your remote-tracking branch. Matching hashes allow the update. Diverging hashes reject the push and leave the upstream commits in place.

## Understanding the Concept

A **force push** replaces a remote branch pointer. History rewriting needs that replacement when the rewritten commits no longer descend from the previous remote tip. Unconditional force push ignores whether anyone else moved the branch.

<DefinitionCard
term="Force Push"
definition="A push that updates a remote branch even when the new tip does not fast-forward from the current remote tip."
/>

**Force-with-lease** adds a compare-and-swap check before that replacement. The lease is the expected remote value. Git asks the remote to update the ref only if that expected value still matches the live tip.

<DefinitionCard
term="Force-with-lease"
definition="A force push that updates a remote ref only when the remote tip still equals an expected commit, usually the local remote-tracking branch."
/>

The default expected value comes from your **remote-tracking branch**, such as `refs/remotes/origin/feature`. That ref is a local cache of what `git fetch` last saw on the remote. The lease compares that cached hash to the remote tip at push time.

<DefinitionCard
term="Remote-tracking Branch"
definition="A local ref under refs/remotes/ that records the last-seen tip of a branch on a remote."
/>

| Form | Expected tip | Scope |
| --- | --- | --- |
| `--force-with-lease` | Each destination's remote-tracking branch | Every ref this push updates |
| `--force-with-lease=<refname>` | That ref's remote-tracking branch | Only the named ref |
| `--force-with-lease=<refname>:<expect>` | The commit you name | Only the named ref, ignoring the tracking cache |

The `<refname>:<expect>` form hard-codes the expected hash. Scripts use it when the remote-tracking cache is unreliable or when several jobs might race on the same branch.

## Applying It in Practice

Rewrite a private feature branch, then update the remote only if nobody else pushed:

```bash
git fetch origin
git switch feature/my-work
git rebase origin/main
git push --force-with-lease origin feature/my-work
```

Protect one named branch during a multi-ref push:

```bash
git push --force-with-lease=feature/my-work origin feature/my-work notes/commits
```

In CI, pin the expected tip to a hash you already validated:

```bash
git push --force-with-lease=feature/my-work:a3f9c12 origin feature/my-work
```

If the lease fails, fetch and inspect the remote branch before deciding whether to rebase onto the new tip or recover the other person's commits.

## Engineering Considerations

The default lease trusts your remote-tracking branch. Background `git fetch` from an IDE, terminal helper, or cron job can advance that cache to the live remote tip without merging those commits into your working branch. The lease then passes, and a force push can overwrite work you never integrated locally.

`--force-if-includes` closes that gap when combined with `--force-with-lease`. Before allowing the update, Git checks that the tip of the remote-tracking branch is reachable from one of your local branch's reflog entries. If those remote commits are absent from that history, the push is rejected even when the tip hashes match.

```bash
git push --force-with-lease --force-if-includes origin feature/my-work
```

<DefinitionCard
term="Force-if-includes"
definition="A push option that requires the remote-tracking tip to appear in your local reflog before a force-with-lease update proceeds."
/>

`--force-if-includes` is a no-op with `--force-with-lease=<refname>:<expect>`, because that form already names the expected tip instead of reading the tracking cache. Other mitigations include a push-only remote that never receives fetches into the same tracking refs, or an explicit `<refname>:<expect>` value taken from a known good commit.

Do not use any force push form on protected shared branches such as `main` or release lines. Prefer lease-protected force pushes for branches with a single owner, an agreed rewrite policy, or a [dependent stack](./stacked-prs) whose owner rebases every layer together.

## Scaling and Operations

Treat rewriteable feature branches as owned resources. Document who may rebase them and whether reviewers should base follow-up work on the remote tip.

In automation, prefer `--force-with-lease=<refname>:<expect>` over the parameterless form. A fixed expected hash survives background fetches and concurrent jobs that update tracking refs between validation and push.

For interactive developer defaults, pair parameterless `--force-with-lease` with `--force-if-includes` where your Git version supports it. Branch protection on hosting platforms should still reject force pushes to shared integration branches.

When a lease rejection appears in CI, fail the job and surface the unexpected remote tip. Silent retries with a refreshed expect value recreate the race the lease exists to prevent.

## Next Steps

- [Cherry-pick vs Rebase](./cherry-pick-vs-rebase): distinguish selected patch copying from branch rewriting
- [Pushing to Remote](/docs/how-to/pushing-to-remote): push rewritten branches from the Treq workflow
- [Auto-rebase AI Branches](/learn/how-to/auto-rebase-ai-branches): keep dependent agent branches current without dropping lease protection
- [Stacked PR Workflow](/learn/workflows/git/stacked-pr): force-push rewritten stacks without clobbering dependents
5 changes: 5 additions & 0 deletions web/learn/concepts/git/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ Start with Git's data model, then learn how to manage parallel and dependent bra
href: "/learn/concepts/git/cherry-pick-vs-rebase",
label: "Cherry-pick vs Rebase",
},
{
type: "link",
href: "/learn/concepts/git/force-with-lease",
label: "Force-with-lease",
},
{
type: "link",
href: "/learn/concepts/git/stacked-prs",
Expand Down
3 changes: 2 additions & 1 deletion web/learn/concepts/git/merge-vs-rebase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ git switch feature/my-work
git rebase origin/main
```

If that branch already exists on a remote and its rewrite is agreed, [force push](/docs/how-to/pushing-to-remote) it with a lease:
If that branch already exists on a remote and its rewrite is agreed, update it with [`--force-with-lease`](./force-with-lease):

```bash
git push --force-with-lease origin feature/my-work
Expand Down Expand Up @@ -114,6 +114,7 @@ After a big rebase, use `git range-diff` to check the rewritten series still say
## Next Steps

- [Cherry-pick vs Rebase](./cherry-pick-vs-rebase): distinguish selected patch copying from branch rewriting
- [Force-with-lease](./force-with-lease): update a rewritten remote branch without clobbering concurrent pushes
- [What are Stacked PRs?](./stacked-prs): manage branches that depend on one another
- [Stacked PR Workflow](/learn/workflows/git/stacked-pr): apply rebasing to a pull-request stack
- [Parallel Development Workflow](/learn/workflows/git/parallel-development): combine integration policy with concurrent branch work
2 changes: 1 addition & 1 deletion web/learn/how-to/auto-rebase-ai-branches.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Pending working-copy edits can block or complicate synchronization. Commit, move

**Rebase succeeds, behavior breaks.** A conflict marker resolution kept the wrong side. Re-read conflicted files and add a regression test.

**Force push without lease.** Concurrent updates on the remote branch can be overwritten, so always push with `--force-with-lease`.
**Force push without lease.** Concurrent updates on the remote branch can be overwritten, so always push with [`--force-with-lease`](/learn/concepts/git/force-with-lease).

**Rebasing during an active agent session.** The agent may write files while history rewrites. Stop the agent or wait until it finishes before rebasing that workspace.

Expand Down
2 changes: 1 addition & 1 deletion web/learn/workflows/git/stacked-pr.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Do not use line count as the only split rule. A small change with a difficult co

## Scaling and Operations

Assign a stack owner who coordinates base changes and tells reviewers when force pushes alter a diff. Several developers can contribute, but one person should maintain the chain.
Assign a stack owner who coordinates base changes and tells reviewers when [force pushes](/learn/concepts/git/force-with-lease) alter a diff. Several developers can contribute, but one person should maintain the chain.

Run CI on each layer and on the combined stack where integration risk warrants it. A passing incremental diff does not prove that all layers work together.

Expand Down
Loading