diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 22f46cf..a499b36 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,12 @@ "Bash(npx vite *)", "Bash(cargo tree *)", "Bash(jobs)", - "Bash(npx svelte-check *)" + "Bash(npx svelte-check *)", + "Bash(git log *)", + "Bash(xxd)", + "Bash(python3 -)", + "Bash(echo \"exit: $?\")", + "Bash(grep -n \"input,\\\\|select,\\\\|input {\\\\|select {\\\\|.repo-form input\\\\|input:focus\\\\|::placeholder\" src/app.css)" ] } } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 52fbb2a..486da8d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,3 +18,24 @@ tauri-build = { version = "2", features = [] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" + +# ── Build profiles ─────────────────────────────────────────────────────────── +# Dev: keep incremental compilation and emit only line-table debug info instead +# of full debuginfo. This cuts link time noticeably (linking is the slow part of +# a `tauri dev` recompile) while still giving panic backtraces with line numbers. +[profile.dev] +incremental = true +debug = "line-tables-only" + +# Optimize third-party dependencies once (they are cached), so the running dev +# build is snappy without slowing down rebuilds of our own crate. +[profile.dev.package."*"] +opt-level = 2 + +# Release: smaller binary (also shrinks the auto-updater download) without +# regressing runtime performance of the code-search feature. +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +strip = true diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index eaa705f..cdd23e6 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -208,6 +208,41 @@ pub fn open_repository(path: String) -> Result { status_for_repo(&repo) } +#[derive(Debug, Clone, Serialize)] +pub struct RepositoryBundle { + pub status: GitStatus, + pub branches: Vec, + pub commits: Vec, + pub files: Vec, +} + +/// Opens a repository and gathers everything the UI needs in a single call. +/// +/// Runs on a blocking thread (so the UI/overlay stays responsive) and resolves +/// the repo and its status only once, instead of the previous four separate +/// commands that each re-ran `git rev-parse` and `git status`. +#[tauri::command] +pub async fn open_repository_bundle( + path: String, + commit_limit: Option, +) -> Result { + tauri::async_runtime::spawn_blocking(move || -> Result { + let repo = resolve_repo(&path)?; + let status = status_for_repo(&repo)?; + let branches = branches_for_repo(&repo)?; + let commits = commits_for_repo(&repo, commit_limit)?; + let files = repository_files_with_status(&repo, &status)?; + Ok(RepositoryBundle { + status, + branches, + commits, + files, + }) + }) + .await + .map_err(|err| format!("Repository konnte nicht geladen werden: {err}"))? +} + #[tauri::command] pub fn get_status(path: String) -> Result { let repo = resolve_repo(&path)?; @@ -217,8 +252,12 @@ pub fn get_status(path: String) -> Result { #[tauri::command] pub fn list_branches(path: String) -> Result, String> { let repo = resolve_repo(&path)?; + branches_for_repo(&repo) +} + +fn branches_for_repo(repo: &Path) -> Result, String> { let output = run_git( - &repo, + repo, [ "for-each-ref", "--format=%(refname)\t%(HEAD)", @@ -281,6 +320,27 @@ pub fn checkout_branch(path: String, branch: String) -> Result, +) -> Result { + let repo = resolve_repo(&path)?; + let branch = validate_new_branch_name(&repo, &branch)?; + match start_point { + Some(start) if !start.trim().is_empty() => { + // Resolve the requested commit first so we fail clearly if it is gone. + let start = verify_commit(&repo, &start)?; + run_git(&repo, ["checkout", "-b", branch.as_str(), start.as_str()])?; + } + _ => { + run_git(&repo, ["checkout", "-b", branch.as_str()])?; + } + } + status_for_repo(&repo) +} + #[tauri::command] pub fn stage_files(path: String, files: Vec) -> Result { let repo = resolve_repo(&path)?; @@ -578,23 +638,34 @@ pub fn merge_branch(path: String, branch: String) -> Result { #[tauri::command] pub fn list_commits(path: String, limit: Option) -> Result, String> { let repo = resolve_repo(&path)?; - if verify_commit(&repo, "HEAD").is_err() { + commits_for_repo(&repo, limit) +} + +fn commits_for_repo(repo: &Path, limit: Option) -> Result, String> { + if verify_commit(repo, "HEAD").is_err() { return Ok(Vec::new()); } let limit = limit.unwrap_or(100).clamp(1, 500).to_string(); + // Fetch the per-commit changed files inline via `--name-status` in a single + // `git log` process, instead of spawning one `git diff-tree` per commit + // (which was ~100 extra processes and the main cost of opening a repo). let output = run_git( - &repo, + repo, [ "log", "--decorate=short", - "--pretty=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%D%x1f%P%x1f%s%x1e", + "--name-status", + "-M", + "-z", + "--root", + "--pretty=format:%x1e%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%D%x1f%P%x1f%s%x1f", "-n", limit.as_str(), ], )?; - parse_commit_log(&repo, &output) + parse_commit_log_inline(&output) } #[tauri::command] @@ -1234,6 +1305,13 @@ fn status_for_repo(repo: &Path) -> Result { fn repository_files(repo: &Path) -> Result, String> { let status = status_for_repo(repo)?; + repository_files_with_status(repo, &status) +} + +fn repository_files_with_status( + repo: &Path, + status: &GitStatus, +) -> Result, String> { let mut files = BTreeMap::::new(); let tracked_output = run_git(repo, ["ls-files", "-z", "--cached", "--deleted"])?; @@ -1572,6 +1650,71 @@ fn commit_search_metadata(repo: &Path, commit: &str) -> Result Result, String> { + const FIELD_SEPARATOR: u8 = 0x1f; + const RECORD_SEPARATOR: u8 = 0x1e; + + let mut commits = Vec::new(); + + for record in output.split(|byte| *byte == RECORD_SEPARATOR) { + // Skip the empty leading chunk and any stray separators left by `-z`. + if record + .iter() + .all(|&byte| matches!(byte, 0 | b'\n' | b'\r' | b' ' | b'\t')) + { + continue; + } + + let parts: Vec<&[u8]> = record.splitn(9, |byte| *byte == FIELD_SEPARATOR).collect(); + if parts.len() < 8 { + return Err(format!( + "Unerwarteter Git-Log-Eintrag: {}", + String::from_utf8_lossy(record) + )); + } + + // Field 8 (if present) holds the name-status list, preceded by the newline + // git inserts between the pretty-format output and the diff. + let mut files_bytes: &[u8] = parts.get(8).copied().unwrap_or(&[]); + while let Some((&first, rest)) = files_bytes.split_first() { + if matches!(first, b'\n' | b'\r') { + files_bytes = rest; + } else { + break; + } + } + + let refs = String::from_utf8_lossy(parts[5]) + .split(',') + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(ToString::to_string) + .collect(); + let parents = String::from_utf8_lossy(parts[6]) + .split_whitespace() + .map(ToString::to_string) + .collect(); + + commits.push(GitCommit { + hash: String::from_utf8_lossy(parts[0]).trim().to_string(), + short_hash: String::from_utf8_lossy(parts[1]).trim().to_string(), + author_name: String::from_utf8_lossy(parts[2]).to_string(), + author_email: String::from_utf8_lossy(parts[3]).to_string(), + date: String::from_utf8_lossy(parts[4]).trim().to_string(), + refs, + parents, + summary: String::from_utf8_lossy(parts[7]).to_string(), + files: parse_commit_files(files_bytes)?, + }); + } + + Ok(commits) +} + fn parse_commit_log(repo: &Path, output: &[u8]) -> Result, String> { const FIELD_SEPARATOR: char = '\x1f'; const RECORD_SEPARATOR: char = '\x1e'; @@ -1839,6 +1982,38 @@ fn local_branch_name_for_remote(remote_branch: &str) -> Option<&str> { .filter(|local| !local.is_empty()) } +fn validate_new_branch_name(repo: &Path, branch: &str) -> Result { + let branch = branch.trim(); + if branch.is_empty() { + return Err("Branch-Name darf nicht leer sein.".to_string()); + } + + let output = git_command() + .arg("-C") + .arg(repo) + .args(["check-ref-format", "--branch", branch]) + .output() + .map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?; + + if !output.status.success() { + let details = command_output_details(&output); + return Err(format!("Ungueltiger Branch-Name: {details}")); + } + + let normalized = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let normalized = if normalized.is_empty() { + branch.to_string() + } else { + normalized + }; + + if ref_exists(repo, &format!("refs/heads/{normalized}"))? { + return Err(format!("Branch '{normalized}' existiert bereits.")); + } + + Ok(normalized) +} + fn ref_exists(repo: &Path, ref_name: &str) -> Result { let output = git_command() .arg("-C") @@ -3186,6 +3361,33 @@ mod tests { assert_eq!(plan, CheckoutPlan::Local("feature/demo".to_string())); } + #[test] + fn create_branch_creates_and_checks_out_local_branch() { + let repo = init_temp_repo("create_branch"); + commit_initial_file(&repo.path); + + let status = create_branch( + repo.path.to_string_lossy().to_string(), + "feature/new-panel".to_string(), + None, + ) + .unwrap(); + + assert_eq!(status.current_branch.as_deref(), Some("feature/new-panel")); + assert!( + ref_exists(&repo.path, "refs/heads/feature/new-panel").unwrap(), + "new branch should exist" + ); + + let err = create_branch( + repo.path.to_string_lossy().to_string(), + "feature/new-panel".to_string(), + None, + ) + .unwrap_err(); + assert!(err.contains("existiert bereits")); + } + #[test] fn restore_to_commit_resets_branch_to_selected_commit() { let repo = init_temp_repo("restore_to_commit"); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 996a15e..a0ce927 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,11 +4,12 @@ mod git; use git::{ cancel_code_search, checkout_branch, commit, compare_commits, compare_file_to_head, - compare_file_to_parent, cred_delete, cred_load, cred_save, diff_file_against_working_tree, - get_remote_url, get_status, list_branches, list_commits, list_file_history, - list_repository_files, merge_branch, open_repository, pull, push, read_conflict, - resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files, - restore_to_commit, search_code_introductions, stage_files, unstage_files, + compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save, + diff_file_against_working_tree, get_remote_url, get_status, list_branches, list_commits, + list_file_history, list_repository_files, merge_branch, open_repository, + open_repository_bundle, pull, push, + read_conflict, resolve_conflict, resolve_conflict_side, restore_file_from_commit, + restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files, SearchCancellationState, }; @@ -22,6 +23,7 @@ fn main() { get_status, list_branches, checkout_branch, + create_branch, stage_files, unstage_files, restore_files, @@ -33,6 +35,7 @@ fn main() { restore_file_from_commit, merge_branch, list_repository_files, + open_repository_bundle, list_file_history, compare_commits, compare_file_to_head, diff --git a/src/App.svelte b/src/App.svelte index a5aba5a..4248692 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,5 +1,5 @@ @@ -934,6 +1009,7 @@ onPush={pushRepo} onRefresh={refreshRepo} onSearch={() => { globalSearchOpen = true; }} + onCompare={openCompareSelect} onToggleAutoRefresh={toggleAutoRefresh} /> @@ -1009,6 +1085,7 @@ {isBusy} onCheckout={checkout} onMerge={merge} + onCreateBranch={createNewBranch} /> - +
@@ -1067,21 +1144,6 @@ onCommitMessageChange={(msg) => { commitMessage = msg; }} />
- - { compareFrom = val; }} - onCompareToChange={(val) => { compareTo = val; }} - onCompare={compareSelectedCommits} - onOpenDialog={openCompareDialog} - />
@@ -1093,6 +1155,7 @@ {expandedCommitHashes} onRestoreCommit={restoreCommit} onPreviewCommitFile={previewCommitFileFromHistory} + onCreateBranchFromCommit={openNewBranchDialog} onToggleCommitFiles={(hash) => { const next = new Set(expandedCommitHashes); if (next.has(hash)) next.delete(hash); else next.add(hash); @@ -1133,10 +1196,42 @@ isSearching={globalSearchBusy} error={globalSearchError} results={globalSearchResults} + files={repoFiles} + fileHistory={fileHistory} + selectedFilePath={selectedExplorerPath} onClose={closeGlobalSearchDialog} onSearch={runGlobalSearch} onCancel={cancelGlobalSearch} onDiff={diffSearchHit} + onSelectFile={selectFileFromSearch} + onFileHistoryDiff={diffSelectedFileFromCommit} + onFileHistoryRestore={restoreSelectedFileFromCommit} + /> +{/if} + + +{#if newBranchCommit} + { newBranchCommit = null; }} + /> +{/if} + + +{#if compareSelectOpen} + { compareFrom = val; }} + onCompareToChange={(val) => { compareTo = val; }} + onCompare={compareSelectedCommits} + onClose={() => { compareSelectOpen = false; }} /> {/if} @@ -1165,6 +1260,11 @@ /> {/if} + +{#if openingRepo} + +{/if} + {#if resolveDialogOpen} div:first-child { min-width: 0; } @@ -1175,11 +1313,38 @@ .global-search-body { display: grid; - grid-template-rows: auto minmax(0, 1fr); + grid-template-rows: auto auto minmax(0, 1fr); min-width: 0; min-height: 0; } + .global-search-tabs { + display: flex; + align-items: center; + gap: 6px; + padding: 9px 12px; + border-bottom: 1px solid var(--color-border-subtle); + background: rgba(7, 8, 16, 0.34); + } + .global-search-tab { + min-height: 30px; + padding: 0 12px; + border-color: transparent; + color: var(--color-ink-dim); + background: transparent; + font-size: 12px; + font-weight: 800; + } + .global-search-tab:hover:not(:disabled) { + border-color: rgba(65,209,255,0.22); + background: rgba(65,209,255,0.07); + } + .global-search-tab.active { + border-color: rgba(65,209,255,0.36); + color: #ffffff; + background: linear-gradient(135deg, rgba(100,108,255,0.26), rgba(65,209,255,0.1)); + } + .global-search-form { display: grid; gap: 10px; @@ -1206,6 +1371,11 @@ white-space: pre; overflow: auto; } + .global-search-query input { + height: 40px; + font-family: var(--font-mono); + font-size: 13px; + } .global-search-options { display: grid; @@ -1342,6 +1512,185 @@ white-space: pre; } + .file-search-form { grid-template-columns: minmax(0, 1fr); } + .file-search-results { + padding: 0; + overflow: hidden; + } + .file-search-split { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.75fr); + height: 100%; + min-width: 0; + min-height: 0; + } + .file-search-column { + min-width: 0; + min-height: 0; + overflow: auto; + padding: 10px; + border-right: 1px solid var(--color-border-subtle); + } + .file-search-list { display: grid; gap: 7px; } + .file-search-hit { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto auto; + align-items: center; + gap: 10px; + width: 100%; + min-height: 48px; + padding: 8px 10px; + border: 1px solid var(--color-border-subtle); + border-radius: 8px; + background: var(--color-surface-raised); + text-align: left; + } + .file-search-hit:hover:not(:disabled) { + border-color: rgba(65,209,255,0.3); + background: var(--color-surface-hover); + } + .file-search-hit.active { + border-color: rgba(90,140,248,0.42); + background: rgba(90,140,248,0.11); + } + .file-search-icon { + display: grid; + place-items: center; + width: 22px; + height: 22px; + color: var(--color-accent); + } + .file-search-icon .language-icon { + display: block; + width: 17px; + height: 17px; + fill: currentColor; + } + .file-search-icon .language-icon path { fill: currentColor; } + .file-search-main { + display: grid; + gap: 2px; + min-width: 0; + } + .file-search-main strong { + min-width: 0; + overflow: hidden; + color: var(--color-ink); + font-family: var(--font-mono); + font-size: 12.5px; + text-overflow: ellipsis; + white-space: nowrap; + } + .file-search-main span { + min-width: 0; + overflow: hidden; + color: var(--color-ink-faint); + font-family: var(--font-mono); + font-size: 11.5px; + text-overflow: ellipsis; + white-space: nowrap; + } + .file-search-action { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--color-accent); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; + } + .file-search-history { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-width: 0; + min-height: 0; + overflow: hidden; + background: rgba(7, 8, 16, 0.18); + } + .file-search-history-head { + display: grid; + gap: 2px; + min-width: 0; + padding: 10px 12px; + border-bottom: 1px solid var(--color-border-subtle); + background: rgba(0,0,0,0.12); + } + .file-search-history-head span { + color: var(--color-ink-faint); + font-size: 10.5px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; + } + .file-search-history-head strong { + min-width: 0; + overflow: hidden; + color: var(--color-ink); + font-family: var(--font-mono); + font-size: 12.5px; + text-overflow: ellipsis; + white-space: nowrap; + } + .file-search-history-list { + display: grid; + align-content: start; + gap: 8px; + min-width: 0; + min-height: 0; + overflow: auto; + padding: 10px; + } + .file-search-history-row { + display: grid; + gap: 8px; + min-width: 0; + padding: 9px; + border: 1px solid var(--color-border-subtle); + border-radius: 8px; + background: var(--color-surface-raised); + } + .file-search-history-main { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 5px 8px; + min-width: 0; + } + .file-search-history-main .hash { + padding: 2px 7px; + border: 1px solid rgba(90,140,248,0.22); + border-radius: 6px; + color: var(--color-accent); + background: rgba(90,140,248,0.13); + font-family: var(--font-mono); + font-size: 11px; + font-weight: 800; + } + .file-search-history-main strong { + min-width: 0; + overflow: hidden; + color: var(--color-ink); + font-size: 12.5px; + text-overflow: ellipsis; + white-space: nowrap; + } + .file-search-history-main > span:last-child { + grid-column: 1 / -1; + overflow: hidden; + color: var(--color-ink-faint); + font-size: 11.5px; + text-overflow: ellipsis; + white-space: nowrap; + } + .file-search-history-actions { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + gap: 6px; + } + /* --- Credential dialog --- */ .cred-card { @@ -1883,7 +2232,6 @@ @media (min-width: 1800px) { .workspace { grid-template-columns: 320px minmax(0, 1fr) 680px; } - .top-section { grid-template-columns: minmax(0, 1fr) 380px; } } @media (max-width: 1400px) { @@ -1929,6 +2277,11 @@ .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); } .compare-dialog .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); } .global-search-options { grid-template-columns: 1fr; } + .file-search-split { grid-template-columns: 1fr; grid-template-rows: minmax(150px, 0.9fr) minmax(220px, 1fr); } + .file-search-column { border-right: none; border-bottom: 1px solid var(--color-border-subtle); } + .file-search-hit { grid-template-columns: auto minmax(0, 1fr); align-items: start; } + .file-search-hit .status-badge, + .file-search-action { grid-column: 2; justify-self: start; } .dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); } .branch-actions { flex-direction: row; justify-content: flex-start; } .tb-action-label { display: none; } diff --git a/src/lib/TitleBar.svelte b/src/lib/TitleBar.svelte index a8ec072..f1b6a4a 100644 --- a/src/lib/TitleBar.svelte +++ b/src/lib/TitleBar.svelte @@ -1,7 +1,7 @@
@@ -29,7 +58,19 @@ Branches

Refs

- {branches.length} +
+ + {branches.length} +
{#if !hasRepository} @@ -37,71 +78,123 @@ {:else if branches.length === 0}

No branches returned.

{:else} -
- {#if localBranches.length > 0} -
- Local - {localBranches.length} -
- {#each localBranches as branch (branch.name)} - {#snippet branchCard()} -
-
-
- {#if branch.current} - Current - {:else} -
- - -
- {/if} -
- {/snippet} - {@render branchCard()} - {/each} +
+ {#if createOpen} +
+
{/if}
diff --git a/src/lib/components/CompareSelectDialog.svelte b/src/lib/components/CompareSelectDialog.svelte new file mode 100644 index 0000000..2cc5dba --- /dev/null +++ b/src/lib/components/CompareSelectDialog.svelte @@ -0,0 +1,108 @@ + + + diff --git a/src/lib/components/GlobalSearchDialog.svelte b/src/lib/components/GlobalSearchDialog.svelte index a7e69e4..b88e42d 100644 --- a/src/lib/components/GlobalSearchDialog.svelte +++ b/src/lib/components/GlobalSearchDialog.svelte @@ -1,6 +1,21 @@
{ if (e.target === e.currentTarget) onClose(); }} > -
diff --git a/src/lib/components/HistoryPanel.svelte b/src/lib/components/HistoryPanel.svelte index 1b1cc7f..2edc4b0 100644 --- a/src/lib/components/HistoryPanel.svelte +++ b/src/lib/components/HistoryPanel.svelte @@ -1,5 +1,5 @@ + + diff --git a/src/lib/components/RepoLoadingOverlay.svelte b/src/lib/components/RepoLoadingOverlay.svelte new file mode 100644 index 0000000..108dc57 --- /dev/null +++ b/src/lib/components/RepoLoadingOverlay.svelte @@ -0,0 +1,175 @@ + + +
+
+ + + +
+ {label}… + {#if repoName} + {repoName} + {/if} +
+ +
+
+
+ + diff --git a/src/lib/components/StatusPanel.svelte b/src/lib/components/StatusPanel.svelte index ba498bd..c1f90db 100644 --- a/src/lib/components/StatusPanel.svelte +++ b/src/lib/components/StatusPanel.svelte @@ -38,6 +38,14 @@ return file.old_path ? `${file.old_path} -> ${file.path}` : file.path; } + function baseName(path: string): string { + return path.split(/[\\/]/).filter(Boolean).pop() ?? path; + } + + function fileName(file: GitFileStatus): string { + return file.old_path ? `${baseName(file.old_path)} -> ${baseName(file.path)}` : baseName(file.path); + } + let hasUnstaged = $derived(changedFiles.some((f) => f.unstaged !== null)); let hasStaged = $derived(changedFiles.some((f) => f.staged !== null)); @@ -90,7 +98,7 @@ {#each changedFiles as file (`${file.old_path ?? ""}:${file.path}`)}
- {displayPath(file)} + {fileName(file)}
diff --git a/src/lib/git.ts b/src/lib/git.ts index e334664..f091a45 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -8,6 +8,7 @@ import type { GitRepositoryFile, GitSearchHit, GitStatus, + RepositoryBundle, StoredCredential, } from "./types"; @@ -15,6 +16,10 @@ export function openRepository(path: string): Promise { return invoke("open_repository", { path }); } +export function openRepositoryBundle(path: string, commitLimit = 100): Promise { + return invoke("open_repository_bundle", { path, commitLimit }); +} + export function getStatus(path: string): Promise { return invoke("get_status", { path }); } @@ -27,6 +32,14 @@ export function checkoutBranch(path: string, branch: string): Promise return invoke("checkout_branch", { path, branch }); } +export function createBranch( + path: string, + branch: string, + startPoint?: string, +): Promise { + return invoke("create_branch", { path, branch, startPoint: startPoint ?? null }); +} + export function stageFiles(path: string, files: string[]): Promise { return invoke("stage_files", { path, files }); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 92831e8..d58ab36 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -54,6 +54,13 @@ export interface GitRepositoryFile { status: FileStatusKind | null; } +export interface RepositoryBundle { + status: GitStatus; + branches: GitBranch[]; + commits: GitCommit[]; + files: GitRepositoryFile[]; +} + export interface GitDiffFile { path: string; old_path: string | null;