no spaces in worktrees or dirs
This commit is contained in:
parent
88eb5122ab
commit
5c5d6a6bc2
1 changed files with 81 additions and 23 deletions
104
workspaces.py
104
workspaces.py
|
|
@ -252,6 +252,49 @@ def write_workspace_readme(ws_dir: Path, name: str, description: str) -> None:
|
|||
(ws_dir / "readme.md").write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
# --- name/branch helpers ----------------------------------------------------
|
||||
|
||||
|
||||
def slugify_workspace_name(name: str) -> str:
|
||||
"""
|
||||
Turn arbitrary workspace name into a safe directory/worktree name.
|
||||
|
||||
- Lowercase
|
||||
- Replace ':' with '-' FIRST (special case so 'fix:my issue' -> 'fix/my-issue' branch later)
|
||||
- Replace spaces with '-'
|
||||
- Replace any char not [a-z0-9._/-] with '-'
|
||||
- Collapse multiple '-' and strip leading/trailing '-' and '/'
|
||||
"""
|
||||
name = name.replace(":", "-")
|
||||
name = name.replace(" ", "-")
|
||||
safe = []
|
||||
for ch in name:
|
||||
if re.match(r"[a-zA-Z0-9._/-]", ch):
|
||||
safe.append(ch.lower())
|
||||
else:
|
||||
safe.append("-")
|
||||
s = "".join(safe)
|
||||
s = re.sub(r"-+", "-", s)
|
||||
s = s.strip("-/")
|
||||
return s or "workspace"
|
||||
|
||||
|
||||
def branch_name_for_workspace(name: str) -> str:
|
||||
"""
|
||||
Compute branch name from workspace name.
|
||||
|
||||
Rules:
|
||||
- Start from slugified name (no spaces/specials).
|
||||
- Replace *first* '-' with '/' so:
|
||||
'fix-my-issue' -> 'fix/my-issue'
|
||||
(original 'fix:my issue' -> slug 'fix-my-issue' -> branch 'fix/my-issue')
|
||||
"""
|
||||
base = slugify_workspace_name(name)
|
||||
if "-" in base:
|
||||
return base.replace("-", "/", 1)
|
||||
return base
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands / main callback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -312,7 +355,8 @@ def list_workspaces(
|
|||
workspaces_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
table = Table(title=f"Workspaces ({workspaces_dir})")
|
||||
table.add_column("Workspace", style="bold")
|
||||
table.add_column("Workspace (dir)", style="bold")
|
||||
table.add_column("Title", overflow="fold")
|
||||
table.add_column("Description", overflow="fold")
|
||||
table.add_column("Repos", overflow="fold")
|
||||
|
||||
|
|
@ -321,7 +365,7 @@ def list_workspaces(
|
|||
raise typer.Exit(0)
|
||||
|
||||
for ws in sorted(p for p in workspaces_dir.iterdir() if p.is_dir()):
|
||||
name, desc = read_workspace_readme(ws)
|
||||
title, desc = read_workspace_readme(ws)
|
||||
repos: List[str] = []
|
||||
for child in sorted(p for p in ws.iterdir() if p.is_dir()):
|
||||
if child.name.lower() == "readme.md":
|
||||
|
|
@ -333,7 +377,7 @@ def list_workspaces(
|
|||
indicator = status.indicator
|
||||
repos.append(f"{child.name} [{branch}] {indicator}")
|
||||
repos_str = "\n".join(repos) if repos else "-"
|
||||
table.add_row(ws.name, desc or "-", repos_str)
|
||||
table.add_row(ws.name, title or "-", desc or "-", repos_str)
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
|
@ -342,11 +386,13 @@ def list_workspaces(
|
|||
|
||||
|
||||
@app.command("create")
|
||||
@app.command("new", hidden=True)
|
||||
def create_workspace(
|
||||
ctx: typer.Context,
|
||||
name: Optional[str] = typer.Option(
|
||||
None, "--name", "-n", help="Name of the new workspace (directory name)."
|
||||
None,
|
||||
"--name",
|
||||
"-n",
|
||||
help="Name of the new workspace (display name; can contain spaces).",
|
||||
),
|
||||
description: Optional[str] = typer.Option(
|
||||
None,
|
||||
|
|
@ -359,25 +405,28 @@ def create_workspace(
|
|||
Create a new workspace.
|
||||
|
||||
- Asks for name and description if not provided.
|
||||
- Creates directory under workspaces_dir.
|
||||
- Creates README with format '# <name>\\n\\n<description>'.
|
||||
- Workspace directory uses a slugified version of the name.
|
||||
- README uses the original name as '# <name>\\n\\n<description>'.
|
||||
"""
|
||||
_settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx)
|
||||
workspaces_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not name:
|
||||
name = Prompt.ask("Workspace name")
|
||||
name = Prompt.ask("Workspace name (can contain spaces)")
|
||||
|
||||
if not description:
|
||||
description = Prompt.ask("Workspace description", default="")
|
||||
|
||||
ws_dir = workspaces_dir / name
|
||||
dir_name = slugify_workspace_name(name)
|
||||
ws_dir = workspaces_dir / dir_name
|
||||
if ws_dir.exists():
|
||||
console.print(f"[red]Workspace '{name}' already exists at {ws_dir}[/red]")
|
||||
console.print(f"[red]Workspace dir '{dir_name}' already exists at {ws_dir}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
write_workspace_readme(ws_dir, name, description)
|
||||
console.print(f"[green]Created workspace[/green] {ws_dir}")
|
||||
console.print(
|
||||
f"[green]Created workspace[/green] title='{name}' dir='{ws_dir.name}' at {ws_dir}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------- list-repos ----------------
|
||||
|
|
@ -391,7 +440,7 @@ def list_repos(
|
|||
"--workspace",
|
||||
"-w",
|
||||
help=(
|
||||
"Workspace name to inspect. "
|
||||
"Workspace directory name to inspect. "
|
||||
"If omitted, uses the workspace containing the current directory."
|
||||
),
|
||||
),
|
||||
|
|
@ -411,7 +460,8 @@ def list_repos(
|
|||
console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
table = Table(title=f"Repos in workspace '{ws_dir.name}'")
|
||||
title, _desc = read_workspace_readme(ws_dir)
|
||||
table = Table(title=f"Repos in workspace '{title}' (dir: {ws_dir.name})")
|
||||
table.add_column("Repo (dir)", style="bold")
|
||||
table.add_column("Branch")
|
||||
table.add_column("Status")
|
||||
|
|
@ -471,7 +521,7 @@ def add_repo(
|
|||
"--workspace",
|
||||
"-w",
|
||||
help=(
|
||||
"Workspace to add repo to. "
|
||||
"Workspace directory name to add repo to. "
|
||||
"If omitted, uses the workspace containing the current directory."
|
||||
),
|
||||
),
|
||||
|
|
@ -490,7 +540,7 @@ def add_repo(
|
|||
|
||||
- Lists all directories in repos_dir as repos.
|
||||
- Uses iterfzf to pick repo if --repo not given.
|
||||
- Creates a worktree for branch named after the workspace
|
||||
- Creates a worktree for a branch derived from the workspace name
|
||||
into workspace_dir / repo_name.
|
||||
"""
|
||||
_settings, repos_dir, workspaces_dir = get_ctx_paths(ctx)
|
||||
|
|
@ -499,7 +549,9 @@ def add_repo(
|
|||
console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
ws_name = ws_dir.name
|
||||
# Use display title from README to derive branch name
|
||||
title, _desc = read_workspace_readme(ws_dir)
|
||||
branch = branch_name_for_workspace(title or ws_dir.name)
|
||||
|
||||
all_repos = list_all_repos(repos_dir)
|
||||
if not all_repos:
|
||||
|
|
@ -533,7 +585,6 @@ def add_repo(
|
|||
raise typer.Exit(0)
|
||||
|
||||
# Ensure branch exists or create it
|
||||
branch = ws_name
|
||||
code, _out, err = run_cmd(["git", "rev-parse", "--verify", branch], cwd=repo_path)
|
||||
if code != 0:
|
||||
# create branch from current HEAD
|
||||
|
|
@ -547,7 +598,7 @@ def add_repo(
|
|||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Create worktree
|
||||
# Create worktree (dir name is safe because repo_path.name is)
|
||||
ws_dir.mkdir(parents=True, exist_ok=True)
|
||||
code, _out, err = run_cmd(
|
||||
["git", "worktree", "add", str(target_dir), branch],
|
||||
|
|
@ -562,7 +613,8 @@ def add_repo(
|
|||
|
||||
console.print(
|
||||
f"[green]Added repo[/green] {repo_path.name} "
|
||||
f"to workspace [bold]{ws_name}[/bold] at {target_dir}"
|
||||
f"to workspace [bold]{title or ws_dir.name}[/bold] "
|
||||
f"(dir: {ws_dir.name}) on branch '{branch}' at {target_dir}"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -617,7 +669,7 @@ def remove_workspace(
|
|||
"--workspace",
|
||||
"-w",
|
||||
help=(
|
||||
"Workspace to remove. "
|
||||
"Workspace directory name to remove. "
|
||||
"If omitted, uses the workspace containing the current directory."
|
||||
),
|
||||
),
|
||||
|
|
@ -644,6 +696,8 @@ def remove_workspace(
|
|||
console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
title, _desc = read_workspace_readme(ws_dir)
|
||||
|
||||
# Collect worktrees (subdirs that look like git repos, excluding readme.md)
|
||||
worktrees: List[Path] = []
|
||||
for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()):
|
||||
|
|
@ -656,7 +710,8 @@ def remove_workspace(
|
|||
if not worktrees:
|
||||
# No worktrees, just remove workspace dir
|
||||
if not Confirm.ask(
|
||||
f"[red]No worktrees found.[/red] Remove empty workspace [bold]{ws_dir.name}[/bold]?",
|
||||
f"[red]No worktrees found.[/red] Remove empty workspace "
|
||||
f"[bold]{title or ws_dir.name}[/bold] (dir: {ws_dir.name})?",
|
||||
default=False,
|
||||
):
|
||||
raise typer.Exit(0)
|
||||
|
|
@ -688,7 +743,8 @@ def remove_workspace(
|
|||
raise typer.Exit(1)
|
||||
|
||||
if not Confirm.ask(
|
||||
f"Remove workspace [bold]{ws_dir.name}[/bold] and clean up its worktrees?",
|
||||
f"Remove workspace [bold]{title or ws_dir.name}[/bold] (dir: {ws_dir.name}) "
|
||||
"and clean up its worktrees?",
|
||||
default=False,
|
||||
):
|
||||
raise typer.Exit(0)
|
||||
|
|
@ -726,7 +782,9 @@ def remove_workspace(
|
|||
|
||||
# Finally, remove the workspace directory itself
|
||||
shutil.rmtree(ws_dir)
|
||||
console.print(f"[green]Removed workspace[/green] {ws_dir}")
|
||||
console.print(
|
||||
f"[green]Removed workspace[/green] title='{title or ws_dir.name}' dir='{ws_dir.name}'"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue