add Repo Managment / Kontext Menu in Braches
This commit is contained in:
@@ -347,6 +347,42 @@ pub fn create_branch(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn rename_branch(
|
||||||
|
path: String,
|
||||||
|
old_branch: String,
|
||||||
|
new_branch: String,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
let old_branch = validate_existing_local_branch_name(&repo, &old_branch)?;
|
||||||
|
let new_branch = validate_new_branch_name(&repo, &new_branch)?;
|
||||||
|
|
||||||
|
run_git(
|
||||||
|
&repo,
|
||||||
|
[
|
||||||
|
"branch",
|
||||||
|
"-m",
|
||||||
|
"--",
|
||||||
|
old_branch.as_str(),
|
||||||
|
new_branch.as_str(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
status_for_repo(&repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn delete_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
let branch = validate_existing_local_branch_name(&repo, &branch)?;
|
||||||
|
let status = status_for_repo(&repo)?;
|
||||||
|
if status.current_branch.as_deref() == Some(branch.as_str()) {
|
||||||
|
return Err("Der aktuelle Branch kann nicht geloescht werden.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
run_git(&repo, ["branch", "-d", "--", branch.as_str()])?;
|
||||||
|
status_for_repo(&repo)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stage_files(path: String, files: Vec<String>) -> Result<GitStatus, String> {
|
pub fn stage_files(path: String, files: Vec<String>) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
@@ -2103,6 +2139,41 @@ fn validate_new_branch_name(repo: &Path, branch: &str) -> Result<String, String>
|
|||||||
Ok(normalized)
|
Ok(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_existing_local_branch_name(repo: &Path, branch: &str) -> Result<String, String> {
|
||||||
|
let branch = branch.trim();
|
||||||
|
if branch.is_empty() {
|
||||||
|
return Err("Branch-Name darf nicht leer sein.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = validate_branch_ref_name(branch)?;
|
||||||
|
if !ref_exists(repo, &format!("refs/heads/{normalized}"))? {
|
||||||
|
return Err(format!(
|
||||||
|
"Lokaler Branch '{normalized}' wurde nicht gefunden."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_branch_ref_name(branch: &str) -> Result<String, String> {
|
||||||
|
let output = git_command()
|
||||||
|
.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();
|
||||||
|
Ok(if normalized.is_empty() {
|
||||||
|
branch.to_string()
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn ref_exists(repo: &Path, ref_name: &str) -> Result<bool, String> {
|
fn ref_exists(repo: &Path, ref_name: &str) -> Result<bool, String> {
|
||||||
let output = git_command()
|
let output = git_command()
|
||||||
.arg("-C")
|
.arg("-C")
|
||||||
@@ -3516,6 +3587,51 @@ mod tests {
|
|||||||
assert!(err.contains("existiert bereits"));
|
assert!(err.contains("existiert bereits"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_branch_renames_existing_local_branch() {
|
||||||
|
let repo = init_temp_repo("rename_branch");
|
||||||
|
commit_initial_file(&repo.path);
|
||||||
|
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
run_git_test(&repo.path, ["branch", "feature/old-panel"]);
|
||||||
|
|
||||||
|
let status = rename_branch(
|
||||||
|
repo.path.to_string_lossy().to_string(),
|
||||||
|
"feature/old-panel".to_string(),
|
||||||
|
"feature/new-panel".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(status.current_branch.as_deref(), Some(current.as_str()));
|
||||||
|
assert!(
|
||||||
|
!ref_exists(&repo.path, "refs/heads/feature/old-panel").unwrap(),
|
||||||
|
"old branch should be gone"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ref_exists(&repo.path, "refs/heads/feature/new-panel").unwrap(),
|
||||||
|
"new branch should exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_branch_removes_local_branch_but_rejects_current_branch() {
|
||||||
|
let repo = init_temp_repo("delete_branch");
|
||||||
|
commit_initial_file(&repo.path);
|
||||||
|
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
run_git_test(&repo.path, ["branch", "stale"]);
|
||||||
|
|
||||||
|
let status =
|
||||||
|
delete_branch(repo.path.to_string_lossy().to_string(), "stale".to_string()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(status.current_branch.as_deref(), Some(current.as_str()));
|
||||||
|
assert!(
|
||||||
|
!ref_exists(&repo.path, "refs/heads/stale").unwrap(),
|
||||||
|
"deleted branch should be gone"
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = delete_branch(repo.path.to_string_lossy().to_string(), current).unwrap_err();
|
||||||
|
assert!(err.contains("aktuelle Branch"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_file_patch_stages_and_discards_selected_changes() {
|
fn apply_file_patch_stages_and_discards_selected_changes() {
|
||||||
let repo = init_temp_repo("apply_file_patch");
|
let repo = init_temp_repo("apply_file_patch");
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ mod git;
|
|||||||
use git::{
|
use git::{
|
||||||
apply_file_patch, cancel_code_search, checkout_branch, commit, compare_commits,
|
apply_file_patch, cancel_code_search, checkout_branch, commit, compare_commits,
|
||||||
compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save,
|
compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save,
|
||||||
diff_file_against_working_tree, get_file_patch, get_remote_url, get_status, list_branches,
|
delete_branch, diff_file_against_working_tree, get_file_patch, get_remote_url, get_status,
|
||||||
list_commits, list_file_history, list_repository_files, merge_branch, open_repo_in_explorer,
|
list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
|
||||||
open_repository, open_repository_bundle, pull, push, read_conflict, resolve_conflict,
|
open_repo_in_explorer, open_repository, open_repository_bundle, pull, push, read_conflict,
|
||||||
resolve_conflict_side, restore_file_from_commit, restore_files, restore_to_commit,
|
rename_branch, resolve_conflict, resolve_conflict_side, restore_file_from_commit,
|
||||||
search_code_introductions, stage_files, unstage_files, SearchCancellationState,
|
restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files,
|
||||||
|
SearchCancellationState,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -24,6 +25,8 @@ fn main() {
|
|||||||
list_branches,
|
list_branches,
|
||||||
checkout_branch,
|
checkout_branch,
|
||||||
create_branch,
|
create_branch,
|
||||||
|
rename_branch,
|
||||||
|
delete_branch,
|
||||||
stage_files,
|
stage_files,
|
||||||
unstage_files,
|
unstage_files,
|
||||||
restore_files,
|
restore_files,
|
||||||
|
|||||||
+61
-1
@@ -16,6 +16,7 @@
|
|||||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||||
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
|
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
|
||||||
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
||||||
|
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
|
||||||
import RepoLoadingOverlay from "./lib/components/RepoLoadingOverlay.svelte";
|
import RepoLoadingOverlay from "./lib/components/RepoLoadingOverlay.svelte";
|
||||||
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
|
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
|
||||||
import StatusPanel from "./lib/components/StatusPanel.svelte";
|
import StatusPanel from "./lib/components/StatusPanel.svelte";
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
cancelCodeSearch,
|
cancelCodeSearch,
|
||||||
applyFilePatch,
|
applyFilePatch,
|
||||||
createBranch,
|
createBranch,
|
||||||
|
deleteBranch,
|
||||||
diffFileAgainstWorkingTree,
|
diffFileAgainstWorkingTree,
|
||||||
compareFileToParent,
|
compareFileToParent,
|
||||||
getStatus,
|
getStatus,
|
||||||
@@ -40,6 +42,7 @@
|
|||||||
openRepositoryBundle,
|
openRepositoryBundle,
|
||||||
pull,
|
pull,
|
||||||
push,
|
push,
|
||||||
|
renameBranch,
|
||||||
getRemoteUrl,
|
getRemoteUrl,
|
||||||
credLoad,
|
credLoad,
|
||||||
credSave,
|
credSave,
|
||||||
@@ -121,6 +124,7 @@
|
|||||||
let compareTo = "";
|
let compareTo = "";
|
||||||
let comparison: GitCommitComparison | null = null;
|
let comparison: GitCommitComparison | null = null;
|
||||||
let newBranchCommit: GitCommit | null = null;
|
let newBranchCommit: GitCommit | null = null;
|
||||||
|
let renameBranchTarget: GitBranchInfo | null = null;
|
||||||
let compareSelectOpen = false;
|
let compareSelectOpen = false;
|
||||||
let compareDialogOpen = false;
|
let compareDialogOpen = false;
|
||||||
let selectedDiffPath = "";
|
let selectedDiffPath = "";
|
||||||
@@ -662,6 +666,45 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renameLocalBranch(branch: GitBranchInfo) {
|
||||||
|
if (!activeRepoPath || branch.remote) return;
|
||||||
|
renameBranchTarget = branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRenameBranch(branchName: string) {
|
||||||
|
const branch = renameBranchTarget;
|
||||||
|
const name = branchName.trim();
|
||||||
|
if (!activeRepoPath || !branch || branch.remote || !name || name === branch.name) return;
|
||||||
|
|
||||||
|
await runOperation(`Renaming ${branch.name}`, async () => {
|
||||||
|
applyStatus(await renameBranch(activeRepoPath, branch.name, name));
|
||||||
|
renameBranchTarget = null;
|
||||||
|
await refreshBranchList(activeRepoPath);
|
||||||
|
await refreshCommitHistory(activeRepoPath);
|
||||||
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
|
await refreshFileHistory(activeRepoPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteLocalBranch(branch: GitBranchInfo) {
|
||||||
|
if (!activeRepoPath || branch.remote) return;
|
||||||
|
if (branch.current) {
|
||||||
|
errorMessage = "The current branch cannot be deleted.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = window.confirm(`Delete local branch "${branch.name}"?\n\nGit will refuse if the branch has unmerged changes.`);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await runOperation(`Deleting ${branch.name}`, async () => {
|
||||||
|
applyStatus(await deleteBranch(activeRepoPath, branch.name));
|
||||||
|
await refreshBranchList(activeRepoPath);
|
||||||
|
await refreshCommitHistory(activeRepoPath);
|
||||||
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
|
await refreshFileHistory(activeRepoPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function openNewBranchDialog(commit: GitCommit) {
|
function openNewBranchDialog(commit: GitCommit) {
|
||||||
if (!activeRepoPath || isBusy) return;
|
if (!activeRepoPath || isBusy) return;
|
||||||
newBranchCommit = commit;
|
newBranchCommit = commit;
|
||||||
@@ -1271,16 +1314,21 @@
|
|||||||
function handleWindowKeydown(event: KeyboardEvent) {
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||||
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
||||||
|
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
|
||||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||||
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleWindowContextMenu(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>GitLite</title>
|
<title>GitLite</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleWindowKeydown} />
|
<svelte:window on:keydown={handleWindowKeydown} on:contextmenu={handleWindowContextMenu} />
|
||||||
|
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
<TitleBar
|
<TitleBar
|
||||||
@@ -1509,6 +1557,8 @@
|
|||||||
onCheckout={checkout}
|
onCheckout={checkout}
|
||||||
onMerge={merge}
|
onMerge={merge}
|
||||||
onCreateBranch={createNewBranch}
|
onCreateBranch={createNewBranch}
|
||||||
|
onRenameBranch={renameLocalBranch}
|
||||||
|
onDeleteBranch={deleteLocalBranch}
|
||||||
/>
|
/>
|
||||||
<ExplorerPanel
|
<ExplorerPanel
|
||||||
{repoFiles}
|
{repoFiles}
|
||||||
@@ -1658,6 +1708,16 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Rename a local branch from the branch context menu -->
|
||||||
|
{#if renameBranchTarget}
|
||||||
|
<RenameBranchDialog
|
||||||
|
branch={renameBranchTarget}
|
||||||
|
{isBusy}
|
||||||
|
onRename={submitRenameBranch}
|
||||||
|
onClose={() => { renameBranchTarget = null; }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Compare: pick the two commits to diff -->
|
<!-- Compare: pick the two commits to diff -->
|
||||||
{#if compareSelectOpen}
|
{#if compareSelectOpen}
|
||||||
<CompareSelectDialog
|
<CompareSelectDialog
|
||||||
|
|||||||
+121
@@ -986,6 +986,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
.branch-panel { position: relative; }
|
||||||
|
|
||||||
.branch-create-toggle {
|
.branch-create-toggle {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
min-width: 26px;
|
min-width: 26px;
|
||||||
@@ -1092,6 +1094,60 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.branch-folder-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
gap: 7px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
padding-left: calc(8px + var(--branch-indent, 0px));
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-ink-dim);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.branch-folder-row + .branch-folder-row,
|
||||||
|
.branch-folder-row + .branch-row,
|
||||||
|
.branch-row + .branch-folder-row {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.branch-folder-row:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-border-subtle);
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
.branch-folder-row.current {
|
||||||
|
border-color: rgba(78,202,118,0.2);
|
||||||
|
background: rgba(78,202,118,0.08);
|
||||||
|
}
|
||||||
|
.branch-folder-row svg { color: var(--color-ink-faint); }
|
||||||
|
.branch-folder-row.current svg { color: #4eca76; }
|
||||||
|
.branch-folder-name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.branch-folder-count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--color-ink-dim);
|
||||||
|
background: rgba(94,110,156,0.14);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.branch-row {
|
.branch-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
@@ -1099,6 +1155,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
padding: 7px 8px;
|
padding: 7px 8px;
|
||||||
|
padding-left: calc(8px + var(--branch-indent, 0px));
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: background 120ms, border-color 120ms;
|
transition: background 120ms, border-color 120ms;
|
||||||
@@ -1118,6 +1175,57 @@
|
|||||||
|
|
||||||
.branch-actions { display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
|
.branch-actions { display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
|
||||||
|
|
||||||
|
.branch-context-menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 120;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 184px;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: 0 18px 50px rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-context-menu button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-context-menu button:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-border-subtle);
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-context-menu button.danger {
|
||||||
|
color: #ff9aa8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-context-menu button.danger:hover:not(:disabled) {
|
||||||
|
border-color: rgba(255,92,117,0.34);
|
||||||
|
background: rgba(255,92,117,0.12);
|
||||||
|
color: #ffd0d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-context-menu button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.48;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Explorer --- */
|
/* --- Explorer --- */
|
||||||
|
|
||||||
.explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
|
.explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
|
||||||
@@ -1378,12 +1486,25 @@
|
|||||||
max-height: calc(100vh - 32px);
|
max-height: calc(100vh - 32px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.rename-branch-dialog {
|
||||||
|
display: block;
|
||||||
|
width: min(520px, calc(100vw - 32px));
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
.new-branch-form {
|
.new-branch-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
.rename-branch-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
.new-branch-target {
|
.new-branch-target {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,7 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, ChevronDown, ChevronRight, GitBranch, GitMerge, Plus, X } from "@lucide/svelte";
|
import { Check, ChevronDown, ChevronRight, Folder, FolderOpen, GitBranch, GitMerge, Pencil, Plus, Trash2, X } from "@lucide/svelte";
|
||||||
import type { GitBranch as GitBranchInfo } from "../types";
|
import type { GitBranch as GitBranchInfo } from "../types";
|
||||||
|
|
||||||
|
type BranchTreeNode = BranchFolderNode | BranchLeafNode;
|
||||||
|
|
||||||
|
interface BranchFolderNode {
|
||||||
|
kind: "folder";
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
children: BranchTreeNode[];
|
||||||
|
branchCount: number;
|
||||||
|
current: boolean;
|
||||||
|
folders: Map<string, BranchFolderNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BranchLeafNode {
|
||||||
|
kind: "branch";
|
||||||
|
id: string;
|
||||||
|
branch: GitBranchInfo;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BranchRow = BranchFolderRow | BranchLeafRow;
|
||||||
|
|
||||||
|
interface BranchFolderRow {
|
||||||
|
kind: "folder";
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
depth: number;
|
||||||
|
branchCount: number;
|
||||||
|
current: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BranchLeafRow {
|
||||||
|
kind: "branch";
|
||||||
|
id: string;
|
||||||
|
branch: GitBranchInfo;
|
||||||
|
displayName: string;
|
||||||
|
depth: number;
|
||||||
|
scopeLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
branches: GitBranchInfo[];
|
branches: GitBranchInfo[];
|
||||||
localBranches: GitBranchInfo[];
|
localBranches: GitBranchInfo[];
|
||||||
@@ -11,6 +50,8 @@
|
|||||||
onCheckout: (branch: GitBranchInfo) => void;
|
onCheckout: (branch: GitBranchInfo) => void;
|
||||||
onMerge: (branch: GitBranchInfo) => void;
|
onMerge: (branch: GitBranchInfo) => void;
|
||||||
onCreateBranch: (branchName: string) => void | Promise<void>;
|
onCreateBranch: (branchName: string) => void | Promise<void>;
|
||||||
|
onRenameBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
|
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -22,6 +63,8 @@
|
|||||||
onCheckout = () => {},
|
onCheckout = () => {},
|
||||||
onMerge = () => {},
|
onMerge = () => {},
|
||||||
onCreateBranch = () => {},
|
onCreateBranch = () => {},
|
||||||
|
onRenameBranch = () => {},
|
||||||
|
onDeleteBranch = () => {},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let localOpen = $state(true);
|
let localOpen = $state(true);
|
||||||
@@ -29,6 +72,118 @@
|
|||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let newBranchName = $state("");
|
let newBranchName = $state("");
|
||||||
let createInput = $state<HTMLInputElement | null>(null);
|
let createInput = $state<HTMLInputElement | null>(null);
|
||||||
|
let panelElement = $state<HTMLElement | null>(null);
|
||||||
|
let contextBranch = $state<GitBranchInfo | null>(null);
|
||||||
|
let contextMenuX = $state(0);
|
||||||
|
let contextMenuY = $state(0);
|
||||||
|
let collapsedBranchFolders = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
|
let localBranchRows = $derived(buildBranchRows("local", localBranches, "local"));
|
||||||
|
let remoteBranchRows = $derived(buildBranchRows("remote", remoteBranches, "remote"));
|
||||||
|
|
||||||
|
function createFolder(id: string, name: string): BranchFolderNode {
|
||||||
|
return {
|
||||||
|
kind: "folder",
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
children: [],
|
||||||
|
branchCount: 0,
|
||||||
|
current: false,
|
||||||
|
folders: new Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBranchRows(scope: string, branchList: GitBranchInfo[], scopeLabel: string): BranchRow[] {
|
||||||
|
const root = createFolder(`${scope}:root`, "");
|
||||||
|
|
||||||
|
for (const branch of branchList) {
|
||||||
|
const parts = branch.name.split("/").filter(Boolean);
|
||||||
|
const displayName = parts.length > 0 ? parts[parts.length - 1] : branch.name;
|
||||||
|
const folderParts = parts.slice(0, -1);
|
||||||
|
let parent = root;
|
||||||
|
|
||||||
|
for (let index = 0; index < folderParts.length; index += 1) {
|
||||||
|
const folderName = folderParts[index];
|
||||||
|
const folderPath = folderParts.slice(0, index + 1).join("/");
|
||||||
|
let folder = parent.folders.get(folderName);
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
folder = createFolder(`${scope}:folder:${folderPath}`, folderName);
|
||||||
|
parent.folders.set(folderName, folder);
|
||||||
|
parent.children.push(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.branchCount += 1;
|
||||||
|
folder.current ||= branch.current;
|
||||||
|
parent = folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push({
|
||||||
|
kind: "branch",
|
||||||
|
id: `${scope}:branch:${branch.name}`,
|
||||||
|
branch,
|
||||||
|
displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBranchNodes(root.children);
|
||||||
|
|
||||||
|
const rows: BranchRow[] = [];
|
||||||
|
flattenBranchNodes(root.children, rows, 0, scopeLabel);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBranchNodes(nodes: BranchTreeNode[]) {
|
||||||
|
nodes.sort((left, right) => {
|
||||||
|
if (left.kind !== right.kind) return left.kind === "folder" ? -1 : 1;
|
||||||
|
const leftName = left.kind === "folder" ? left.name : left.displayName;
|
||||||
|
const rightName = right.kind === "folder" ? right.name : right.displayName;
|
||||||
|
return leftName.localeCompare(rightName, undefined, { sensitivity: "base" });
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.kind === "folder") sortBranchNodes(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenBranchNodes(nodes: BranchTreeNode[], rows: BranchRow[], depth: number, scopeLabel: string) {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.kind === "folder") {
|
||||||
|
rows.push({
|
||||||
|
kind: "folder",
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
depth,
|
||||||
|
branchCount: node.branchCount,
|
||||||
|
current: node.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBranchFolderOpen(node.id)) {
|
||||||
|
flattenBranchNodes(node.children, rows, depth + 1, scopeLabel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rows.push({
|
||||||
|
kind: "branch",
|
||||||
|
id: node.id,
|
||||||
|
branch: node.branch,
|
||||||
|
displayName: node.displayName,
|
||||||
|
depth,
|
||||||
|
scopeLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBranchFolderOpen(id: string) {
|
||||||
|
return !collapsedBranchFolders.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBranchFolder(id: string) {
|
||||||
|
const next = new Set(collapsedBranchFolders);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
collapsedBranchFolders = next;
|
||||||
|
}
|
||||||
|
|
||||||
function openCreateForm() {
|
function openCreateForm() {
|
||||||
if (!hasRepository || isBusy) return;
|
if (!hasRepository || isBusy) return;
|
||||||
@@ -57,9 +212,49 @@
|
|||||||
if (target?.closest("button")) return;
|
if (target?.closest("button")) return;
|
||||||
onCheckout(branch);
|
onCheckout(branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openBranchContextMenu(event: MouseEvent, branch: GitBranchInfo) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isBusy || branch.remote) return;
|
||||||
|
|
||||||
|
const rect = panelElement?.getBoundingClientRect();
|
||||||
|
const rawX = rect ? event.clientX - rect.left : event.offsetX;
|
||||||
|
const rawY = rect ? event.clientY - rect.top : event.offsetY;
|
||||||
|
const maxX = Math.max(8, (rect?.width ?? window.innerWidth) - 192);
|
||||||
|
const maxY = Math.max(8, (rect?.height ?? window.innerHeight) - 92);
|
||||||
|
|
||||||
|
contextBranch = branch;
|
||||||
|
contextMenuX = Math.max(8, Math.min(rawX, maxX));
|
||||||
|
contextMenuY = Math.max(8, Math.min(rawY, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBranchContextMenu() {
|
||||||
|
contextBranch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameContextBranch() {
|
||||||
|
const branch = contextBranch;
|
||||||
|
if (!branch || isBusy) return;
|
||||||
|
closeBranchContextMenu();
|
||||||
|
await onRenameBranch(branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteContextBranch() {
|
||||||
|
const branch = contextBranch;
|
||||||
|
if (!branch || branch.current || isBusy) return;
|
||||||
|
closeBranchContextMenu();
|
||||||
|
await onDeleteBranch(branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") closeBranchContextMenu();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Branches">
|
<svelte:window on:click={closeBranchContextMenu} on:keydown={handleWindowKeydown} />
|
||||||
|
|
||||||
|
<section bind:this={panelElement} class="panel branch-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Branches">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Branches</span>
|
<span class="eyebrow">Branches</span>
|
||||||
@@ -127,34 +322,58 @@
|
|||||||
{#if localBranches.length === 0}
|
{#if localBranches.length === 0}
|
||||||
<div class="branch-empty">No local branches.</div>
|
<div class="branch-empty">No local branches.</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each localBranches as branch (branch.name)}
|
{#each localBranchRows as row (row.id)}
|
||||||
<article
|
{#if row.kind === "folder"}
|
||||||
class="branch-row"
|
<button
|
||||||
class:current={branch.current}
|
class="branch-folder-row"
|
||||||
ondblclick={(event) => checkoutOnDoubleClick(event, branch)}
|
class:current={row.current}
|
||||||
title={branch.current ? "Current branch" : "Double-click to checkout"}
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
||||||
>
|
type="button"
|
||||||
<div class="branch-info">
|
onclick={() => toggleBranchFolder(row.id)}
|
||||||
<GitBranch size={16} aria-hidden="true" />
|
aria-expanded={isBranchFolderOpen(row.id)}
|
||||||
<div>
|
title={`${row.name} (${row.branchCount})`}
|
||||||
<strong>{branch.name}</strong>
|
>
|
||||||
<span>local</span>
|
{#if isBranchFolderOpen(row.id)}
|
||||||
|
<ChevronDown size={14} aria-hidden="true" />
|
||||||
|
<FolderOpen size={15} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight size={14} aria-hidden="true" />
|
||||||
|
<Folder size={15} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
<span class="branch-folder-name">{row.name}</span>
|
||||||
|
<span class="branch-folder-count">{row.branchCount}</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<article
|
||||||
|
class="branch-row"
|
||||||
|
class:current={row.branch.current}
|
||||||
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
||||||
|
ondblclick={(event) => checkoutOnDoubleClick(event, row.branch)}
|
||||||
|
oncontextmenu={(event) => openBranchContextMenu(event, row.branch)}
|
||||||
|
title={row.branch.current ? "Current branch" : row.branch.name}
|
||||||
|
>
|
||||||
|
<div class="branch-info">
|
||||||
|
<GitBranch size={16} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<strong>{row.displayName}</strong>
|
||||||
|
<span>{row.scopeLabel}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{#if row.branch.current}
|
||||||
{#if branch.current}
|
<span class="pill pill-active">Current</span>
|
||||||
<span class="pill pill-active">Current</span>
|
{:else}
|
||||||
{:else}
|
<div class="branch-actions">
|
||||||
<div class="branch-actions">
|
<button class="btn-sm" type="button" onclick={() => onCheckout(row.branch)} disabled={isBusy}>
|
||||||
<button class="btn-sm" type="button" onclick={() => onCheckout(branch)} disabled={isBusy}>
|
Checkout
|
||||||
Checkout
|
</button>
|
||||||
</button>
|
<button class="btn-sm" type="button" onclick={() => onMerge(row.branch)} disabled={isBusy} title="Merge into current">
|
||||||
<button class="btn-sm" type="button" onclick={() => onMerge(branch)} disabled={isBusy} title="Merge into current">
|
<GitMerge size={15} aria-hidden="true" />
|
||||||
<GitMerge size={15} aria-hidden="true" />
|
Merge
|
||||||
Merge
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</article>
|
||||||
</article>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -180,38 +399,87 @@
|
|||||||
{#if remoteBranches.length === 0}
|
{#if remoteBranches.length === 0}
|
||||||
<div class="branch-empty">No remote branches.</div>
|
<div class="branch-empty">No remote branches.</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each remoteBranches as branch (branch.name)}
|
{#each remoteBranchRows as row (row.id)}
|
||||||
<article
|
{#if row.kind === "folder"}
|
||||||
class="branch-row"
|
<button
|
||||||
class:current={branch.current}
|
class="branch-folder-row"
|
||||||
ondblclick={(event) => checkoutOnDoubleClick(event, branch)}
|
class:current={row.current}
|
||||||
title={branch.current ? "Current branch" : "Double-click to checkout"}
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
||||||
>
|
type="button"
|
||||||
<div class="branch-info">
|
onclick={() => toggleBranchFolder(row.id)}
|
||||||
<GitBranch size={16} aria-hidden="true" />
|
aria-expanded={isBranchFolderOpen(row.id)}
|
||||||
<div>
|
title={`${row.name} (${row.branchCount})`}
|
||||||
<strong>{branch.name}</strong>
|
>
|
||||||
<span>remote</span>
|
{#if isBranchFolderOpen(row.id)}
|
||||||
|
<ChevronDown size={14} aria-hidden="true" />
|
||||||
|
<FolderOpen size={15} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight size={14} aria-hidden="true" />
|
||||||
|
<Folder size={15} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
<span class="branch-folder-name">{row.name}</span>
|
||||||
|
<span class="branch-folder-count">{row.branchCount}</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<article
|
||||||
|
class="branch-row"
|
||||||
|
class:current={row.branch.current}
|
||||||
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
||||||
|
ondblclick={(event) => checkoutOnDoubleClick(event, row.branch)}
|
||||||
|
title={row.branch.current ? "Current branch" : row.branch.name}
|
||||||
|
>
|
||||||
|
<div class="branch-info">
|
||||||
|
<GitBranch size={16} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<strong>{row.displayName}</strong>
|
||||||
|
<span>{row.scopeLabel}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{#if row.branch.current}
|
||||||
{#if branch.current}
|
<span class="pill pill-active">Current</span>
|
||||||
<span class="pill pill-active">Current</span>
|
{:else}
|
||||||
{:else}
|
<div class="branch-actions">
|
||||||
<div class="branch-actions">
|
<button class="btn-sm" type="button" onclick={() => onCheckout(row.branch)} disabled={isBusy}>
|
||||||
<button class="btn-sm" type="button" onclick={() => onCheckout(branch)} disabled={isBusy}>
|
Checkout
|
||||||
Checkout
|
</button>
|
||||||
</button>
|
<button class="btn-sm" type="button" onclick={() => onMerge(row.branch)} disabled={isBusy} title="Merge into current">
|
||||||
<button class="btn-sm" type="button" onclick={() => onMerge(branch)} disabled={isBusy} title="Merge into current">
|
<GitMerge size={15} aria-hidden="true" />
|
||||||
<GitMerge size={15} aria-hidden="true" />
|
Merge
|
||||||
Merge
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</article>
|
||||||
</article>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if contextBranch}
|
||||||
|
<div
|
||||||
|
class="branch-context-menu"
|
||||||
|
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
||||||
|
role="menu"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label={`Actions for ${contextBranch.name}`}
|
||||||
|
>
|
||||||
|
<button type="button" role="menuitem" onclick={renameContextBranch} disabled={isBusy}>
|
||||||
|
<Pencil size={14} aria-hidden="true" />
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="danger"
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onclick={deleteContextBranch}
|
||||||
|
disabled={isBusy || contextBranch.current}
|
||||||
|
title={contextBranch.current ? "Current branch cannot be deleted" : "Delete local branch"}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { GitBranch, LoaderCircle, X } from "@lucide/svelte";
|
||||||
|
import type { GitBranch as GitBranchInfo } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
branch: GitBranchInfo;
|
||||||
|
isBusy: boolean;
|
||||||
|
onRename: (name: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
branch,
|
||||||
|
isBusy = false,
|
||||||
|
onRename = () => {},
|
||||||
|
onClose = () => {},
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let name = $state("");
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
name = branch.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
function submit(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
const value = name.trim();
|
||||||
|
if (!value || value === branch.name) return;
|
||||||
|
onRename(value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dialog-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
>
|
||||||
|
<div class="dialog rename-branch-dialog" role="dialog" aria-modal="true" aria-label="Rename branch" tabindex="-1">
|
||||||
|
<header class="dialog-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Rename branch</span>
|
||||||
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">{branch.name}</h2>
|
||||||
|
</div>
|
||||||
|
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||||
|
<X size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form class="rename-branch-form" onsubmit={submit}>
|
||||||
|
<label class="new-branch-field">
|
||||||
|
<span>Branch name</span>
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
|
<input
|
||||||
|
bind:value={name}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
disabled={isBusy}
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="new-branch-actions">
|
||||||
|
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="btn-primary" type="submit" disabled={isBusy || name.trim().length === 0 || name.trim() === branch.name}>
|
||||||
|
{#if isBusy}
|
||||||
|
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<GitBranch size={16} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -45,6 +45,18 @@ export function createBranch(
|
|||||||
return invoke<GitStatus>("create_branch", { path, branch, startPoint: startPoint ?? null });
|
return invoke<GitStatus>("create_branch", { path, branch, startPoint: startPoint ?? null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renameBranch(
|
||||||
|
path: string,
|
||||||
|
oldBranch: string,
|
||||||
|
newBranch: string,
|
||||||
|
): Promise<GitStatus> {
|
||||||
|
return invoke<GitStatus>("rename_branch", { path, oldBranch, newBranch });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteBranch(path: string, branch: string): Promise<GitStatus> {
|
||||||
|
return invoke<GitStatus>("delete_branch", { path, branch });
|
||||||
|
}
|
||||||
|
|
||||||
export function stageFiles(path: string, files: string[]): Promise<GitStatus> {
|
export function stageFiles(path: string, files: string[]): Promise<GitStatus> {
|
||||||
return invoke<GitStatus>("stage_files", { path, files });
|
return invoke<GitStatus>("stage_files", { path, files });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user