Skip to content

feat: --from <ref> cuts a new branch from a base you name - #649

Open
JSmithRobotics wants to merge 1 commit into
mainfrom
feat/from-ref
Open

JSmithRobotics wants to merge 1 commit into
mainfrom
feat/from-ref

Conversation

@JSmithRobotics

@JSmithRobotics JSmithRobotics commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

What this adds

dl owner/repo@branch cuts a new branch from the repository's default branch. --from <ref> says which base to cut from instead:

dl owner/repo@feature/thing --from release/2.1

The base is fetched fresh before the branch is cut, so --from names a ref as it is on the remote now, not as a stale local clone last saw it.

Behaviour worth stating

  • The base is fetched, then cut from. A base that cannot be resolved after that fetch is refused, and no branch is created. Inventing a branch from whatever happened to be local is the failure this avoids.
  • It only applies when a branch is actually being cut. If the branch already exists, --from does not silently rebase or move it; the existing branch wins and is named as such.
  • It is not stored with the workspace. Cutting a branch is a one-time event, so replaying --from on a later attach would be meaningless at best. from_is_absent_from_what_the_workspace_stores pins that.

Testing

Nine unit tests in workspace_clone.rs cover: cutting from a named base, an unresolvable base being refused without creating a branch, a base fetch that fails being reported while the cut still proceeds from what is there, --from against an already-existing branch naming the branch rather than the base, and the flag's absence from stored workspace state.

--from is added to the README's flag table, to VALUE_FLAGS (pinned against clap's own parser by the_value_flags_are_the_ones_clap_takes_values_for), and to the bash completion's option and value-taking lists.

cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, and the README/citation/prose doc guards all pass.

Summary by Sourcery

Allow users to cut new workspace branches from a named, freshly fetched remote ref with explicit validation and one-time launch semantics.

New Features:

  • Add a --from <ref> launch option that creates new branches from a freshly fetched named remote ref instead of the repository default branch.

Bug Fixes:

  • Refuse unresolved bases without creating a branch and clearly report when --from is used with an existing branch.
  • Prevent --from from being persisted in workspace state or applied to commands that do not cut branches.

Enhancements:

  • Propagate the new launch option through CLI parsing, command dispatch, library APIs, and branch preparation.
  • Update Bash completion, aid argument handling, README documentation, and public API snapshots for the new option.

Documentation:

  • Document --from <ref> usage, applicability, and one-time launch semantics in the README.

Tests:

  • Add coverage for fresh base fetching, unresolved bases, existing branches, and exclusion of the base from stored workspace records.
  • Add CLI grammar and value-flag parser coverage for --from.

@sourcery-ai

sourcery-ai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds a per-launch --from <ref> option that is parsed and completed consistently, propagated only through cold branch-cutting flows, and implemented by freshly fetching the named remote ref before creating a branch; invalid bases and existing target branches are refused, while the option is never persisted.

Sequence diagram for cutting a branch from a named ref

sequenceDiagram
    participant User
    participant CLI
    participant Launch
    participant CloneManager
    participant Remote
    participant Git

    User->>CLI: dl owner/repo@branch --from base
    CLI->>Launch: from_ref(Some(base))
    Launch->>CloneManager: prepare_cold_from(..., branch, base, ...)
    CloneManager->>Remote: fetch_ref(branch)
    Remote-->>CloneManager: branch missing
    CloneManager->>Remote: fetch_ref(base)
    Remote-->>CloneManager: Updated
    CloneManager->>Git: ensure_branch_exists(branch, base)
    Git-->>CloneManager: branch created
    CloneManager-->>Launch: PreparedWorkspace
Loading

State diagram for --from branch-cut validation

stateDiagram-v2
    [*] --> CheckTargetBranch
    CheckTargetBranch --> BranchAlreadyExists: target branch exists
    CheckTargetBranch --> FetchBase: target branch is new
    FetchBase --> BaseNotResolved: fetch fails or ref is missing
    FetchBase --> CutBranch: base fetched successfully
    CutBranch --> WorkspacePrepared
    BranchAlreadyExists --> [*]
    BaseNotResolved --> [*]
    WorkspacePrepared --> [*]
Loading

File-Level Changes

Change Details Files
Thread the per-launch base ref from CLI parsing through launch placement without persisting it.
  • Parse --from <ref> for workspace-opening forms and reject it for global commands.
  • Propagate the value through dispatch and launch APIs into cold workspace preparation.
  • Keep the value out of stored workspace state and ignore it for operations that do not cut branches.
rust/dl/src/cli.rs
rust/dl/src/commands.rs
rust/dl/src/launch.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/workspace_clone.rs
Implement fresh remote-base resolution and guarded branch creation.
  • Fetch the requested base before creating a new branch and use it as the branch start point.
  • Refuse unresolved bases without creating a branch, while reporting fetch failures appropriately.
  • Detect an existing target branch first and refuse with the existing-branch outcome instead of consulting or applying the base.
rust/devlaunch-core/src/flows/workspace_clone.rs
rust/devlaunch-core/src/render.rs
Expose the new value-taking option consistently across documentation, parsing helpers, and shell completion.
  • Document --from semantics and its non-persistent, new-branch-only behavior.
  • Add it to value-option metadata and aid's dl-grammar recognition.
  • Offer it in bash completion as a global/value-taking option that can precede a workspace spec.
README.md
rust/aid/src/rewrite.rs
rust/devlaunch-core/completions/dl.bash
rust/dl/src/cli.rs
Add focused coverage for the new branch-cutting and CLI-state semantics.
  • Test fresh base fetching, branch creation from the named ref, unresolved-base refusal, existing-branch handling, and absence from serialized workspace records.
  • Update affected call-site fixtures and API snapshots.
rust/devlaunch-core/src/flows/workspace_clone.rs
rust/devlaunch-core/src/flows/launch.rs
rust/dl/src/cli.rs
rust/devlaunch-core/public-api.api.txt
rust/devlaunch-core/public-api.rest.txt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="rust/devlaunch-core/src/flows/workspace_clone.rs" line_range="820-830" />
<code_context>
+            });
+        }
+
+        let base = match self.repo_manager.fetch_ref(owner, repo, from_ref, notices) {
+            Ok(FetchOutcome::Updated) => BranchBase::Fresh,
+            Ok(FetchOutcome::RefMissingOnRemote) => {
+                return Err(EnsureBranchError::BaseNotResolved {
+                    base: from_ref.to_owned(),
+                });
+            }
+            Ok(FetchOutcome::Failed { reason }) => {
+                notices.say(CacheNotice::RefNotFetched {
</code_context>
<issue_to_address>
**issue (broader_impact):** A failed fetch of the requested base returns `BaseNotResolved` before `ensure_branch_exists` is called, so `--from` refuses to cut the branch instead of reporting the fetch failure and proceeding from the base already present in the cache. This contradicts the stated behavior and the test coverage described for a failed base fetch.

**Triggers:** When the remote fetch for `from_ref` fails but the requested base already exists locally in the bare cache.

**Suggested fix:** Preserve the cached base on `FetchOutcome::Failed` after emitting `RefNotFetched`, and continue to `ensure_branch_exists`; reserve `BaseNotResolved` for `RefMissingOnRemote` and unsafe/unresolvable refs.

```suggestion
            Ok(FetchOutcome::Failed { reason }) => {
                notices.say(CacheNotice::RefNotFetched {
                    owner: owner.to_owned(),
                    repo: repo.to_owned(),
                    branch: from_ref.to_owned(),
                    reason: NotRefreshed::FetchFailed { reason },
                });
                BranchBase::Stale {
                    base: from_ref.to_owned(),
                    reason: NotRefreshed::FetchFailed { reason },
                }
            }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +820 to +830
Ok(FetchOutcome::Failed { reason }) => {
notices.say(CacheNotice::RefNotFetched {
owner: owner.to_owned(),
repo: repo.to_owned(),
branch: from_ref.to_owned(),
reason: NotRefreshed::FetchFailed { reason },
});
return Err(EnsureBranchError::BaseNotResolved {
base: from_ref.to_owned(),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (broader_impact): A failed fetch of the requested base returns BaseNotResolved before ensure_branch_exists is called, so --from refuses to cut the branch instead of reporting the fetch failure and proceeding from the base already present in the cache. This contradicts the stated behavior and the test coverage described for a failed base fetch.

Triggers: When the remote fetch for from_ref fails but the requested base already exists locally in the bare cache.

Suggested fix: Preserve the cached base on FetchOutcome::Failed after emitting RefNotFetched, and continue to ensure_branch_exists; reserve BaseNotResolved for RefMissingOnRemote and unsafe/unresolvable refs.

Suggested change
Ok(FetchOutcome::Failed { reason }) => {
notices.say(CacheNotice::RefNotFetched {
owner: owner.to_owned(),
repo: repo.to_owned(),
branch: from_ref.to_owned(),
reason: NotRefreshed::FetchFailed { reason },
});
return Err(EnsureBranchError::BaseNotResolved {
base: from_ref.to_owned(),
});
}
Ok(FetchOutcome::Failed { reason }) => {
notices.say(CacheNotice::RefNotFetched {
owner: owner.to_owned(),
repo: repo.to_owned(),
branch: from_ref.to_owned(),
reason: NotRefreshed::FetchFailed { reason },
});
BranchBase::Stale {
base: from_ref.to_owned(),
reason: NotRefreshed::FetchFailed { reason },
}
}

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.92929% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.41%. Comparing base (11d160d) to head (23701e3).

Files with missing lines Patch % Lines
rust/devlaunch-core/src/flows/workspace_clone.rs 92.75% 15 Missing ⚠️
rust/dl/src/commands.rs 90.00% 2 Missing ⚠️
rust/dl/src/lib.rs 0.00% 2 Missing ⚠️
rust/dl/src/render.rs 0.00% 2 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 94.63% <92.92%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 94.63% <92.92%> (-0.03%) ⬇️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

`dl owner/repo@fix/123` creates the branch if it is not there, and what it cut
from was settled with no way to ask for anything else: the default branch's
remote ref, freshly fetched. `--from` replaces the answer
`WorkspaceCloneManager::ensure_branch` gives and nothing else.

Carried forward onto upstream's v0.49.0 rework of that method (312005c,
1a8dc37, 8da1a86) rather than replayed from the old fork's diff: upstream's
current `ensure_branch` already matches the shape the old patch assumed, so
the seam it hooks into (a new `ensure_branch_from` arm, taken before the
no-flag fetch/default-branch dance) needed no rediscovery. `prepare_cold_from`
threads `from` through the lock the same way `prepare_cold` always has, and
`prepare_cold` itself becomes a `None` call of it so the module's forty-odd
existing test call sites stay untouched.

An unresolvable base is a refusal naming the base, and creates no branch:
falling back to the default would be the same "invented base" failure the
no-flag path already forbids, arriving through a new door. A branch that
already exists is a refusal that names the branch, not the base -- existence
is decided before the base is resolved, so ignoring the flag there would look
like it worked while the workspace opens on whatever the branch already is.

Like `--claude-profile` and unlike `--devcontainer`: a base describes an event
that happened once, and a workspace that re-applied one on every later launch
would be claiming to re-cut a branch it did not. `Launch::from_ref` rides one
call and is never read back off a record.

`aid` gained no `--from` of its own, but `DL_VALUE_OPTIONS` had to learn that
dl's `--from` takes a value, or `aid --from develop owner/repo fix it` reads
"develop" as the prompt's first word. `completions/dl.bash` carries the same
fact in three tables, each pinned by `completion_tables`. `render_select`
picks up the same `#[allow(clippy::too_many_arguments)]` its sibling
`render_workspace` already carries, for the trio of independent launch
modifiers.

README's `--from` paragraph is restored (it was pulled earlier for describing
a flag the binary did not yet have), and the public-api snapshots carry the
four new additive rows (`Launch::from_ref` twice, `EnsureBranchError`'s two new
variants), inserted by anchoring on content rather than copying a full
regeneration -- a local nightly renders unrelated toolchain noise
(`core::io::error::Error` vs `std::io::error::Error`, blanket-impl reordering)
that has nothing to do with this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdSFnBdxie6TosHVmjLY28
@JSmithRobotics

Copy link
Copy Markdown
Collaborator Author

Not changing this one — the behaviour is intended, and the doc directly above ensure_branch_from states it:

from_ref cannot be resolved. Only once branch is confirmed new: from_ref is fetched exactly as the default branch would have been, and anything short of FetchOutcome::Updated means this launch cannot prove the base is current, so it is named in the refusal rather than substituted for.

So the review's premise — that this contradicts the stated behaviour and the test coverage — does not hold. BaseNotResolved on FetchOutcome::Failed is what is specified.

The asymmetry with the no---from path is the point rather than an inconsistency. Those two paths are reading the cache for different questions:

  • A launch on an existing branch falls back to the cached tip because the alternative is refusing to open a workspace over a transient network failure. If the tip is stale you can see that it is, and the next fetch fixes it. The staleness is visible and recoverable.
  • Cutting a new branch is a one-time act whose result outlives the decision. A branch cut from a silently stale base is a branch nobody can later identify the base of — the commit it was cut from is simply whatever happened to be cached that morning, and no later fetch undoes it.

--from exists so the operator can name the base. Substituting a different one because the named one could not be fetched answers a question they did not ask, and does it silently.

The refusal names the ref, so the recovery is to fetch and re-run, or to pass a base that is already current.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant