Repository navigation
feat: Add branch completion and auto-creation for dl command - #6
Merged
Merged
Conversation
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>
Reviewer's GuideImplements 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-creationsequenceDiagram
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
Sequence diagram for bash completion of dl owner/repo@branchsequenceDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The new
parse_owner_repo_branchlogic duplicates some of the existing owner/repo parsing rules; consider refactoring it to reuseis_git_spec/OWNER_REPO_PATTERNor 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-codehttps://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 prefergit@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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- 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 Report❌ Patch coverage is
Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dl owner/repo@<TAB>dl owner/repo@newbranchChanges
Branch Completion
~/.cache/dl/completions.bashfor instant completiongit ls-remoteto fetch branches (noghCLI dependency)dl --update-cacheBranch Auto-Creation
git ls-remote+git push)Workspace ID Fix
owner/repo@feature/my-branch→ workspace IDfeature-my-branchTest plan
dl owner/repo@existingbranchopens correct workspacedl owner/repo@newbranchcreates branch and opens workspace@🤖 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:
Enhancements:
Tests: