Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# APP-4218: Git operations dialogs compare against the branch's actual parent
|
||||
## Summary
|
||||
The Push and Create PR dialogs must scope their "Included commits" / "Changes" previews to the commits and changes on top of the current branch's *actual* parent branch — the branch it was created off of. Today the dialogs fall back to comparing against the repo's main branch when there's no upstream, so a branch-off-a-branch surfaces every inherited commit as if it were new work.
|
||||
## Problem
|
||||
When a user creates `feature-b` off `feature-a` (no upstream yet) and opens the Push / Publish dialog, the "Included commits" list shows every commit between `main` and `HEAD` — including all commits inherited from `feature-a`. The same thing happens in the Create PR dialog's "Changes" list, its AI-generated title/description input, and the resulting `gh pr create` command (which targets the repo default branch). The user only wants to publish / open a PR for the work they actually did on `feature-b`.
|
||||
The correct base is a property of the branch itself (what it was branched from), not a user choice. The dialogs should detect that base from the repo and use it directly.
|
||||
## Goals
|
||||
- The Push / Publish and Create PR dialogs compare against the current branch's actual parent branch, not unconditionally against main.
|
||||
- Parent-branch detection is automatic, simple, and independent of the code review pane's diff-mode selector.
|
||||
- The Create PR dialog targets the detected parent via `gh pr create --base <parent>`.
|
||||
- Fall back to the repo's main branch when detection can't produce a parent, preserving today's common-path behavior.
|
||||
## Non-goals
|
||||
- Letting the user override the detected parent (follow-up if needed).
|
||||
- Reflecting the detected parent anywhere outside the Push and Create PR dialogs.
|
||||
- Changing the Commit dialog's behavior beyond its `Commit and create PR` chain.
|
||||
## Figma
|
||||
Figma: none provided (text/list-only change inside existing dialog chrome).
|
||||
## Behavior
|
||||
1. **Parent branch.** The current branch's parent is the local or remote-tracking branch whose tip is an ancestor of `HEAD` and is *closest* to `HEAD` — i.e. `git merge-base --is-ancestor <candidate> HEAD` succeeds and `git rev-list --count <candidate>..HEAD` is smallest among the candidates. The current branch is excluded from the candidate pool. If no candidate qualifies, the parent is the repo's detected main branch.
|
||||
2. **Push / Publish dialog — included commits.** The "Included commits" list shows the commits in `<parent>..HEAD`. When there are no commits ahead of the parent, the list is empty and the primary Publish button is disabled.
|
||||
3. **Create PR dialog — changes list.** The "Changes" list shows the files and per-file stats from `<parent>..<end_ref>`, where `<end_ref>` is `origin/<current_branch>` if that remote ref exists, otherwise `HEAD` — same end-ref pattern as today.
|
||||
4. **Create PR dialog — AI inputs.** The diff fed into AI title / description generation and the branch commit-message list also use `<parent>..<end_ref>`.
|
||||
5. **Create PR dialog — PR target.** `gh pr create` is invoked with `--base <parent>` so the PR targets the parent branch. If the parent ref is a remote-tracking ref (e.g. `origin/feature-a`), the `origin/` prefix is stripped before passing to `gh`. Applies to both the AI-generated-content path and the `--fill` fallback.
|
||||
6. **Detection timing.** Parent detection runs whenever the dialog's helpers need it; results are not cached long-term. Rebases and new branches are picked up on the next open.
|
||||
7. **Fallback errors.** If detection fails outright (not a git repo, git call errors) the dialogs use the main branch. If `gh pr create --base <parent>` is rejected by GitHub (e.g. the parent isn't pushed to origin), the user sees the existing generic "Git operation failed." toast; the dialog does not silently retry without `--base`.
|
||||
8. **Commit dialog.** The Commit dialog itself is unchanged. Its `Commit and create PR` chain uses the detected parent for the final PR step (same rule as §5).
|
||||
9. **Independence from the diff-mode dropdown.** The pane's diff-mode dropdown is unrelated to detection. Changing it doesn't change the dialogs' previews, and detection doesn't change the dropdown.
|
||||
## Success criteria
|
||||
1. On `feature-b` branched from `feature-a` (no upstream on either), the Publish dialog shows only the commits added on `feature-b`, and the Create PR dialog's Changes list shows only files changed on `feature-b`.
|
||||
2. Confirming Create PR in that setup runs `gh pr create --base feature-a …` and the resulting PR targets `feature-a`.
|
||||
3. On a fresh branch off `main` (no upstream), the Publish dialog shows `main..HEAD` — same commits as today.
|
||||
4. After rebasing `feature-b` onto `main`, the next dialog open detects `main` as the parent and shows the rebased range.
|
||||
5. If no branch is an ancestor of `HEAD`, detection falls back to `main` — no error.
|
||||
6. The Commit dialog's `Commit and create PR` chain targets the detected parent.
|
||||
7. The pane's diff-mode dropdown and the dialogs' previews are independent: changing one does not change the other.
|
||||
@@ -0,0 +1,76 @@
|
||||
# APP-4218: Git operations dialogs compare against the branch's actual parent — Tech Spec
|
||||
Product spec: `specs/APP-4218/PRODUCT.md`
|
||||
## Context
|
||||
Today the Push / Publish dialog, the Create PR dialog, and their AI helpers hard-code the repo's main branch as the comparison base whenever the current branch has no upstream. On a branch-off-a-branch, every commit inherited from the parent branch shows up as "included" in the push / PR.
|
||||
All of the offending code paths already route their base through one function call: `detect_main_branch`. The fix is to introduce a `detect_parent_branch` helper that returns the closest-ancestor branch (falling back to main), and swap the four callers that currently say `detect_main_branch` to say `detect_parent_branch`. No function signatures change.
|
||||
Relevant code:
|
||||
- `app/src/util/git.rs:316-359` — `get_unpushed_commits`: `git log @{u}..HEAD` with `main..HEAD` fallback.
|
||||
- `app/src/util/git.rs:602-634` — `get_branch_diff_entries`: `main..<end>`.
|
||||
- `app/src/util/git.rs:743-766` — `get_diff_for_pr`: `main..<end>`, feeds AI.
|
||||
- `app/src/util/git.rs:775-784` — `get_branch_commit_messages`: `main..HEAD`, feeds AI.
|
||||
- `app/src/util/git.rs:803-826` — `create_pr`: invokes `gh pr create` without `--base`.
|
||||
- `app/src/code_review/git_dialog/{push,pr}.rs` — per-dialog state, unchanged in shape. The detected parent is used transparently through the four util helpers.
|
||||
## Proposed changes
|
||||
### 1. `detect_parent_branch`
|
||||
```rust path=null start=null
|
||||
// app/src/util/git.rs
|
||||
/// Returns the closest-ancestor branch of `HEAD`, or the main branch when
|
||||
/// no candidate qualifies. Ties prefer the detected main branch, then local
|
||||
/// over `origin/*`, then alphabetical for determinism.
|
||||
pub async fn detect_parent_branch(repo_path: &Path) -> Result<String>;
|
||||
```
|
||||
Implementation (all upfront queries run in parallel via `futures::join!`):
|
||||
1. `git for-each-ref --merged HEAD --format='%(objectname) %(refname:short)' refs/heads refs/remotes` to list ancestor refs with their commit SHAs. `--merged HEAD` filters out non-ancestors at the git level, avoiding per-candidate subprocess spawns.
|
||||
2. `git log HEAD --format=%H` to walk HEAD's history once. A `HashMap<&str, usize>` of `sha → position` gives each candidate's distance from HEAD in O(1) lookups.
|
||||
3. Resolve the actual upstream via `git rev-parse --abbrev-ref --symbolic-full-name @{u}` and exclude it (plus the current branch name) from candidates. Handles non-`origin` upstream configurations.
|
||||
4. Rank candidates by `(distance, !is_main, !is_local, name)`. Log the winner at debug.
|
||||
5. If no candidate qualified, return `detect_main_branch(repo_path)`.
|
||||
Return type is a plain `String` — either a local branch (`feature-a`) or a remote-tracking ref (`origin/feature-a`).
|
||||
### 2. Swap `detect_main_branch` for `detect_parent_branch` inside the four helpers
|
||||
No caller / signature changes. Each helper keeps its current shape; only the internal base-branch lookup changes:
|
||||
- `get_unpushed_commits`: the no-upstream fallback branch becomes `detect_parent_branch` instead of `detect_main_branch`. The primary `@{u}..HEAD` path is unchanged (when an upstream exists, it's still the most accurate "what will be pushed"). When upstream is unset, the fallback now uses the closest ancestor.
|
||||
- `get_branch_diff_entries`: `let base = detect_parent_branch(repo_path).await?;` in place of the current `detect_main_branch` call. The `{base}..{end_ref}` range logic is unchanged.
|
||||
- `get_diff_for_pr`: same one-line swap.
|
||||
- `get_branch_commit_messages`: same one-line swap.
|
||||
### 3. `create_pr` passes `--base`
|
||||
`create_pr` internally calls `detect_parent_branch`, strips any `origin/` prefix, and passes `--base <parent>` to `gh pr create`. Signature unchanged:
|
||||
```rust path=null start=null
|
||||
pub async fn create_pr(
|
||||
repo_path: &Path,
|
||||
title: Option<&str>,
|
||||
body: Option<&str>,
|
||||
) -> Result<PrInfo> {
|
||||
let base = detect_parent_branch(repo_path).await?;
|
||||
let base = base.strip_prefix("origin/").unwrap_or(&base).to_string();
|
||||
// ...existing gh pr create invocation, plus --base <base>...
|
||||
}
|
||||
```
|
||||
If detection errors, propagate the error — the caller's existing `user_facing_git_error` path shows the generic failure toast.
|
||||
### 4. Dialogs
|
||||
No dialog-level plumbing. The four util helpers (`get_unpushed_commits`, `get_branch_diff_entries`, `get_diff_for_pr`, `get_branch_commit_messages`) already feed the Push and Create-PR dialogs; swapping them to `detect_parent_branch` internally is enough. The Commit dialog's `CommitAndCreatePr` chain inherits the fix via `create_pr` (§3).
|
||||
Surfacing the detected parent in the dialog chrome (e.g. a "Based on" row) was evaluated and dropped for now — it added visible latency waiting on detection to resolve, with limited user value. Tracked as a follow-up.
|
||||
### 5. Feature flag gating
|
||||
All changes live under `FeatureFlag::GitOperationsInCodeReview` (already gating the dialogs); no new flag.
|
||||
## Risks and mitigations
|
||||
### Heuristic picks the wrong branch
|
||||
Two branches pointing at the same commit, deleted historical parents, etc. The parent isn't visible in the UI right now, so bad detections only manifest as a wrong commit list / wrong PR base. A follow-up can surface the parent or add a per-branch override.
|
||||
### Cost of repeated detection
|
||||
`detect_parent_branch` runs inside each of the four helpers on every dialog open (and once more in `create_pr`). Each detection is 2–4 parallel subprocess calls (`for-each-ref --merged HEAD`, `log HEAD --format=%H`, `rev-parse @{u}`, `detect_main_branch`) regardless of branch count — typically sub-100ms even in repos with thousands of remote-tracking refs. For the common PR-create flow, that's ~4× the cost on top of the AI call, which is already the dominant latency. Acceptable. If large-repo latency becomes measurable, cache per-repo inside `detect_parent_branch` itself (follow-up).
|
||||
### PR targets an unpushed base
|
||||
If the parent is a local-only branch, `gh pr create --base <b>` fails. Surfaces via the generic "Git operation failed." toast; we do not silently retry without `--base`.
|
||||
### Backwards compatibility
|
||||
Fresh-feature-off-main shapes resolve to `main`, so today's behavior is preserved on the common path.
|
||||
## Testing and validation
|
||||
References below are to `specs/APP-4218/PRODUCT.md` success criteria.
|
||||
### Manual validation
|
||||
- `feature-a` (pushed), `git checkout -b feature-b feature-a`, 1 new commit, no push: Publish dialog shows 1 commit (SC 1). Create PR shows only those files; confirming runs `gh pr create --base feature-a` and the PR targets `feature-a` (SC 2).
|
||||
- Fresh branch off main, no upstream: dialog shows `main..HEAD` (SC 3).
|
||||
- Rebase `feature-b` onto `main`, reopen dialog: commits list reflects the rebased range (SC 4).
|
||||
- Commit-and-create-PR on `feature-b`: PR targets `feature-a` (SC 6).
|
||||
- Change the pane's diff-mode dropdown; reopen dialogs: previews are unchanged (SC 7).
|
||||
### Integration / screenshot coverage
|
||||
None added. The `git_dialog` module ships without an integration harness today (see `specs/APP-4125/TECH.md`).
|
||||
## Follow-ups
|
||||
- **Surface the detected parent in the dialog chrome** (a "Based on" row) once we have a way to populate it without visible latency — e.g. caching it on `DiffMetadata` so it's ready by the time the dialog opens.
|
||||
- **Per-branch override** for mis-detected parents (stored in `.git/config` as `branch.<name>.warpParent`).
|
||||
- **Per-repo caching** inside `detect_parent_branch` if repeated detection shows up in profiles.
|
||||
Reference in New Issue
Block a user