Skip to content

feat: Add branch completion and auto-creation for dl command - #6

Merged
blooop merged 6 commits into
mainfrom
feature/branch-completion-and-creation
Jan 18, 2026
Merged

blooop merged 6 commits into
mainfrom
feature/branch-completion-and-creation

Conversation

@blooop

@blooop blooop commented Jan 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add tab completion for branch names when typing dl owner/repo@<TAB>
  • Auto-create branches that don't exist when running dl owner/repo@newbranch
  • Fix workspace ID derivation to match devpod's behavior (uses branch name, not repo name)

Changes

Branch Completion

  • Branches are cached in ~/.cache/dl/completions.bash for instant completion
  • Uses git ls-remote to fetch branches (no gh CLI dependency)
  • Cache refreshes automatically after workspace operations or via dl --update-cache

Branch Auto-Creation

  • When specifying a non-existent branch, it's automatically created from the default branch
  • Uses standard git commands (git ls-remote + git push)
  • Only requires normal git push access to the repository

Workspace ID Fix

  • Fixed bug where SSH would connect to wrong workspace when using branches
  • Devpod uses the branch name (sanitized) as workspace ID, not the repo name
  • Example: owner/repo@feature/my-branch → workspace ID feature-my-branch

Test plan

  • All 138 tests pass
  • Test dl owner/repo@existingbranch opens correct workspace
  • Test dl owner/repo@newbranch creates branch and opens workspace
  • Test tab completion shows branches after @

🤖 Generated with Claude Code

Summary by Sourcery

Add remote branch awareness to the dl CLI, including branch-based workspace IDs, branch auto-creation, and enhanced shell completion using a cached list of branches.

New Features:

  • Support parsing owner/repo@branch specs and deriving workspace IDs from sanitized branch names for git-based workspaces.
  • Automatically ensure remote branches exist (creating them from the default branch if needed) when launching workspaces from owner/repo@branch specs.
  • Extend bash completion to suggest owner/repo@branch combinations using a cached list of remote branches per repository.

Enhancements:

  • Populate the completion cache with discovered remote branches for all known repositories and expose them in both JSON and bash cache formats.

Tests:

  • Add comprehensive unit tests for parsing owner/repo@branch specs, remote branch existence/creation helpers, completion cache updates with branches, and new main-flow behaviors around branch handling.

This change adds several features for working with git branches:

Branch completion:
- Tab completion now suggests branches for known repos (e.g., dl owner/repo@<TAB>)
- Branches are cached locally for instant completion (no network delay)
- Cache is updated via `dl --update-cache` or automatically after workspace ops

Branch auto-creation:
- When specifying a non-existent branch (dl owner/repo@newbranch), it's created
- Uses `git ls-remote` and `git push` - no gh CLI dependency required
- Only requires standard git push access to the repository

Workspace ID fix:
- Fixed workspace ID derivation to match devpod's behavior
- With branch: workspace ID = sanitized branch name (lowercase, / -> -)
- Without branch: workspace ID = repo name
- This fixes the bug where SSH would connect to wrong workspace

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jan 17, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implements branch-aware behavior for the dl CLI by adding remote branch discovery and caching for shell completion, introducing auto-creation/validation of remote branches when using owner/repo@branch specs, and aligning workspace ID derivation with devpod’s branch-based semantics, with corresponding test coverage added for all new behaviors.

Sequence diagram for dl owner/repo@branch execution with auto-creation

sequenceDiagram
    actor User
    participant dl_main as dl_main
    participant parser as parse_owner_repo_branch
    participant ensure as ensure_remote_branch
    participant exists as remote_branch_exists
    participant create_branch as create_remote_branch
    participant head as get_remote_head_sha
    participant git as git_cli
    participant devpod as devpod_workspace

    User->>dl_main: invoke dl owner/repo@feature/my-branch
    dl_main->>parser: parse_owner_repo_branch(raw_spec)
    parser-->>dl_main: (owner/repo, feature/my-branch)
    dl_main->>ensure: ensure_remote_branch(owner/repo, feature/my-branch)
    ensure->>exists: remote_branch_exists(owner_repo, branch)
    exists->>git: git ls-remote --heads https://github.com/owner/repo branch
    git-->>exists: refs/heads/branch or empty
    alt branch exists
        exists-->>ensure: True
        ensure-->>dl_main: True
    else branch does not exist
        exists-->>ensure: False
        ensure->>create_branch: create_remote_branch(owner_repo, branch)
        create_branch->>head: get_remote_head_sha(owner_repo)
        head->>git: git ls-remote https://github.com/owner/repo HEAD
        git-->>head: sha HEAD
        head-->>create_branch: sha
        create_branch->>git: git push https://github.com/owner/repo sha:refs/heads/branch
        git-->>create_branch: success or failure
        create_branch-->>ensure: True or False
        ensure-->>dl_main: True or False
    end
    alt branch ensured
        dl_main->>dl_main: workspace_id = spec_to_workspace_id(raw_spec)
        dl_main->>devpod: open workspace workspace_id
        devpod-->>User: workspace for branch ready
    else branch not ensured
        dl_main-->>User: exit code 1
    end
Loading

Sequence diagram for bash completion of dl owner/repo@branch

sequenceDiagram
    actor User
    participant Bash as bash_completion
    participant dl as dl_completion_cache_updater
    participant cache as bash_cache_file
    participant git as git_cli

    User->>Bash: type dl and press TAB
    Bash->>cache: source completions.bash
    cache-->>Bash: DL_WORKSPACES, DL_REPOS, DL_OWNERS, DL_BRANCHES
    Bash-->>User: initial completions

    User->>Bash: type dl owner/repo@ and press TAB
    Bash->>cache: read DL_BRANCHES (owner/repo@branch entries)
    alt DL_BRANCHES populated
        Bash-->>User: branch name completions for owner/repo@
    else DL_BRANCHES empty
        Bash-->>User: no branch completions
    end

    User->>dl: run dl --update-cache
    dl->>dl: update_completion_cache()
    dl->>git: git ls-remote --heads https://github.com/owner/repo for each repo
    git-->>dl: refs/heads/branch list
    dl->>cache: write_completion_cache (branches list)
    dl->>cache: write_bash_completion_cache (DL_BRANCHES)
    cache-->>dl: updated file

    User->>Bash: type dl owner/repo@ and press TAB
    Bash->>cache: source updated completions.bash
    cache-->>Bash: DL_BRANCHES with branches
    Bash-->>User: branch completions owner/repo@branch
Loading

File-Level Changes

Change Details Files
Add remote branch discovery and integrate branches into completion cache and bash completions.
  • Introduce get_remote_branches to query remote GitHub branches via git ls-remote with timeout and error handling.
  • Extend update_completion_cache to collect branches for all known repos, store them as owner/repo@branch strings, and include them in both JSON and bash cache structures.
  • Update write_bash_completion_cache and dl.bash completion script to expose DL_BRANCHES and complete owner/repo@branch values when typing after @, while preserving owner/repo and workspace completions.
devlaunch/dl.py
devlaunch/completions/dl.bash
test/test_dl.py
Add remote branch existence/creation helpers and wire them into dl main flow for owner/repo@branch specs.
  • Implement remote_branch_exists, get_remote_head_sha, create_remote_branch, and ensure_remote_branch using git ls-remote and git push with logging and error handling.
  • Add parse_owner_repo_branch to safely parse owner/repo[@Branch] vs paths/URLs/workspace names.
  • Update main to, for new git specs with a branch, ensure the remote branch exists (creating from default HEAD if necessary) before bringing up the workspace, and to fail fast if this step fails.
  • Add tests covering success/failure paths for branch existence checks, HEAD lookup, branch creation, ensure_remote_branch logic, and main() behavior for existing vs new workspaces and various branch specs.
devlaunch/dl.py
test/test_dl.py
Align workspace ID derivation with devpod’s branch-based behavior.
  • Introduce sanitize_workspace_id to lower-case and replace slashes with dashes to match devpod workspace IDs.
  • Modify spec_to_workspace_id so git specs with @Branch use the sanitized branch name as the workspace ID, while git specs without a branch still use the repo name and path specs use the directory name.
  • Extend tests to verify workspace IDs for simple owner/repo, owner/repo@branch, feature branches with slashes, and mixed-case branch names.
devlaunch/dl.py
test/test_dl.py
Extend completion cache and roundtrip tests to cover branches.
  • Augment cache read/write tests to include a branches list and assert roundtrip fidelity.
  • Add tests for bash completion cache generation that verify the DL_BRANCHES variable is rendered correctly with owner/repo@branch entries.
  • Add tests for update_completion_cache to validate happy-path branch aggregation, as well as handling of branch-fetch failures returning an empty branches list.
devlaunch/dl.py
test/test_dl.py

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 3 issues, and left some high level feedback:

  • The new parse_owner_repo_branch logic duplicates some of the existing owner/repo parsing rules; consider refactoring it to reuse is_git_spec/OWNER_REPO_PATTERN or a shared helper so that any future changes to what constitutes a valid repo spec stay consistent in one place.
  • The remote operations (get_remote_branches, remote_branch_exists, create_remote_branch) all hard-code https://github.com/{owner_repo} as the remote, which may break for users who only have SSH auth set up; it might be more robust to derive the remote URL from an existing workspace/repo configuration or at least prefer git@github.com: when available.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `parse_owner_repo_branch` logic duplicates some of the existing owner/repo parsing rules; consider refactoring it to reuse `is_git_spec`/`OWNER_REPO_PATTERN` or a shared helper so that any future changes to what constitutes a valid repo spec stay consistent in one place.
- The remote operations (`get_remote_branches`, `remote_branch_exists`, `create_remote_branch`) all hard-code `https://github.com/{owner_repo}` as the remote, which may break for users who only have SSH auth set up; it might be more robust to derive the remote URL from an existing workspace/repo configuration or at least prefer `git@github.com:` when available.

## Individual Comments

### Comment 1
<location> `devlaunch/dl.py:391-400` </location>
<code_context>
     return []


+def remote_branch_exists(owner_repo: str, branch: str) -> bool:
+    """Check if a branch exists on a remote GitHub repository."""
+    url = f"/{owner_repo}"
+    try:
+        result = subprocess.run(
+            ["git", "ls-remote", "--heads", url, branch],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+        return result.returncode == 0 and result.stdout.strip() != ""
+    except (OSError, subprocess.SubprocessError):
+        return False
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Network subprocesses for remote branch checks lack timeouts and may hang

Both `remote_branch_exists()` and `get_remote_head_sha()` invoke `git ls-remote` without a timeout, so a slow/unreachable remote or credential prompt can hang the main process. Please add a `timeout` to these `subprocess.run` calls and handle `TimeoutExpired` in line with the existing subprocess helpers.
</issue_to_address>

### Comment 2
<location> `test/test_dl.py:414-423` </location>
<code_context>
+class TestParseOwnerRepoBranch:
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for specs with `@` that are paths or URLs with branches

Since `parse_owner_repo_branch` distinguishes owner/repo+branch specs from paths/URLs, please add cases like `./my-project@foo` and `https://github.com/owner/repo@main`. That will lock in the behavior that these are treated as paths/URLs (not owner/repo+branch) and prevent regressions where `ensure_remote_branch` might run on unintended inputs.
</issue_to_address>

### Comment 3
<location> `devlaunch/dl.py:109` </location>
<code_context>
         pass


+def get_remote_branches(owner_repo: str) -> List[str]:
+    """Get list of branches from a remote GitHub repository."""
+    url = f"/{owner_repo}"
</code_context>

<issue_to_address>
**issue (complexity):** Consider consolidating the repeated git ls-remote logic and simplifying owner/repo@branch parsing into focused helpers to reduce cognitive load and duplication.

You can reduce a fair bit of cognitive load by consolidating the `git ls-remote` helpers and tightening the owner/repo@branch parsing.

### 1. Consolidate `git ls-remote` helpers

Right now you have three separate functions that all:

- Build `https://github.com/{owner_repo}`
- Call `git ls-remote` with slightly different args
- Parse similar output formats

You can centralize the subprocess + parsing and keep the existing behavior in thin wrappers:

```python
def _git_ls_remote(owner_repo: str, *args: str) -> Optional[str]:
    """Run git ls-remote for a single ref and return stdout or None on error."""
    url = f"/{owner_repo}"
    try:
        result = subprocess.run(
            ["git", "ls-remote", url, *args],
            capture_output=True,
            text=True,
            check=False,
            timeout=5,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout
    except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
        return None
    return None
```

Then your specific helpers become very small and easier to reason about:

```python
def get_remote_branches(owner_repo: str) -> List[str]:
    output = _git_ls_remote(owner_repo, "--heads")
    if not output:
        return []
    branches: List[str] = []
    for line in output.strip().splitlines():
        if "refs/heads/" in line:
            branches.append(line.split("refs/heads/")[-1])
    return branches


def remote_branch_exists(owner_repo: str, branch: str) -> bool:
    output = _git_ls_remote(owner_repo, "--heads", branch)
    return bool(output)


def get_remote_head_sha(owner_repo: str) -> Optional[str]:
    output = _git_ls_remote(owner_repo, "HEAD")
    if not output:
        return None
    # "<sha>\tHEAD"
    return output.strip().split()[0]
```

This keeps all current semantics (including timeouts) but removes duplicated URL construction, subprocess invocation, and error handling.

### 2. Simplify `parse_owner_repo_branch`

`parse_owner_repo_branch` currently rechecks a lot of conditions already implied by your pattern and other helpers. You can keep the same external behavior with a more direct implementation:

```python
def parse_owner_repo_branch(spec: str) -> Optional[tuple[str, Optional[str]]]:
    """Parse owner/repo[@branch] spec into (owner_repo, branch) or None."""
    # Skip obvious non-git specs
    if is_path_spec(spec):
        return None
    if "://" in spec or spec.startswith(("github.com/", "gitlab.com/")):
        return None

    if not OWNER_REPO_PATTERN.match(spec):
        return None

    owner_repo, _, branch = spec.partition("@")
    return owner_repo, branch or None
```

That keeps validation in one place and avoids multiple `if` branches for essentially the same check.

If you want to make the `main()` flow more self-documenting, you can also wrap the entrypoint behavior in a single helper:

```python
def ensure_remote_branch_for_spec(raw_spec: str) -> bool:
    parsed = parse_owner_repo_branch(raw_spec)
    if not parsed:
        return True
    owner_repo, branch = parsed
    if not branch:
        return True
    return ensure_remote_branch(owner_repo, branch)
```

and in `main()`:

```python
if not ensure_remote_branch_for_spec(raw_spec):
    return 1
```

This keeps the new behavior but flattens the call chain that `main()` needs to understand.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread devlaunch/dl.py Outdated
Comment thread devlaunch/dl.py
blooop and others added 3 commits January 17, 2026 18:09
- Add shared _git_ls_remote() helper with 5s timeout to consolidate
  duplicate subprocess logic and prevent hanging on slow remotes
- Refactor remote_branch_exists() and get_remote_head_sha() to use
  the new helper (fixes timeout issue)
- Add tests for path/URL specs containing @ to ensure they're not
  mistakenly treated as owner/repo@branch specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jan 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.20%. Comparing base (a26b942) to head (f590fd4).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
devlaunch/dl.py 93.33% 8 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main       #6      +/-   ##
==========================================
+ Coverage   65.40%   73.20%   +7.80%     
==========================================
  Files           3        3              
  Lines         422      530     +108     
==========================================
+ Hits          276      388     +112     
+ Misses        146      142       -4     
Files with missing lines Coverage Δ
devlaunch/dl.py 80.91% <93.33%> (+6.97%) ⬆️

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

blooop and others added 2 commits January 18, 2026 15:44
- Use SSH URLs (git@github.com:) for all git operations instead of HTTPS
  to avoid credential prompts
- Use persistent cache directory (~/.cache/dl/git) for git operations
- Pass --id flag to devpod to allow multiple branches of the same repo
  to be open simultaneously (each branch gets its own workspace ID)
- Fix workspace ID derivation to match devpod's actual behavior
- Add comprehensive tests for new functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@blooop
blooop merged commit ea9a2c0 into main Jan 18, 2026
7 checks passed
@blooop
blooop deleted the feature/branch-completion-and-creation branch January 18, 2026 15:47
@blooop blooop mentioned this pull request Jan 18, 2026
2 tasks
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