Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Clone GitHub repo dialog: a destination folder that doesn't exist yet is now created (recursively) when you clone, instead of failing with "Destination parent does not exist". The typed path is also tilde-expanded (e.g. `~/Desktop/SoftwareSavants/code`) before cloning — `clone_github_repo_and_add` runs `expand_tilde` and `clone_repo` `create_dir_all`s the parent, matching `list_subdirs`/`create_subdir`. The destination autocomplete no longer shows the inline `Create "<name>"` row or its "folder doesn't exist" warning — the path is just created under the hood on Clone (new `allowCreate` prop on `PathAutocomplete`, defaults to true so the BtsBuilder dialog keeps its inline create)
- Clone GitHub repo dialog: row clicks now select a repo instead of cloning immediately and stamp the repo URL into the search input; the local filter normalizes URL queries to the `owner/repo` slug so the list stays narrowed after selection. An explicit Clone button at the bottom-right stays disabled until both a repo (or URL/slug query) and a destination folder are set. Destination folder moved below the result list. While cloning, the picker stays visible (controls disabled) and a compact `max-h-[10rem]` terminal-style preview box appears above the footer streaming git's `--progress` stderr (newest line at the top, capped at 200 lines) via a new `clone-progress` Tauri event from `clone_repo_with_progress` in `github.rs`; the Clone button reads "Cloning..." while git is running
- Fix `Clone failed: git clone failed: … the repository exists.` for users without SSH set up. GitHub clones now go over HTTPS (`https://github.com/<owner>/<repo>.git`) so `gh auth login`'s credential helper supplies the OAuth token instead of relying on user SSH keys. The unused `resolve_repo_clone_urls` + its `gh repo view` roundtrip are gone. A failed clone now also `fs::remove_dir_all`s the partial worktree git left behind, so retrying doesn't immediately hit "Destination already exists"
- Clone GitHub repo dialog no longer scrolls vertically when the destination folder's autocomplete dropdown opens. Overriding the base `Dialog`'s `overflow-y-auto` with `!overflow-visible` lets the absolute-positioned `PathAutocomplete` list pop out below the dialog instead of clipping (which was the trigger that forced the dialog into a scroll container). `PathAutocomplete`'s dropdown also tightened from `max-h-56` to `max-h-40` so it stays compact when there are many sibling folders
Expand Down
20 changes: 19 additions & 1 deletion src-tauri/src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@ pub fn clone_repo(remote_url: &str, parent_dir: &str, dir_name: &str) -> Result<

let parent = Path::new(parent_dir);
if !parent.is_dir() {
return Err(format!("Destination parent does not exist: {parent_dir}"));
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create destination folder {parent_dir}: {e}"))?;
}
let target = parent.join(dir_name);
if target.exists() {
Expand Down Expand Up @@ -1248,6 +1249,23 @@ mod tests {
);
}

#[test]
fn clone_creates_missing_destination_parent() {
let base = tempfile::tempdir().expect("tempdir");
let parent = base.path().join("does/not/exist/yet");
assert!(!parent.exists(), "precondition: parent must be missing");
let result = clone_repo(
"https://invalid.example.invalid/no/such/repo.git",
parent.to_str().unwrap(),
"verun-clone-mkparent-target",
);
assert!(
parent.is_dir(),
"missing destination parent {parent:?} should have been created",
);
assert!(result.is_err());
}

#[test]
fn parse_auth_status_offline_dns_failure() {
// `gh auth status` when DNS resolution fails. We must distinguish
Expand Down
13 changes: 11 additions & 2 deletions src-tauri/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ pub async fn clone_github_repo_and_add(
if parent_dir.trim().is_empty() {
return Err("Choose a destination folder before cloning.".to_string());
}
let parent_dir = crate::bts_scaffold::expand_tilde(parent_dir.trim())?
.to_string_lossy()
.into_owned();
let (final_url, dir_name) = if let Some(nwo) = name_with_owner.as_ref() {
let nwo = nwo.trim();
if nwo.is_empty() || !nwo.contains('/') {
Expand Down Expand Up @@ -1634,7 +1637,8 @@ pub async fn git_commit_amend(
.ok_or_else(|| format!("Task {task_id} not found"))?;

let hash = flatten_join(
tokio::task::spawn_blocking(move || git_ops::commit_amend(&t.worktree_path, &message)).await,
tokio::task::spawn_blocking(move || git_ops::commit_amend(&t.worktree_path, &message))
.await,
)?;
emit_git_local_changed(&app, &task_id);
Ok(hash)
Expand Down Expand Up @@ -1689,7 +1693,12 @@ pub async fn get_staged_diff(

flatten_join(
tokio::task::spawn_blocking(move || {
git_ops::get_staged_diff(&t.worktree_path, &file_path, context_lines, ignore_whitespace)
git_ops::get_staged_diff(
&t.worktree_path,
&file_path,
context_lines,
ignore_whitespace,
)
})
.await,
)
Expand Down
1 change: 1 addition & 0 deletions src/components/CloneRepoDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,7 @@ export const CloneRepoDialog: Component<Props> = (props) => {
value={parentDir()}
onChange={setParentDir}
placeholder="~"
allowCreate={false}
/>
</div>

Expand Down
10 changes: 10 additions & 0 deletions src/components/PathAutocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ describe('PathAutocomplete', () => {
expect(await findByText(/Create.*myproj/)).toBeTruthy()
})

it('does not show the Create row when allowCreate is false', async () => {
listSubdirsMock.mockResolvedValue(['Desktop', 'Documents'])
const { findByText, queryByText, getByRole } = render(() => (
<PathAutocomplete value="~/Des" onChange={() => {}} allowCreate={false} />
))
fireEvent.focus(getByRole('textbox'))
await findByText('Desktop')
expect(queryByText(/Create.*Des/)).toBeNull()
})

it('does not show Create row when prefix exactly matches existing dir', async () => {
listSubdirsMock.mockResolvedValue(['Desktop'])
const { findByText, queryByText, getByRole } = render(() => (
Expand Down
2 changes: 2 additions & 0 deletions src/components/PathAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface Props {
onChange: (next: string) => void
placeholder?: string
autoFocus?: boolean
allowCreate?: boolean
}

function splitPath(raw: string): { parent: string; prefix: string } {
Expand Down Expand Up @@ -72,6 +73,7 @@ export const PathAutocomplete: Component<Props> = (props) => {
})

const createName = createMemo<string | null>(() => {
if (props.allowCreate === false) return null
const { prefix } = splitPath(props.value)
if (!prefix || prefix === '.' || prefix === '..' || !VALID_NAME.test(prefix)) return null
const exact = entries().some((n) => n.toLowerCase() === prefix.toLowerCase())
Expand Down