Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fa57eea6f | ||
|
|
444a7acadd | ||
|
|
ac7cacd687 | ||
|
|
80dc66366d | ||
|
|
a6c86daf62 | ||
|
|
f88b6961d1 |
@@ -1,11 +1,15 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable, List, NamedTuple, Optional
|
from typing import Any, Iterable, List, NamedTuple, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
from minio import Minio
|
from minio import Minio
|
||||||
from minio.error import S3Error
|
from minio.error import S3Error
|
||||||
@@ -329,6 +333,65 @@ def _publish_target(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _gitea_request(url: str, token: str, data: Optional[bytes] = None, **headers: str):
|
||||||
|
request = Request(url, data=data, headers={"Authorization": f"token {token}", **headers})
|
||||||
|
return urlopen(request, timeout=120)
|
||||||
|
|
||||||
|
|
||||||
|
def _upload_gitea_release_asset(version: str, artifact: Path) -> None:
|
||||||
|
"""Attach the installer to the Gitea release that triggered this build."""
|
||||||
|
api_url = os.environ.get("GITEA_API", "").rstrip("/")
|
||||||
|
owner = os.environ.get("OWNER", "")
|
||||||
|
repo = os.environ.get("REPO", "")
|
||||||
|
token = os.environ.get("GITEA_TOKEN") or os.environ.get("GITEA_FALLBACK_TOKEN")
|
||||||
|
if not all((api_url, owner, repo, token)):
|
||||||
|
raise ConfigurationError(
|
||||||
|
"GITEA_API, OWNER, REPO and GITEA_TOKEN are required to publish "
|
||||||
|
"the release asset."
|
||||||
|
)
|
||||||
|
|
||||||
|
release_url = (
|
||||||
|
f"{api_url}/repos/{quote(owner, safe='')}/{quote(repo, safe='')}"
|
||||||
|
f"/releases/tags/{quote(version, safe='')}"
|
||||||
|
)
|
||||||
|
with _gitea_request(release_url, token) as response:
|
||||||
|
release = json.load(response)
|
||||||
|
|
||||||
|
existing_names = {
|
||||||
|
asset.get("name") for asset in release.get("assets", []) if isinstance(asset, dict)
|
||||||
|
}
|
||||||
|
if artifact.name in existing_names:
|
||||||
|
print(f"Release asset {artifact.name} already exists; skipping upload.")
|
||||||
|
return
|
||||||
|
|
||||||
|
boundary = f"----gitty-{uuid.uuid4().hex}"
|
||||||
|
content_type = mimetypes.guess_type(artifact.name)[0] or "application/octet-stream"
|
||||||
|
body = b"".join(
|
||||||
|
(
|
||||||
|
f"--{boundary}\r\n".encode(),
|
||||||
|
(
|
||||||
|
f'Content-Disposition: form-data; name="attachment"; '
|
||||||
|
f'filename="{artifact.name}"\r\n'
|
||||||
|
).encode(),
|
||||||
|
f"Content-Type: {content_type}\r\n\r\n".encode(),
|
||||||
|
artifact.read_bytes(),
|
||||||
|
f"\r\n--{boundary}--\r\n".encode(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
upload_url = (
|
||||||
|
f"{api_url}/repos/{quote(owner, safe='')}/{quote(repo, safe='')}"
|
||||||
|
f"/releases/{release['id']}/assets?{urlencode({'name': artifact.name})}"
|
||||||
|
)
|
||||||
|
with _gitea_request(
|
||||||
|
upload_url,
|
||||||
|
token,
|
||||||
|
data=body,
|
||||||
|
**{"Content-Type": f"multipart/form-data; boundary={boundary}"},
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
print(f"Attached {artifact.name} to Gitea release {version}.")
|
||||||
|
|
||||||
|
|
||||||
def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
|
def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
|
||||||
resolved_version = _resolve_version(version)
|
resolved_version = _resolve_version(version)
|
||||||
artifact_base_url = os.environ.get("ARTIFACT_BASE_URL", "")
|
artifact_base_url = os.environ.get("ARTIFACT_BASE_URL", "")
|
||||||
@@ -385,6 +448,8 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
|
|||||||
artifact_base_url=artifact_base_url,
|
artifact_base_url=artifact_base_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_upload_gitea_release_asset(resolved_version, primary)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "gitty",
|
"name": "gitty",
|
||||||
"version": "2026.8.1",
|
"version": "2026.8.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gitty",
|
"name": "gitty",
|
||||||
"version": "2026.8.1",
|
"version": "2026.8.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/svelte": "^1.21.0",
|
"@lucide/svelte": "^1.21.0",
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gitty",
|
"name": "gitty",
|
||||||
"version": "2026.8.1",
|
"version": "2026.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+297
-105
@@ -382,13 +382,13 @@ fn check_search_cancelled(cancellation: Option<&SearchCancellation>) -> Result<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn open_repository(path: String) -> Result<GitStatus, String> {
|
pub fn open_repository(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn init_repository(path: String, initial_branch: Option<String>) -> Result<GitStatus, String> {
|
pub fn init_repository(path: String, initial_branch: Option<String>) -> Result<GitStatus, String> {
|
||||||
let path = PathBuf::from(path.trim());
|
let path = PathBuf::from(path.trim());
|
||||||
if path.as_os_str().is_empty() {
|
if path.as_os_str().is_empty() {
|
||||||
@@ -416,13 +416,13 @@ pub fn init_repository(path: String, initial_branch: Option<String>) -> Result<G
|
|||||||
status_for_repo(&path)
|
status_for_repo(&path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn open_repo_in_explorer(path: String) -> Result<(), String> {
|
pub fn open_repo_in_explorer(path: String) -> Result<(), String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
open_path_in_file_manager(&repo)
|
open_path_in_file_manager(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn open_repository_file(path: String, file: String) -> Result<(), String> {
|
pub fn open_repository_file(path: String, file: String) -> Result<(), String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -448,6 +448,56 @@ pub struct RepositoryBundle {
|
|||||||
pub files: Vec<GitRepositoryFile>,
|
pub files: Vec<GitRepositoryFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_git_task<T, F>(context: &'static str, task: F) -> Result<T, String>
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
F: FnOnce() -> Result<T, String> + Send + 'static,
|
||||||
|
{
|
||||||
|
tauri::async_runtime::spawn_blocking(task)
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("{context}: {error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_git_worker<T>(name: &str, result: thread::Result<Result<T, String>>) -> Result<T, String> {
|
||||||
|
result.map_err(|_| format!("The {name} Git worker stopped unexpectedly."))?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repository_bundle_for_repo(
|
||||||
|
repo: &Path,
|
||||||
|
commit_limit: Option<u32>,
|
||||||
|
) -> Result<RepositoryBundle, String> {
|
||||||
|
// Status is needed by the file tree. Once it is available, all remaining
|
||||||
|
// reads are independent and can run concurrently. Each worker only starts
|
||||||
|
// read-only Git processes, so this is safe while cutting the former
|
||||||
|
// branches -> tags -> stashes -> commits -> files waterfall down to the
|
||||||
|
// duration of its slowest member.
|
||||||
|
let status = status_for_repo(repo)?;
|
||||||
|
let (branches, tags, stashes, commits, files) = thread::scope(|scope| -> Result<_, String> {
|
||||||
|
let branches = scope.spawn(|| branches_for_repo(repo));
|
||||||
|
let tags = scope.spawn(|| tags_for_repo(repo));
|
||||||
|
let stashes = scope.spawn(|| stashes_for_repo(repo));
|
||||||
|
let commits = scope.spawn(|| commits_for_repo(repo, commit_limit));
|
||||||
|
let files = scope.spawn(|| repository_files_with_status(repo, &status));
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
join_git_worker("branch", branches.join())?,
|
||||||
|
join_git_worker("tag", tags.join())?,
|
||||||
|
join_git_worker("stash", stashes.join())?,
|
||||||
|
join_git_worker("history", commits.join())?,
|
||||||
|
join_git_worker("file tree", files.join())?,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(RepositoryBundle {
|
||||||
|
status,
|
||||||
|
branches,
|
||||||
|
tags,
|
||||||
|
stashes,
|
||||||
|
commits,
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn clone_repository(
|
pub async fn clone_repository(
|
||||||
remote_url: String,
|
remote_url: String,
|
||||||
@@ -481,40 +531,32 @@ pub async fn open_repository_bundle(
|
|||||||
path: String,
|
path: String,
|
||||||
commit_limit: Option<u32>,
|
commit_limit: Option<u32>,
|
||||||
) -> Result<RepositoryBundle, String> {
|
) -> Result<RepositoryBundle, String> {
|
||||||
tauri::async_runtime::spawn_blocking(move || -> Result<RepositoryBundle, String> {
|
run_git_task("Could not load repository", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let status = status_for_repo(&repo)?;
|
repository_bundle_for_repo(&repo, commit_limit)
|
||||||
let branches = branches_for_repo(&repo)?;
|
|
||||||
let tags = tags_for_repo(&repo)?;
|
|
||||||
let stashes = stashes_for_repo(&repo)?;
|
|
||||||
let commits = commits_for_repo(&repo, commit_limit)?;
|
|
||||||
let files = repository_files_with_status(&repo, &status)?;
|
|
||||||
Ok(RepositoryBundle {
|
|
||||||
status,
|
|
||||||
branches,
|
|
||||||
tags,
|
|
||||||
stashes,
|
|
||||||
commits,
|
|
||||||
files,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Could not load repository: {err}"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_status(path: String) -> Result<GitStatus, String> {
|
pub async fn get_status(path: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not refresh repository status", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_branches(path: String) -> Result<Vec<GitBranch>, String> {
|
pub async fn list_branches(path: String) -> Result<Vec<GitBranch>, String> {
|
||||||
|
run_git_task("Could not load branches", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
branches_for_repo(&repo)
|
branches_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
|
pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
|
||||||
log::info!(target: "gitty::remote", "list_remotes invoked: path={path:?}");
|
log::info!(target: "gitty::remote", "list_remotes invoked: path={path:?}");
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
@@ -535,7 +577,7 @@ pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn add_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
pub fn add_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let name = validate_remote_name(&repo, &name, false)?;
|
let name = validate_remote_name(&repo, &name, false)?;
|
||||||
@@ -544,7 +586,7 @@ pub fn add_remote(path: String, name: String, url: String) -> Result<Vec<GitRemo
|
|||||||
list_remotes(path)
|
list_remotes(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitRemote>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let name = validate_remote_name(&repo, &name, true)?;
|
let name = validate_remote_name(&repo, &name, true)?;
|
||||||
@@ -553,7 +595,7 @@ pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitR
|
|||||||
list_remotes(path)
|
list_remotes(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, String> {
|
pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, String> {
|
||||||
log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}");
|
log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}");
|
||||||
let result = (|| {
|
let result = (|| {
|
||||||
@@ -574,7 +616,7 @@ pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, Strin
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn set_branch_upstream(
|
pub fn set_branch_upstream(
|
||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
@@ -607,7 +649,7 @@ pub fn set_branch_upstream(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn delete_remote_branch(
|
pub fn delete_remote_branch(
|
||||||
path: String,
|
path: String,
|
||||||
remote: String,
|
remote: String,
|
||||||
@@ -634,15 +676,21 @@ pub fn delete_remote_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_stashes(path: String) -> Result<Vec<GitStash>, String> {
|
pub async fn list_stashes(path: String) -> Result<Vec<GitStash>, String> {
|
||||||
|
run_git_task("Could not load stashes", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
stashes_for_repo(&repo)
|
stashes_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_tags(path: String) -> Result<Vec<GitTag>, String> {
|
pub async fn list_tags(path: String) -> Result<Vec<GitTag>, String> {
|
||||||
|
run_git_task("Could not load tags", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
tags_for_repo(&repo)
|
tags_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags_for_repo(repo: &Path) -> Result<Vec<GitTag>, String> {
|
fn tags_for_repo(repo: &Path) -> Result<Vec<GitTag>, String> {
|
||||||
@@ -790,11 +838,12 @@ fn parse_stash_subject(subject: &str) -> (Option<String>, String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_push(
|
pub async fn stash_push(
|
||||||
path: String,
|
path: String,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
include_untracked: bool,
|
include_untracked: bool,
|
||||||
) -> Result<GitStatus, String> {
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not stash changes", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let trimmed_message = message.unwrap_or_default().trim().to_string();
|
let trimmed_message = message.unwrap_or_default().trim().to_string();
|
||||||
let mut args: Vec<OsString> = vec![OsString::from("stash"), OsString::from("push")];
|
let mut args: Vec<OsString> = vec![OsString::from("stash"), OsString::from("push")];
|
||||||
@@ -808,21 +857,32 @@ pub fn stash_push(
|
|||||||
|
|
||||||
run_git(&repo, args)?;
|
run_git(&repo, args)?;
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_apply(path: String, selector: String) -> Result<GitStatus, String> {
|
pub async fn stash_apply(path: String, selector: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not apply stash", move || {
|
||||||
run_stash_update(path, "apply", selector)
|
run_stash_update(path, "apply", selector)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_pop(path: String, selector: String) -> Result<GitStatus, String> {
|
pub async fn stash_pop(path: String, selector: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not pop stash", move || {
|
||||||
run_stash_update(path, "pop", selector)
|
run_stash_update(path, "pop", selector)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stash_drop(path: String, selector: String) -> Result<GitStatus, String> {
|
pub async fn stash_drop(path: String, selector: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not drop stash", move || {
|
||||||
run_stash_update(path, "drop", selector)
|
run_stash_update(path, "drop", selector)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_stash_update(path: String, action: &str, selector: String) -> Result<GitStatus, String> {
|
fn run_stash_update(path: String, action: &str, selector: String) -> Result<GitStatus, String> {
|
||||||
@@ -859,7 +919,14 @@ fn validate_stash_selector(selector: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
pub async fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not check out branch", move || {
|
||||||
|
checkout_branch_core(path, branch)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkout_branch_core(path: String, branch: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let branch = branch.trim().to_string();
|
let branch = branch.trim().to_string();
|
||||||
if branch.is_empty() {
|
if branch.is_empty() {
|
||||||
@@ -885,7 +952,18 @@ pub fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_branch(
|
pub async fn create_branch(
|
||||||
|
path: String,
|
||||||
|
branch: String,
|
||||||
|
start_point: Option<String>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not create branch", move || {
|
||||||
|
create_branch_core(path, branch, start_point)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_branch_core(
|
||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
start_point: Option<String>,
|
start_point: Option<String>,
|
||||||
@@ -906,7 +984,18 @@ pub fn create_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn rename_branch(
|
pub async fn rename_branch(
|
||||||
|
path: String,
|
||||||
|
old_branch: String,
|
||||||
|
new_branch: String,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not rename branch", move || {
|
||||||
|
rename_branch_core(path, old_branch, new_branch)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_branch_core(
|
||||||
path: String,
|
path: String,
|
||||||
old_branch: String,
|
old_branch: String,
|
||||||
new_branch: String,
|
new_branch: String,
|
||||||
@@ -929,7 +1018,18 @@ pub fn rename_branch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_branch(
|
pub async fn delete_branch(
|
||||||
|
path: String,
|
||||||
|
branch: String,
|
||||||
|
force: Option<bool>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not delete branch", move || {
|
||||||
|
delete_branch_core(path, branch, force)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_branch_core(
|
||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
force: Option<bool>,
|
force: Option<bool>,
|
||||||
@@ -1070,13 +1170,43 @@ fn worktrees_for_repo(repo: &Path) -> Result<Vec<GitWorktree>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn list_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not load worktrees", move || {
|
||||||
|
list_worktrees_core(path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_worktrees_core(path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
worktrees_for_repo(&repo)
|
worktrees_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_worktree(
|
pub async fn add_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
branch: Option<String>,
|
||||||
|
new_branch: Option<String>,
|
||||||
|
start_point: Option<String>,
|
||||||
|
detached: Option<bool>,
|
||||||
|
lock: Option<bool>,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not add worktree", move || {
|
||||||
|
add_worktree_core(
|
||||||
|
path,
|
||||||
|
worktree_path,
|
||||||
|
branch,
|
||||||
|
new_branch,
|
||||||
|
start_point,
|
||||||
|
detached,
|
||||||
|
lock,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
@@ -1136,7 +1266,18 @@ pub fn add_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn remove_worktree(
|
pub async fn remove_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
force: Option<bool>,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not remove worktree", move || {
|
||||||
|
remove_worktree_core(path, worktree_path, force)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
force: Option<bool>,
|
force: Option<bool>,
|
||||||
@@ -1168,7 +1309,18 @@ pub fn remove_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn move_worktree(
|
pub async fn move_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
destination: String,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not move worktree", move || {
|
||||||
|
move_worktree_core(path, worktree_path, destination)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
destination: String,
|
destination: String,
|
||||||
@@ -1191,7 +1343,18 @@ pub fn move_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn lock_worktree(
|
pub async fn lock_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
reason: Option<String>,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not lock worktree", move || {
|
||||||
|
lock_worktree_core(path, worktree_path, reason)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock_worktree_core(
|
||||||
path: String,
|
path: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
@@ -1211,28 +1374,59 @@ pub fn lock_worktree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn unlock_worktree(path: String, worktree_path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn unlock_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not unlock worktree", move || {
|
||||||
|
unlock_worktree_core(path, worktree_path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_worktree_core(path: String, worktree_path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
run_git(&repo, ["worktree", "unlock", "--", worktree_path.as_str()])?;
|
run_git(&repo, ["worktree", "unlock", "--", worktree_path.as_str()])?;
|
||||||
worktrees_for_repo(&repo)
|
worktrees_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn prune_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn prune_worktrees(path: String) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not prune worktrees", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
run_git(&repo, ["worktree", "prune"])?;
|
run_git(&repo, ["worktree", "prune"])?;
|
||||||
worktrees_for_repo(&repo)
|
worktrees_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn repair_worktree(path: String, worktree_path: String) -> Result<Vec<GitWorktree>, String> {
|
pub async fn repair_worktree(
|
||||||
|
path: String,
|
||||||
|
worktree_path: String,
|
||||||
|
) -> Result<Vec<GitWorktree>, String> {
|
||||||
|
run_git_task("Could not repair worktree", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
run_git(&repo, ["worktree", "repair", "--", worktree_path.as_str()])?;
|
run_git(&repo, ["worktree", "repair", "--", worktree_path.as_str()])?;
|
||||||
worktrees_for_repo(&repo)
|
worktrees_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_tag(
|
pub async fn create_tag(
|
||||||
|
path: String,
|
||||||
|
name: String,
|
||||||
|
target: Option<String>,
|
||||||
|
message: Option<String>,
|
||||||
|
) -> Result<Vec<GitTag>, String> {
|
||||||
|
run_git_task("Could not create tag", move || {
|
||||||
|
create_tag_core(path, name, target, message)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tag_core(
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
@@ -1271,20 +1465,24 @@ pub fn create_tag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_tag(path: String, name: String) -> Result<Vec<GitTag>, String> {
|
pub async fn delete_tag(path: String, name: String) -> Result<Vec<GitTag>, String> {
|
||||||
|
run_git_task("Could not delete tag", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let name = validate_existing_tag_name(&repo, &name)?;
|
let name = validate_existing_tag_name(&repo, &name)?;
|
||||||
run_git(&repo, ["tag", "-d", name.as_str()])?;
|
run_git(&repo, ["tag", "-d", name.as_str()])?;
|
||||||
tags_for_repo(&repo)
|
tags_for_repo(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn push_tag(
|
pub async fn push_tag(
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
run_git_task("Could not push tag", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let name = validate_existing_tag_name(&repo, &name)?;
|
let name = validate_existing_tag_name(&repo, &name)?;
|
||||||
let remote = initial_push_remote_name(&repo)?;
|
let remote = initial_push_remote_name(&repo)?;
|
||||||
@@ -1301,6 +1499,8 @@ pub fn push_tag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_new_tag_name(repo: &Path, name: &str) -> Result<String, String> {
|
fn validate_new_tag_name(repo: &Path, name: &str) -> Result<String, String> {
|
||||||
@@ -1435,7 +1635,7 @@ pub async fn restore_files(
|
|||||||
.map_err(|err| format!("Could not restore files: {err}"))?
|
.map_err(|err| format!("Could not restore files: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_file_patch(path: String, file: String, staged: bool) -> Result<String, String> {
|
pub fn get_file_patch(path: String, file: String, staged: bool) -> Result<String, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -1866,7 +2066,7 @@ pub async fn commit_ai_review(
|
|||||||
parse_ai_review(&raw)
|
parse_ai_review(&raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn apply_file_patch(
|
pub fn apply_file_patch(
|
||||||
path: String,
|
path: String,
|
||||||
file: String,
|
file: String,
|
||||||
@@ -1951,7 +2151,7 @@ pub async fn amend_commit(path: String, message: Option<String>) -> Result<GitSt
|
|||||||
.map_err(|err| format!("Could not amend commit: {err}"))?
|
.map_err(|err| format!("Could not amend commit: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn last_commit_message(path: String) -> Result<Option<String>, String> {
|
pub fn last_commit_message(path: String) -> Result<Option<String>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if verify_commit(&repo, "HEAD").is_err() {
|
if verify_commit(&repo, "HEAD").is_err() {
|
||||||
@@ -1967,7 +2167,7 @@ pub fn last_commit_message(path: String) -> Result<Option<String>, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn undo_last_commit(path: String) -> Result<GitStatus, String> {
|
pub fn undo_last_commit(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if verify_commit(&repo, "HEAD").is_err() {
|
if verify_commit(&repo, "HEAD").is_err() {
|
||||||
@@ -2146,7 +2346,7 @@ fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
|||||||
|
|
||||||
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
||||||
/// current branch, falling back to `origin`, then the first configured remote).
|
/// current branch, falling back to `origin`, then the first configured remote).
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_remote_url(path: String) -> Result<Option<String>, String> {
|
pub fn get_remote_url(path: String) -> Result<Option<String>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let remote = upstream_remote_name(&repo).unwrap_or_else(|| "origin".to_string());
|
let remote = upstream_remote_name(&repo).unwrap_or_else(|| "origin".to_string());
|
||||||
@@ -2301,7 +2501,7 @@ fn push_args_for_repo_to(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
||||||
let entry = cred_entry(&key)?;
|
let entry = cred_entry(&key)?;
|
||||||
match entry.get_password() {
|
match entry.get_password() {
|
||||||
@@ -2315,7 +2515,7 @@ pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cred_save(
|
pub fn cred_save(
|
||||||
key: String,
|
key: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -2336,7 +2536,7 @@ pub fn cred_save(
|
|||||||
.map_err(|err| format!("Saving to keychain failed: {err}"))
|
.map_err(|err| format!("Saving to keychain failed: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cred_delete(key: String) -> Result<(), String> {
|
pub fn cred_delete(key: String) -> Result<(), String> {
|
||||||
let entry = cred_entry(&key)?;
|
let entry = cred_entry(&key)?;
|
||||||
match entry.delete_credential() {
|
match entry.delete_credential() {
|
||||||
@@ -2402,7 +2602,7 @@ pub async fn merge_branch(
|
|||||||
.map_err(|err| format!("Could not merge: {err}"))?
|
.map_err(|err| format!("Could not merge: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn merge_continue(path: String) -> Result<GitStatus, String> {
|
pub fn merge_continue(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !merge_in_progress(&repo) {
|
if !merge_in_progress(&repo) {
|
||||||
@@ -2415,7 +2615,7 @@ pub fn merge_continue(path: String) -> Result<GitStatus, String> {
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn merge_abort(path: String) -> Result<GitStatus, String> {
|
pub fn merge_abort(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !merge_in_progress(&repo) {
|
if !merge_in_progress(&repo) {
|
||||||
@@ -2425,7 +2625,7 @@ pub fn merge_abort(path: String) -> Result<GitStatus, String> {
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn revert_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
pub fn revert_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let commit = verify_commit(&repo, &commit)?;
|
let commit = verify_commit(&repo, &commit)?;
|
||||||
@@ -2470,7 +2670,7 @@ pub async fn rebase_branch(path: String, branch: String) -> Result<GitStatus, St
|
|||||||
.map_err(|err| format!("Could not rebase: {err}"))?
|
.map_err(|err| format!("Could not rebase: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn list_interactive_rebase_commits(
|
pub fn list_interactive_rebase_commits(
|
||||||
path: String,
|
path: String,
|
||||||
base: String,
|
base: String,
|
||||||
@@ -2550,7 +2750,7 @@ pub async fn start_interactive_rebase(
|
|||||||
.map_err(|err| format!("Could not run interactive rebase: {err}"))?
|
.map_err(|err| format!("Could not run interactive rebase: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !rebase_in_progress(&repo) {
|
if !rebase_in_progress(&repo) {
|
||||||
@@ -2580,7 +2780,7 @@ pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !rebase_in_progress(&repo) {
|
if !rebase_in_progress(&repo) {
|
||||||
@@ -2592,7 +2792,7 @@ pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>, String> {
|
pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let limit = limit.unwrap_or(250).clamp(1, 1_000).to_string();
|
let limit = limit.unwrap_or(250).clamp(1, 1_000).to_string();
|
||||||
@@ -2610,7 +2810,7 @@ pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>,
|
|||||||
parse_reflog(&output)
|
parse_reflog(&output)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn restore_reflog_entry(
|
pub fn restore_reflog_entry(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -2870,7 +3070,7 @@ pub async fn cherry_pick_commit(path: String, commit: String) -> Result<GitStatu
|
|||||||
.map_err(|err| format!("Could not cherry-pick: {err}"))?
|
.map_err(|err| format!("Could not cherry-pick: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cherry_pick_continue(path: String) -> Result<GitStatus, String> {
|
pub fn cherry_pick_continue(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !cherry_pick_in_progress(&repo) {
|
if !cherry_pick_in_progress(&repo) {
|
||||||
@@ -2888,7 +3088,7 @@ pub fn cherry_pick_continue(path: String) -> Result<GitStatus, String> {
|
|||||||
cherry_pick_status_or_error(&repo, output, "Cherry-pick continue failed")
|
cherry_pick_status_or_error(&repo, output, "Cherry-pick continue failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn cherry_pick_abort(path: String) -> Result<GitStatus, String> {
|
pub fn cherry_pick_abort(path: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if !cherry_pick_in_progress(&repo) {
|
if !cherry_pick_in_progress(&repo) {
|
||||||
@@ -2917,13 +3117,16 @@ fn cherry_pick_status_or_error(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_commits(
|
pub async fn list_commits(
|
||||||
path: String,
|
path: String,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
skip: Option<u32>,
|
skip: Option<u32>,
|
||||||
) -> Result<Vec<GitCommit>, String> {
|
) -> Result<Vec<GitCommit>, String> {
|
||||||
|
run_git_task("Could not load commit history", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
commit_page_for_repo(&repo, limit, skip)
|
commit_page_for_repo(&repo, limit, skip)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commits_for_repo(repo: &Path, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
|
fn commits_for_repo(repo: &Path, limit: Option<u32>) -> Result<Vec<GitCommit>, String> {
|
||||||
@@ -2968,9 +3171,12 @@ fn commit_page_for_repo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_repository_files(path: String) -> Result<Vec<GitRepositoryFile>, String> {
|
pub async fn list_repository_files(path: String) -> Result<Vec<GitRepositoryFile>, String> {
|
||||||
|
run_git_task("Could not load repository files", move || {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
repository_files(&repo)
|
repository_files(&repo)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -3020,7 +3226,7 @@ pub fn cancel_file_history(
|
|||||||
|
|
||||||
const UNCOMMITTED_BLAME_HASH: &str = "0000000000000000000000000000000000000000";
|
const UNCOMMITTED_BLAME_HASH: &str = "0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_file_blame(path: String, file: String) -> Result<GitBlameResult, String> {
|
pub fn get_file_blame(path: String, file: String) -> Result<GitBlameResult, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -3318,7 +3524,7 @@ fn search_code_introductions_core(
|
|||||||
Ok(hits)
|
Ok(hits)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let commit_hash = verify_commit(&repo, &commit)?;
|
let commit_hash = verify_commit(&repo, &commit)?;
|
||||||
@@ -3341,7 +3547,7 @@ pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, Stri
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn restore_file_from_commit(
|
pub fn restore_file_from_commit(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3359,7 +3565,7 @@ pub fn restore_file_from_commit(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn compare_commits(
|
pub fn compare_commits(
|
||||||
path: String,
|
path: String,
|
||||||
from: String,
|
from: String,
|
||||||
@@ -3417,7 +3623,7 @@ pub fn compare_commits(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn diff_file_against_working_tree(
|
pub fn diff_file_against_working_tree(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3463,7 +3669,7 @@ pub fn diff_file_against_working_tree(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn compare_file_to_head(
|
pub fn compare_file_to_head(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3525,7 +3731,7 @@ pub fn compare_file_to_head(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn compare_file_to_parent(
|
pub fn compare_file_to_parent(
|
||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
@@ -3599,7 +3805,7 @@ pub fn compare_file_to_parent(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String> {
|
pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -3635,7 +3841,7 @@ pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn resolve_conflict_side(
|
pub fn resolve_conflict_side(
|
||||||
path: String,
|
path: String,
|
||||||
file: String,
|
file: String,
|
||||||
@@ -3677,7 +3883,7 @@ fn index_stage_size(repo: &Path, stage: u8, file: &str) -> Option<u64> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn resolve_conflict(path: String, file: String, content: String) -> Result<GitStatus, String> {
|
pub fn resolve_conflict(path: String, file: String, content: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
@@ -4073,21 +4279,7 @@ fn clone_repository_core(
|
|||||||
run_git_clone(remote_url.trim(), &target, username, password)?;
|
run_git_clone(remote_url.trim(), &target, username, password)?;
|
||||||
|
|
||||||
let repo = resolve_repo(&target.to_string_lossy())?;
|
let repo = resolve_repo(&target.to_string_lossy())?;
|
||||||
let status = status_for_repo(&repo)?;
|
repository_bundle_for_repo(&repo, commit_limit)
|
||||||
let branches = branches_for_repo(&repo)?;
|
|
||||||
let tags = tags_for_repo(&repo)?;
|
|
||||||
let stashes = stashes_for_repo(&repo)?;
|
|
||||||
let commits = commits_for_repo(&repo, commit_limit)?;
|
|
||||||
let files = repository_files_with_status(&repo, &status)?;
|
|
||||||
|
|
||||||
Ok(RepositoryBundle {
|
|
||||||
status,
|
|
||||||
branches,
|
|
||||||
tags,
|
|
||||||
stashes,
|
|
||||||
commits,
|
|
||||||
files,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_target_path(
|
fn clone_target_path(
|
||||||
@@ -6747,7 +6939,7 @@ mod tests {
|
|||||||
let repo = init_temp_repo("create_branch");
|
let repo = init_temp_repo("create_branch");
|
||||||
commit_initial_file(&repo.path);
|
commit_initial_file(&repo.path);
|
||||||
|
|
||||||
let status = create_branch(
|
let status = create_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/new-panel".to_string(),
|
"feature/new-panel".to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -6760,7 +6952,7 @@ mod tests {
|
|||||||
"new branch should exist"
|
"new branch should exist"
|
||||||
);
|
);
|
||||||
|
|
||||||
let err = create_branch(
|
let err = create_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/new-panel".to_string(),
|
"feature/new-panel".to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -6776,7 +6968,7 @@ mod tests {
|
|||||||
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
run_git_test(&repo.path, ["branch", "feature/old-panel"]);
|
run_git_test(&repo.path, ["branch", "feature/old-panel"]);
|
||||||
|
|
||||||
let status = rename_branch(
|
let status = rename_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/old-panel".to_string(),
|
"feature/old-panel".to_string(),
|
||||||
"feature/new-panel".to_string(),
|
"feature/new-panel".to_string(),
|
||||||
@@ -6801,7 +6993,7 @@ mod tests {
|
|||||||
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
let current = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
run_git_test(&repo.path, ["branch", "stale"]);
|
run_git_test(&repo.path, ["branch", "stale"]);
|
||||||
|
|
||||||
let status = delete_branch(
|
let status = delete_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"stale".to_string(),
|
"stale".to_string(),
|
||||||
None,
|
None,
|
||||||
@@ -6815,7 +7007,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err =
|
||||||
delete_branch(repo.path.to_string_lossy().to_string(), current, None).unwrap_err();
|
delete_branch_core(repo.path.to_string_lossy().to_string(), current, None).unwrap_err();
|
||||||
assert!(err.contains("current branch"));
|
assert!(err.contains("current branch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6831,7 +7023,7 @@ mod tests {
|
|||||||
run_git_test(&repo.path, ["commit", "-q", "-m", "feature"]);
|
run_git_test(&repo.path, ["commit", "-q", "-m", "feature"]);
|
||||||
run_git_test(&repo.path, ["checkout", "-q", current.as_str()]);
|
run_git_test(&repo.path, ["checkout", "-q", current.as_str()]);
|
||||||
|
|
||||||
let err = delete_branch(
|
let err = delete_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/unmerged".to_string(),
|
"feature/unmerged".to_string(),
|
||||||
Some(false),
|
Some(false),
|
||||||
@@ -6839,7 +7031,7 @@ mod tests {
|
|||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.contains("not fully merged"));
|
assert!(err.contains("not fully merged"));
|
||||||
|
|
||||||
delete_branch(
|
delete_branch_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
"feature/unmerged".to_string(),
|
"feature/unmerged".to_string(),
|
||||||
Some(true),
|
Some(true),
|
||||||
@@ -7216,7 +7408,7 @@ mod tests {
|
|||||||
commit_initial_file(&repo.path);
|
commit_initial_file(&repo.path);
|
||||||
run_git_test(&repo.path, ["branch", "feature"]);
|
run_git_test(&repo.path, ["branch", "feature"]);
|
||||||
|
|
||||||
let rows = add_worktree(
|
let rows = add_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
Some("feature".to_string()),
|
Some("feature".to_string()),
|
||||||
@@ -7234,7 +7426,7 @@ mod tests {
|
|||||||
assert!(!linked.is_main);
|
assert!(!linked.is_main);
|
||||||
assert!(linked.clean);
|
assert!(linked.clean);
|
||||||
|
|
||||||
let rows = lock_worktree(
|
let rows = lock_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
Some("test lock".to_string()),
|
Some("test lock".to_string()),
|
||||||
@@ -7247,13 +7439,13 @@ mod tests {
|
|||||||
assert!(linked.locked);
|
assert!(linked.locked);
|
||||||
assert_eq!(linked.lock_reason.as_deref(), Some("test lock"));
|
assert_eq!(linked.lock_reason.as_deref(), Some("test lock"));
|
||||||
|
|
||||||
unlock_worktree(
|
unlock_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
)
|
)
|
||||||
.expect("worktree should unlock");
|
.expect("worktree should unlock");
|
||||||
|
|
||||||
let rows = remove_worktree(
|
let rows = remove_worktree_core(
|
||||||
repo.path.to_string_lossy().to_string(),
|
repo.path.to_string_lossy().to_string(),
|
||||||
destination.path.to_string_lossy().to_string(),
|
destination.path.to_string_lossy().to_string(),
|
||||||
Some(false),
|
Some(false),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Gitty",
|
"productName": "Gitty",
|
||||||
"version": "2026.8.1",
|
"version": "2026.8.2",
|
||||||
"identifier": "com.gitty",
|
"identifier": "com.gitty",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+175
-163
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, tick } from "svelte";
|
import { onDestroy, onMount, tick } from "svelte";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||||
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||||
|
import { beginFrontendShutdown } from "./lib/telemetry";
|
||||||
|
|
||||||
import TitleBar from "./lib/TitleBar.svelte";
|
import TitleBar from "./lib/TitleBar.svelte";
|
||||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||||
@@ -151,6 +153,7 @@
|
|||||||
RebaseCommit,
|
RebaseCommit,
|
||||||
RebasePlanItem,
|
RebasePlanItem,
|
||||||
ReflogEntry,
|
ReflogEntry,
|
||||||
|
RepositoryBundle,
|
||||||
StoredCredential,
|
StoredCredential,
|
||||||
} from "./lib/types";
|
} from "./lib/types";
|
||||||
|
|
||||||
@@ -202,6 +205,15 @@
|
|||||||
clearIfCurrent: (message: string) => void;
|
clearIfCurrent: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RepositoryRefreshOptions {
|
||||||
|
branches?: boolean;
|
||||||
|
tags?: boolean;
|
||||||
|
stashes?: boolean;
|
||||||
|
commits?: boolean;
|
||||||
|
files?: boolean;
|
||||||
|
fileHistory?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
||||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||||
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
|
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
|
||||||
@@ -370,12 +382,13 @@
|
|||||||
const BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS = 30_000;
|
const BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS = 30_000;
|
||||||
const BACKGROUND_FETCH_OTHER_BATCH_SIZE = 2;
|
const BACKGROUND_FETCH_OTHER_BATCH_SIZE = 2;
|
||||||
const STARTUP_SPLASH_MIN_VISIBLE_MS = 850;
|
const STARTUP_SPLASH_MIN_VISIBLE_MS = 850;
|
||||||
const STARTUP_FETCH_MAX_WAIT_MS = 20_000;
|
|
||||||
let backgroundFetchTimer: ReturnType<typeof setInterval> | undefined;
|
let backgroundFetchTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let backgroundFetchInFlight = false;
|
let backgroundFetchInFlight = false;
|
||||||
let backgroundRepoStatusInFlight = false;
|
let backgroundRepoStatusInFlight = false;
|
||||||
let backgroundRepoStatusIndex = 0;
|
let backgroundRepoStatusIndex = 0;
|
||||||
|
let appShuttingDown = false;
|
||||||
|
let unlistenCloseRequested: (() => void) | undefined;
|
||||||
let lastRepoSwitchAt = 0;
|
let lastRepoSwitchAt = 0;
|
||||||
let updateToastOpen = false;
|
let updateToastOpen = false;
|
||||||
let updateToastState: UpdateToastState = "available";
|
let updateToastState: UpdateToastState = "available";
|
||||||
@@ -476,21 +489,44 @@
|
|||||||
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
||||||
void runStartupSequence();
|
void runStartupSequence();
|
||||||
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
||||||
|
window.addEventListener("beforeunload", handleAppShutdown);
|
||||||
|
window.addEventListener("pagehide", handleAppShutdown);
|
||||||
|
try {
|
||||||
|
void getCurrentWindow().onCloseRequested(() => handleAppShutdown()).then((unlisten) => {
|
||||||
|
if (appShuttingDown) unlisten();
|
||||||
|
else unlistenCloseRequested = unlisten;
|
||||||
|
}).catch(() => {
|
||||||
|
// Browser preview has no Tauri window; DOM lifecycle events still cover it.
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// getCurrentWindow itself throws synchronously in a plain browser preview.
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
handleAppShutdown();
|
||||||
themeMediaQuery?.removeEventListener("change", handleSystemThemeChange);
|
themeMediaQuery?.removeEventListener("change", handleSystemThemeChange);
|
||||||
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
|
window.removeEventListener("beforeunload", handleAppShutdown);
|
||||||
if (backgroundRepoStatusTimer) clearInterval(backgroundRepoStatusTimer);
|
window.removeEventListener("pagehide", handleAppShutdown);
|
||||||
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
|
unlistenCloseRequested?.();
|
||||||
if (commitAiPollTimer) clearInterval(commitAiPollTimer);
|
|
||||||
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
||||||
Object.values(errorAutoHideStates).forEach((state) => {
|
Object.values(errorAutoHideStates).forEach((state) => {
|
||||||
if (state?.timer) clearTimeout(state.timer);
|
if (state?.timer) clearTimeout(state.timer);
|
||||||
});
|
});
|
||||||
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
|
// Do not invoke backend cancellation here: Windows may already be shutting
|
||||||
|
// down, and starting another IPC/Git operation is precisely what we avoid.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleAppShutdown() {
|
||||||
|
if (appShuttingDown) return;
|
||||||
|
appShuttingDown = true;
|
||||||
|
beginFrontendShutdown();
|
||||||
|
if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = undefined; }
|
||||||
|
if (backgroundRepoStatusTimer) { clearInterval(backgroundRepoStatusTimer); backgroundRepoStatusTimer = undefined; }
|
||||||
|
if (backgroundFetchTimer) { clearInterval(backgroundFetchTimer); backgroundFetchTimer = undefined; }
|
||||||
|
if (commitAiPollTimer) { clearInterval(commitAiPollTimer); commitAiPollTimer = undefined; }
|
||||||
|
}
|
||||||
|
|
||||||
function wait(ms: number): Promise<void> {
|
function wait(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -528,6 +564,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startBackgroundTimers() {
|
function startBackgroundTimers() {
|
||||||
|
if (appShuttingDown) return;
|
||||||
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
||||||
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
||||||
backgroundFetchTimer = setInterval(() => { void backgroundFetchTick(); }, BACKGROUND_FETCH_INTERVAL);
|
backgroundFetchTimer = setInterval(() => { void backgroundFetchTick(); }, BACKGROUND_FETCH_INTERVAL);
|
||||||
@@ -544,16 +581,15 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await waitForStartupPaint();
|
await waitForStartupPaint();
|
||||||
await Promise.race([
|
|
||||||
fetchOpenRepositoriesDuringStartup(),
|
|
||||||
wait(STARTUP_FETCH_MAX_WAIT_MS),
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
const remainingSplashTime = STARTUP_SPLASH_MIN_VISIBLE_MS - (performance.now() - startupStartedAt);
|
const remainingSplashTime = STARTUP_SPLASH_MIN_VISIBLE_MS - (performance.now() - startupStartedAt);
|
||||||
if (remainingSplashTime > 0) await wait(remainingSplashTime);
|
if (remainingSplashTime > 0) await wait(remainingSplashTime);
|
||||||
|
|
||||||
await closeStartupSplashscreen();
|
await closeStartupSplashscreen();
|
||||||
startBackgroundTimers();
|
startBackgroundTimers();
|
||||||
|
// Remote access can take seconds (offline networks, SSH negotiation,
|
||||||
|
// credential helpers). It must never hold the startup screen hostage.
|
||||||
|
void fetchOpenRepositoriesDuringStartup();
|
||||||
void backgroundRepoStatusTick(false);
|
void backgroundRepoStatusTick(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,7 +666,7 @@
|
|||||||
// Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/
|
// Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/
|
||||||
// Push buttons instead, not as a background popup.
|
// Push buttons instead, not as a background popup.
|
||||||
async function backgroundFetchTick() {
|
async function backgroundFetchTick() {
|
||||||
if (!autoRefreshEnabled) return;
|
if (appShuttingDown || !autoRefreshEnabled) return;
|
||||||
|
|
||||||
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
|
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
|
||||||
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
|
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
|
||||||
@@ -650,7 +686,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function backgroundFetchRepo(path: string) {
|
async function backgroundFetchRepo(path: string) {
|
||||||
if (!autoRefreshEnabled || !path || backgroundFetchInFlight) return;
|
if (appShuttingDown || !autoRefreshEnabled || !path || backgroundFetchInFlight) return;
|
||||||
|
|
||||||
backgroundFetchInFlight = true;
|
backgroundFetchInFlight = true;
|
||||||
try {
|
try {
|
||||||
@@ -700,7 +736,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function backgroundRepoStatusTick(fetchFirst: boolean) {
|
async function backgroundRepoStatusTick(fetchFirst: boolean) {
|
||||||
if (!autoRefreshEnabled || backgroundRepoStatusInFlight) return;
|
if (appShuttingDown || !autoRefreshEnabled || backgroundRepoStatusInFlight) return;
|
||||||
const others = knownRepoPathsForBackground();
|
const others = knownRepoPathsForBackground();
|
||||||
if (others.length === 0) return;
|
if (others.length === 0) return;
|
||||||
|
|
||||||
@@ -711,6 +747,7 @@
|
|||||||
// still never running more than one `git fetch` subprocess at a time.
|
// still never running more than one `git fetch` subprocess at a time.
|
||||||
const batchSize = fetchFirst ? BACKGROUND_FETCH_OTHER_BATCH_SIZE : BACKGROUND_REPO_STATUS_BATCH_SIZE;
|
const batchSize = fetchFirst ? BACKGROUND_FETCH_OTHER_BATCH_SIZE : BACKGROUND_REPO_STATUS_BATCH_SIZE;
|
||||||
for (let step = 0; step < Math.min(batchSize, others.length); step++) {
|
for (let step = 0; step < Math.min(batchSize, others.length); step++) {
|
||||||
|
if (appShuttingDown) break;
|
||||||
if (backgroundRepoStatusIndex >= others.length) backgroundRepoStatusIndex = 0;
|
if (backgroundRepoStatusIndex >= others.length) backgroundRepoStatusIndex = 0;
|
||||||
const path = others[backgroundRepoStatusIndex];
|
const path = others[backgroundRepoStatusIndex];
|
||||||
backgroundRepoStatusIndex += 1;
|
backgroundRepoStatusIndex += 1;
|
||||||
@@ -729,7 +766,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function autoRefreshTick() {
|
async function autoRefreshTick() {
|
||||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
||||||
const path = activeRepoPath;
|
const path = activeRepoPath;
|
||||||
autoRefreshInFlight = true;
|
autoRefreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
@@ -740,23 +777,14 @@
|
|||||||
// now-active repo's name and data with this one's.
|
// now-active repo's name and data with this one's.
|
||||||
if (!sameRepoPath(path, activeRepoPath)) return;
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
|
if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
|
||||||
applyStatus(nextStatus);
|
|
||||||
// Something changed — reload branches, commits and files in one bundled call.
|
// Something changed — reload branches, commits and files in one bundled call.
|
||||||
const bundle = await openRepositoryBundle(path, Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1);
|
const bundle = await openRepositoryBundle(path, Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1);
|
||||||
if (!sameRepoPath(path, activeRepoPath)) return;
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
const previousHeadHash = lastFileHistoryHeadHash;
|
await applyRepositoryBundle(path, bundle);
|
||||||
await refreshBranchList(path, bundle.branches);
|
|
||||||
await refreshTags(path, bundle.tags);
|
|
||||||
await refreshStashes(path, bundle.stashes);
|
|
||||||
await refreshCommitHistory(path, bundle.commits);
|
|
||||||
await refreshExplorerFiles(path, bundle.files);
|
|
||||||
// File history reflects `git log`, which only changes when HEAD actually moves
|
// File history reflects `git log`, which only changes when HEAD actually moves
|
||||||
// (new commit, checkout, merge, ...) — skip the reload otherwise so a plain
|
// (new commit, checkout, merge, ...) — skip the reload otherwise so a plain
|
||||||
// working-tree/status change (staging, edits) doesn't keep re-fetching and
|
// working-tree/status change (staging, edits) doesn't keep re-fetching and
|
||||||
// flickering the currently viewed file's history.
|
// flickering the currently viewed file's history.
|
||||||
if (lastFileHistoryHeadHash !== previousHeadHash && sameRepoPath(path, activeRepoPath)) {
|
|
||||||
await refreshFileHistory(path);
|
|
||||||
}
|
|
||||||
} catch { /* ignore transient errors */ } finally {
|
} catch { /* ignore transient errors */ } finally {
|
||||||
autoRefreshInFlight = false;
|
autoRefreshInFlight = false;
|
||||||
}
|
}
|
||||||
@@ -1089,10 +1117,7 @@
|
|||||||
aiCommitSplitOpen = false;
|
aiCommitSplitOpen = false;
|
||||||
aiCommitPlan = null;
|
aiCommitPlan = null;
|
||||||
commitMessage = "";
|
commitMessage = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("ai_commit_split_applied", { groups: plan.groups.length, files: allFiles.length });
|
trackEvent("ai_commit_split_applied", { groups: plan.groups.length, files: allFiles.length });
|
||||||
});
|
});
|
||||||
commitAiSplitting = false;
|
commitAiSplitting = false;
|
||||||
@@ -1870,15 +1895,21 @@
|
|||||||
// ── Refresh helpers ────────────────────────────────────────────────────────
|
// ── Refresh helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function refreshBranchList(path = activeRepoPath, prefetched?: GitBranchInfo[]) {
|
async function refreshBranchList(path = activeRepoPath, prefetched?: GitBranchInfo[]) {
|
||||||
branches = prefetched ?? (await listBranches(path));
|
const nextBranches = prefetched ?? (await listBranches(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
branches = nextBranches;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTags(path = activeRepoPath, prefetched?: GitTag[]) {
|
async function refreshTags(path = activeRepoPath, prefetched?: GitTag[]) {
|
||||||
tags = prefetched ?? (await listTags(path));
|
const nextTags = prefetched ?? (await listTags(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
tags = nextTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStashes(path = activeRepoPath, prefetched?: GitStash[]) {
|
async function refreshStashes(path = activeRepoPath, prefetched?: GitStash[]) {
|
||||||
stashes = prefetched ?? (await listStashes(path));
|
const nextStashes = prefetched ?? (await listStashes(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
stashes = nextStashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
|
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
|
||||||
@@ -1935,7 +1966,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshExplorerFiles(path = activeRepoPath, prefetched?: GitRepositoryFile[]) {
|
async function refreshExplorerFiles(path = activeRepoPath, prefetched?: GitRepositoryFile[]) {
|
||||||
repoFiles = prefetched ?? (await listRepositoryFiles(path));
|
const nextFiles = prefetched ?? (await listRepositoryFiles(path));
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
repoFiles = nextFiles;
|
||||||
const folderPaths = allExplorerFolderPaths(repoFiles);
|
const folderPaths = allExplorerFolderPaths(repoFiles);
|
||||||
expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder)));
|
expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder)));
|
||||||
if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
|
if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
|
||||||
@@ -1945,11 +1978,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshRepositoryViews(
|
||||||
|
path = activeRepoPath,
|
||||||
|
options: RepositoryRefreshOptions = {},
|
||||||
|
) {
|
||||||
|
const tasks: Promise<void>[] = [];
|
||||||
|
if (options.branches ?? true) tasks.push(refreshBranchList(path));
|
||||||
|
if (options.tags ?? false) tasks.push(refreshTags(path));
|
||||||
|
if (options.stashes ?? false) tasks.push(refreshStashes(path));
|
||||||
|
if (options.commits ?? true) tasks.push(refreshCommitHistory(path));
|
||||||
|
if (options.files ?? true) tasks.push(refreshExplorerFiles(path));
|
||||||
|
|
||||||
|
// These reads do not depend on each other. Starting them together removes
|
||||||
|
// several IPC/Git-process waterfalls after every user operation.
|
||||||
|
await Promise.all(tasks);
|
||||||
|
|
||||||
|
// Explorer refresh may invalidate the selected path, so file history runs
|
||||||
|
// after the parallel group rather than racing a disappearing selection.
|
||||||
|
if (options.fileHistory ?? true) await refreshFileHistory(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyRepositoryBundle(
|
||||||
|
path: string,
|
||||||
|
bundle: RepositoryBundle,
|
||||||
|
forceFileHistory = false,
|
||||||
|
) {
|
||||||
|
const previousHeadHash = lastFileHistoryHeadHash;
|
||||||
|
applyStatus(bundle.status);
|
||||||
|
const resolvedPath = activeRepoPath || path;
|
||||||
|
await Promise.all([
|
||||||
|
refreshBranchList(resolvedPath, bundle.branches),
|
||||||
|
refreshTags(resolvedPath, bundle.tags),
|
||||||
|
refreshStashes(resolvedPath, bundle.stashes),
|
||||||
|
refreshCommitHistory(resolvedPath, bundle.commits),
|
||||||
|
refreshExplorerFiles(resolvedPath, bundle.files),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (forceFileHistory || lastFileHistoryHeadHash !== previousHeadHash) {
|
||||||
|
await refreshFileHistory(resolvedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshRepositorySnapshot(path = activeRepoPath, forceFileHistory = false) {
|
||||||
|
const bundle = await openRepositoryBundle(
|
||||||
|
path,
|
||||||
|
Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1,
|
||||||
|
);
|
||||||
|
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||||
|
await applyRepositoryBundle(path, bundle, forceFileHistory);
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshRefsAndCommitGraph(path = activeRepoPath) {
|
async function refreshRefsAndCommitGraph(path = activeRepoPath) {
|
||||||
const previousHeadHash = lastFileHistoryHeadHash;
|
const previousHeadHash = lastFileHistoryHeadHash;
|
||||||
await refreshBranchList(path);
|
await Promise.all([
|
||||||
await refreshTags(path);
|
refreshBranchList(path),
|
||||||
await refreshCommitHistory(path);
|
refreshTags(path),
|
||||||
|
refreshCommitHistory(path),
|
||||||
|
]);
|
||||||
if (lastFileHistoryHeadHash !== previousHeadHash && fileHistoryDialogOpen) {
|
if (lastFileHistoryHeadHash !== previousHeadHash && fileHistoryDialogOpen) {
|
||||||
await refreshFileHistory(path);
|
await refreshFileHistory(path);
|
||||||
}
|
}
|
||||||
@@ -2025,14 +2110,9 @@
|
|||||||
const bundle = await openRepositoryBundle(path, COMMIT_HISTORY_PAGE_SIZE + 1);
|
const bundle = await openRepositoryBundle(path, COMMIT_HISTORY_PAGE_SIZE + 1);
|
||||||
if (requestId !== repoOpenRequestId) return;
|
if (requestId !== repoOpenRequestId) return;
|
||||||
resetRepositoryState(false);
|
resetRepositoryState(false);
|
||||||
applyStatus(bundle.status);
|
await applyRepositoryBundle(path, bundle);
|
||||||
if (globalSearchBusy) void cancelGlobalSearch();
|
if (globalSearchBusy) void cancelGlobalSearch();
|
||||||
activeView = "repository";
|
activeView = "repository";
|
||||||
await refreshBranchList(activeRepoPath, bundle.branches);
|
|
||||||
await refreshTags(activeRepoPath, bundle.tags);
|
|
||||||
await refreshStashes(activeRepoPath, bundle.stashes);
|
|
||||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
|
||||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
|
||||||
lastRepoSwitchAt = Date.now();
|
lastRepoSwitchAt = Date.now();
|
||||||
trackEvent("repository_opened", {
|
trackEvent("repository_opened", {
|
||||||
changed_files: bundle.status.files.length,
|
changed_files: bundle.status.files.length,
|
||||||
@@ -2099,14 +2179,9 @@
|
|||||||
COMMIT_HISTORY_PAGE_SIZE + 1,
|
COMMIT_HISTORY_PAGE_SIZE + 1,
|
||||||
);
|
);
|
||||||
resetRepositoryState(false);
|
resetRepositoryState(false);
|
||||||
applyStatus(bundle.status);
|
await applyRepositoryBundle(bundle.status.repo_path, bundle);
|
||||||
if (globalSearchBusy) void cancelGlobalSearch();
|
if (globalSearchBusy) void cancelGlobalSearch();
|
||||||
activeView = "repository";
|
activeView = "repository";
|
||||||
await refreshBranchList(activeRepoPath, bundle.branches);
|
|
||||||
await refreshTags(activeRepoPath, bundle.tags);
|
|
||||||
await refreshStashes(activeRepoPath, bundle.stashes);
|
|
||||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
|
||||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
|
||||||
cloneDialogOpen = false;
|
cloneDialogOpen = false;
|
||||||
pendingClone = null;
|
pendingClone = null;
|
||||||
if (credDialogAction === "clone") {
|
if (credDialogAction === "clone") {
|
||||||
@@ -2325,12 +2400,7 @@
|
|||||||
async function refreshRepo() {
|
async function refreshRepo() {
|
||||||
if (!activeRepoPath) { await openRepo(); return; }
|
if (!activeRepoPath) { await openRepo(); return; }
|
||||||
await runOperation("Refreshing", async () => {
|
await runOperation("Refreshing", async () => {
|
||||||
applyStatus(await getStatus(activeRepoPath));
|
await refreshRepositorySnapshot(activeRepoPath, true);
|
||||||
await refreshBranchList(activeRepoPath);
|
|
||||||
await refreshStashes(activeRepoPath);
|
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_refreshed", {
|
trackEvent("repository_refreshed", {
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
});
|
});
|
||||||
@@ -2341,10 +2411,7 @@
|
|||||||
if (!activeRepoPath || branch.current) return;
|
if (!activeRepoPath || branch.current) return;
|
||||||
await runOperation(`Checking out ${branch.name}`, async () => {
|
await runOperation(`Checking out ${branch.name}`, async () => {
|
||||||
applyStatus(await checkoutBranch(activeRepoPath, branch.name));
|
applyStatus(await checkoutBranch(activeRepoPath, branch.name));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_checked_out", {
|
trackEvent("branch_checked_out", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2356,10 +2423,7 @@
|
|||||||
if (!activeRepoPath || !name) return;
|
if (!activeRepoPath || !name) return;
|
||||||
await runOperation(`Creating ${name}`, async () => {
|
await runOperation(`Creating ${name}`, async () => {
|
||||||
applyStatus(await createBranch(activeRepoPath, name));
|
applyStatus(await createBranch(activeRepoPath, name));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_created");
|
trackEvent("branch_created");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2378,10 +2442,7 @@
|
|||||||
await runOperation(`Renaming ${branch.name}`, async () => {
|
await runOperation(`Renaming ${branch.name}`, async () => {
|
||||||
applyStatus(await renameBranch(activeRepoPath, branch.name, name));
|
applyStatus(await renameBranch(activeRepoPath, branch.name, name));
|
||||||
renameBranchTarget = null;
|
renameBranchTarget = null;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_renamed");
|
trackEvent("branch_renamed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2429,10 +2490,7 @@
|
|||||||
applyStatus(await deleteBranch(activeRepoPath, branch.name, deleteBranchForce));
|
applyStatus(await deleteBranch(activeRepoPath, branch.name, deleteBranchForce));
|
||||||
deleteBranchTarget = null;
|
deleteBranchTarget = null;
|
||||||
deleteBranchForce = false;
|
deleteBranchForce = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_deleted", {
|
trackEvent("branch_deleted", {
|
||||||
force: forceDelete ? 1 : 0,
|
force: forceDelete ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2608,10 +2666,7 @@
|
|||||||
await runOperation(`Creating ${name}`, async () => {
|
await runOperation(`Creating ${name}`, async () => {
|
||||||
applyStatus(await createBranch(activeRepoPath, name, target.hash));
|
applyStatus(await createBranch(activeRepoPath, name, target.hash));
|
||||||
newBranchCommit = null;
|
newBranchCommit = null;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_created_from_commit");
|
trackEvent("branch_created_from_commit");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2623,10 +2678,7 @@
|
|||||||
if (!["default", "squash", "ff-only", "no-ff"].includes(strategy)) { errorMessage = "Unknown merge strategy."; return; }
|
if (!["default", "squash", "ff-only", "no-ff"].includes(strategy)) { errorMessage = "Unknown merge strategy."; return; }
|
||||||
await runOperation(`Merging ${branch.name}`, async () => {
|
await runOperation(`Merging ${branch.name}`, async () => {
|
||||||
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy as import("./lib/types").MergeStrategy));
|
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy as import("./lib/types").MergeStrategy));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_merged", {
|
trackEvent("branch_merged", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2637,10 +2689,7 @@
|
|||||||
if (!activeRepoPath || branch.current || rebaseInProgress || cherryPickInProgress) return;
|
if (!activeRepoPath || branch.current || rebaseInProgress || cherryPickInProgress) return;
|
||||||
await runOperation(`Rebasing onto ${branch.name}`, async () => {
|
await runOperation(`Rebasing onto ${branch.name}`, async () => {
|
||||||
applyStatus(await rebaseBranch(activeRepoPath, branch.name));
|
applyStatus(await rebaseBranch(activeRepoPath, branch.name));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("branch_rebased", {
|
trackEvent("branch_rebased", {
|
||||||
remote: branch.remote ? 1 : 0,
|
remote: branch.remote ? 1 : 0,
|
||||||
});
|
});
|
||||||
@@ -2651,10 +2700,7 @@
|
|||||||
if (!activeRepoPath || !rebaseInProgress || hasConflicts) return;
|
if (!activeRepoPath || !rebaseInProgress || hasConflicts) return;
|
||||||
await runOperation("Continuing rebase", async () => {
|
await runOperation("Continuing rebase", async () => {
|
||||||
applyStatus(await rebaseContinue(activeRepoPath));
|
applyStatus(await rebaseContinue(activeRepoPath));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("rebase_continued");
|
trackEvent("rebase_continued");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2670,10 +2716,7 @@
|
|||||||
resolveDialogOpen = false;
|
resolveDialogOpen = false;
|
||||||
conflict = null;
|
conflict = null;
|
||||||
conflictTarget = "";
|
conflictTarget = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("rebase_aborted");
|
trackEvent("rebase_aborted");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2717,10 +2760,7 @@
|
|||||||
await runOperation("Starting interactive rebase", async () => {
|
await runOperation("Starting interactive rebase", async () => {
|
||||||
applyStatus(await startInteractiveRebase(activeRepoPath, interactiveRebaseBase, plan));
|
applyStatus(await startInteractiveRebase(activeRepoPath, interactiveRebaseBase, plan));
|
||||||
interactiveRebaseOpen = false;
|
interactiveRebaseOpen = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("interactive_rebase_started", { commits: plan.length });
|
trackEvent("interactive_rebase_started", { commits: plan.length });
|
||||||
});
|
});
|
||||||
if (interactiveRebaseOpen && errorMessage) interactiveRebaseError = errorMessage;
|
if (interactiveRebaseOpen && errorMessage) interactiveRebaseError = errorMessage;
|
||||||
@@ -2762,10 +2802,7 @@
|
|||||||
await runOperation("Restoring reflog entry", async () => {
|
await runOperation("Restoring reflog entry", async () => {
|
||||||
applyStatus(await restoreReflogEntry(activeRepoPath, entry.hash, branch.trim()));
|
applyStatus(await restoreReflogEntry(activeRepoPath, entry.hash, branch.trim()));
|
||||||
reflogOpen = false;
|
reflogOpen = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("reflog_recovered");
|
trackEvent("reflog_recovered");
|
||||||
});
|
});
|
||||||
if (reflogOpen && errorMessage) reflogError = errorMessage;
|
if (reflogOpen && errorMessage) reflogError = errorMessage;
|
||||||
@@ -2818,10 +2855,7 @@
|
|||||||
if (!activeRepoPath || rebaseInProgress || cherryPickInProgress) return;
|
if (!activeRepoPath || rebaseInProgress || cherryPickInProgress) return;
|
||||||
await runOperation(`Cherry-picking ${commit.short_hash}`, async () => {
|
await runOperation(`Cherry-picking ${commit.short_hash}`, async () => {
|
||||||
applyStatus(await cherryPickCommit(activeRepoPath, commit.hash));
|
applyStatus(await cherryPickCommit(activeRepoPath, commit.hash));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_cherry_picked");
|
trackEvent("commit_cherry_picked");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2830,10 +2864,7 @@
|
|||||||
if (!activeRepoPath || !cherryPickInProgress || hasConflicts) return;
|
if (!activeRepoPath || !cherryPickInProgress || hasConflicts) return;
|
||||||
await runOperation("Continuing cherry-pick", async () => {
|
await runOperation("Continuing cherry-pick", async () => {
|
||||||
applyStatus(await cherryPickContinue(activeRepoPath));
|
applyStatus(await cherryPickContinue(activeRepoPath));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("cherry_pick_continued");
|
trackEvent("cherry_pick_continued");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2849,10 +2880,7 @@
|
|||||||
resolveDialogOpen = false;
|
resolveDialogOpen = false;
|
||||||
conflict = null;
|
conflict = null;
|
||||||
conflictTarget = "";
|
conflictTarget = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("cherry_pick_aborted");
|
trackEvent("cherry_pick_aborted");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2928,10 +2956,7 @@
|
|||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
await runOperation("Pulling", async () => {
|
await runOperation("Pulling", async () => {
|
||||||
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_pulled", {
|
trackEvent("repository_pulled", {
|
||||||
from_stored_credential: fromStore ? 1 : 0,
|
from_stored_credential: fromStore ? 1 : 0,
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
@@ -2970,9 +2995,7 @@
|
|||||||
await runOperation("Pushing", async () => {
|
await runOperation("Pushing", async () => {
|
||||||
applyStatus(await push(activeRepoPath, username, password, remoteActionForceWithLease, selectedRemote || undefined));
|
applyStatus(await push(activeRepoPath, username, password, remoteActionForceWithLease, selectedRemote || undefined));
|
||||||
remoteActionForceWithLease = false;
|
remoteActionForceWithLease = false;
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { files: false });
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_pushed", {
|
trackEvent("repository_pushed", {
|
||||||
from_stored_credential: fromStore ? 1 : 0,
|
from_stored_credential: fromStore ? 1 : 0,
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
@@ -2996,10 +3019,7 @@
|
|||||||
|
|
||||||
await runOperation("Pulling before push", async () => {
|
await runOperation("Pulling before push", async () => {
|
||||||
applyStatus(await pull(activeRepoPath, username, password));
|
applyStatus(await pull(activeRepoPath, username, password));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
@@ -3016,9 +3036,7 @@
|
|||||||
|
|
||||||
await runOperation("Pushing after pull", async () => {
|
await runOperation("Pushing after pull", async () => {
|
||||||
applyStatus(await push(activeRepoPath, username, password));
|
applyStatus(await push(activeRepoPath, username, password));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { files: false });
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("repository_pushed_after_pull", {
|
trackEvent("repository_pushed_after_pull", {
|
||||||
from_stored_credential: fromStore ? 1 : 0,
|
from_stored_credential: fromStore ? 1 : 0,
|
||||||
changed_files: status?.files.length ?? 0,
|
changed_files: status?.files.length ?? 0,
|
||||||
@@ -3112,18 +3130,24 @@
|
|||||||
if (!activeRepoPath || !window.confirm(`Revert commit ${commit.short_hash} (${commit.summary}) with a new commit?`)) return;
|
if (!activeRepoPath || !window.confirm(`Revert commit ${commit.short_hash} (${commit.summary}) with a new commit?`)) return;
|
||||||
await runOperation(`Reverting ${commit.short_hash}`, async () => {
|
await runOperation(`Reverting ${commit.short_hash}`, async () => {
|
||||||
applyStatus(await revertCommit(activeRepoPath, commit.hash));
|
applyStatus(await revertCommit(activeRepoPath, commit.hash));
|
||||||
await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); await refreshFileHistory(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function continueMerge() {
|
async function continueMerge() {
|
||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation("Continuing merge", async () => { applyStatus(await mergeContinue(activeRepoPath)); await refreshCommitHistory(activeRepoPath); });
|
await runOperation("Continuing merge", async () => {
|
||||||
|
applyStatus(await mergeContinue(activeRepoPath));
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function abortMerge() {
|
async function abortMerge() {
|
||||||
if (!activeRepoPath || !window.confirm("Abort the current merge and restore the pre-merge state?")) return;
|
if (!activeRepoPath || !window.confirm("Abort the current merge and restore the pre-merge state?")) return;
|
||||||
await runOperation("Aborting merge", async () => { applyStatus(await mergeAbort(activeRepoPath)); await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); });
|
await runOperation("Aborting merge", async () => {
|
||||||
|
applyStatus(await mergeAbort(activeRepoPath));
|
||||||
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPruneRepo() {
|
async function fetchPruneRepo() {
|
||||||
@@ -3180,9 +3204,11 @@
|
|||||||
const stashedFiles = changedFiles.length;
|
const stashedFiles = changedFiles.length;
|
||||||
await runOperation("Stashing changes", async () => {
|
await runOperation("Stashing changes", async () => {
|
||||||
applyStatus(await stashPush(activeRepoPath, message, includeUntracked));
|
applyStatus(await stashPush(activeRepoPath, message, includeUntracked));
|
||||||
await refreshStashes(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, {
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
branches: false,
|
||||||
await refreshFileHistory(activeRepoPath);
|
stashes: true,
|
||||||
|
commits: false,
|
||||||
|
});
|
||||||
trackEvent("stash_saved", {
|
trackEvent("stash_saved", {
|
||||||
include_untracked: includeUntracked ? 1 : 0,
|
include_untracked: includeUntracked ? 1 : 0,
|
||||||
changed_files: stashedFiles,
|
changed_files: stashedFiles,
|
||||||
@@ -3194,9 +3220,11 @@
|
|||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation(`Applying ${stash.selector}`, async () => {
|
await runOperation(`Applying ${stash.selector}`, async () => {
|
||||||
applyStatus(await stashApply(activeRepoPath, stash.selector));
|
applyStatus(await stashApply(activeRepoPath, stash.selector));
|
||||||
await refreshStashes(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, {
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
branches: false,
|
||||||
await refreshFileHistory(activeRepoPath);
|
stashes: true,
|
||||||
|
commits: false,
|
||||||
|
});
|
||||||
trackEvent("stash_applied");
|
trackEvent("stash_applied");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3205,9 +3233,11 @@
|
|||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation(`Popping ${stash.selector}`, async () => {
|
await runOperation(`Popping ${stash.selector}`, async () => {
|
||||||
applyStatus(await stashPop(activeRepoPath, stash.selector));
|
applyStatus(await stashPop(activeRepoPath, stash.selector));
|
||||||
await refreshStashes(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, {
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
branches: false,
|
||||||
await refreshFileHistory(activeRepoPath);
|
stashes: true,
|
||||||
|
commits: false,
|
||||||
|
});
|
||||||
trackEvent("stash_popped");
|
trackEvent("stash_popped");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3278,8 +3308,7 @@
|
|||||||
const paths = files.map((file) => file.path);
|
const paths = files.map((file) => file.path);
|
||||||
await runOperation(files.length === 1 ? `Discarding ${baseName(files[0].path)}` : `Discarding ${files.length} files`, async () => {
|
await runOperation(files.length === 1 ? `Discarding ${baseName(files[0].path)}` : `Discarding ${files.length} files`, async () => {
|
||||||
applyStatus(await restoreFiles(activeRepoPath, paths, staged));
|
applyStatus(await restoreFiles(activeRepoPath, paths, staged));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("file_discarded", {
|
trackEvent("file_discarded", {
|
||||||
files: files.length,
|
files: files.length,
|
||||||
staged: staged ? 1 : 0,
|
staged: staged ? 1 : 0,
|
||||||
@@ -3298,8 +3327,7 @@
|
|||||||
if (stagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, stagedPaths, true);
|
if (stagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, stagedPaths, true);
|
||||||
if (unstagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, unstagedPaths, false);
|
if (unstagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, unstagedPaths, false);
|
||||||
if (nextStatus) applyStatus(nextStatus);
|
if (nextStatus) applyStatus(nextStatus);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("file_discarded", {
|
trackEvent("file_discarded", {
|
||||||
files: files.length,
|
files: files.length,
|
||||||
staged: 2,
|
staged: 2,
|
||||||
@@ -3400,8 +3428,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
applyStatus(await applyFilePatch(activeRepoPath, file.path, patch, action));
|
applyStatus(await applyFilePatch(activeRepoPath, file.path, patch, action));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
|
|
||||||
const updatedPatch = await getFilePatch(activeRepoPath, file.path, staged);
|
const updatedPatch = await getFilePatch(activeRepoPath, file.path, staged);
|
||||||
if (updatedPatch.trim()) {
|
if (updatedPatch.trim()) {
|
||||||
@@ -3507,10 +3534,7 @@
|
|||||||
amendMode = false;
|
amendMode = false;
|
||||||
preAmendDraftMessage = "";
|
preAmendDraftMessage = "";
|
||||||
lastLocalAiGeneratedMessage = "";
|
lastLocalAiGeneratedMessage = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_created", { amend: 1 });
|
trackEvent("commit_created", { amend: 1 });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -3521,10 +3545,7 @@
|
|||||||
applyStatus(await commit(activeRepoPath, message));
|
applyStatus(await commit(activeRepoPath, message));
|
||||||
commitMessage = "";
|
commitMessage = "";
|
||||||
lastLocalAiGeneratedMessage = "";
|
lastLocalAiGeneratedMessage = "";
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
|
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3566,10 +3587,7 @@
|
|||||||
commitMessage = preAmendDraftMessage;
|
commitMessage = preAmendDraftMessage;
|
||||||
preAmendDraftMessage = "";
|
preAmendDraftMessage = "";
|
||||||
}
|
}
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_undone");
|
trackEvent("commit_undone");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3582,10 +3600,7 @@
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
await runOperation(`Restoring ${target.short_hash}`, async () => {
|
await runOperation(`Restoring ${target.short_hash}`, async () => {
|
||||||
applyStatus(await restoreToCommit(activeRepoPath, target.hash));
|
applyStatus(await restoreToCommit(activeRepoPath, target.hash));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_restored");
|
trackEvent("commit_restored");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3596,8 +3611,7 @@
|
|||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
await runOperation(`Restoring ${file.path}`, async () => {
|
await runOperation(`Restoring ${file.path}`, async () => {
|
||||||
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, file.path));
|
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, file.path));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("commit_file_restored");
|
trackEvent("commit_file_restored");
|
||||||
});
|
});
|
||||||
return !errorMessage;
|
return !errorMessage;
|
||||||
@@ -3742,8 +3756,7 @@
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
await runOperation(`Restoring ${selectedExplorerPath}`, async () => {
|
await runOperation(`Restoring ${selectedExplorerPath}`, async () => {
|
||||||
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, selectedExplorerPath));
|
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, selectedExplorerPath));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
trackEvent("selected_file_restored_from_commit", {
|
trackEvent("selected_file_restored_from_commit", {
|
||||||
kind,
|
kind,
|
||||||
});
|
});
|
||||||
@@ -3935,8 +3948,7 @@
|
|||||||
}
|
}
|
||||||
preparedResolutions = {};
|
preparedResolutions = {};
|
||||||
if (nextStatus) applyStatus(nextStatus);
|
if (nextStatus) applyStatus(nextStatus);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshRepositoryViews(activeRepoPath, { branches: false, commits: false });
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
|
|
||||||
const remaining = (nextStatus?.files ?? status?.files ?? []).filter(
|
const remaining = (nextStatus?.files ?? status?.files ?? []).filter(
|
||||||
(f) => f.staged === "conflicted" || f.unstaged === "conflicted",
|
(f) => f.staged === "conflicted" || f.unstaged === "conflicted",
|
||||||
|
|||||||
+3
-4
@@ -2689,9 +2689,8 @@
|
|||||||
filter: drop-shadow(0 0 3px rgba(224,160,64,0.24));
|
filter: drop-shadow(0 0 3px rgba(224,160,64,0.24));
|
||||||
}
|
}
|
||||||
.graph-svg path.graph-segment-behind {
|
.graph-svg path.graph-segment-behind {
|
||||||
stroke: #7aacff;
|
|
||||||
stroke-dasharray: 4 4;
|
stroke-dasharray: 4 4;
|
||||||
filter: drop-shadow(0 0 3px rgba(122,172,255,0.24));
|
filter: drop-shadow(0 0 3px rgba(122,172,255,0.2));
|
||||||
}
|
}
|
||||||
.graph-dot {
|
.graph-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -2715,8 +2714,8 @@
|
|||||||
box-shadow: 0 0 0 1px rgba(224,160,64,0.42), 0 0 10px rgba(224,160,64,0.16);
|
box-shadow: 0 0 0 1px rgba(224,160,64,0.42), 0 0 10px rgba(224,160,64,0.16);
|
||||||
}
|
}
|
||||||
.graph-dot.behind {
|
.graph-dot.behind {
|
||||||
background: #7aacff;
|
background: var(--dot-color, #5a8cf8);
|
||||||
box-shadow: 0 0 0 1px rgba(122,172,255,0.46), 0 0 10px rgba(122,172,255,0.18);
|
box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8), 0 0 10px rgba(122,172,255,0.18);
|
||||||
}
|
}
|
||||||
.graph-dot.tip { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8); }
|
.graph-dot.tip { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8); }
|
||||||
.graph-dot.merge {
|
.graph-dot.merge {
|
||||||
|
|||||||
@@ -140,15 +140,18 @@
|
|||||||
|
|
||||||
const currentBranches = branchMembership.get(commit.hash) ?? localBranchRefs(commit);
|
const currentBranches = branchMembership.get(commit.hash) ?? localBranchRefs(commit);
|
||||||
const commitBranches = uniqueStrings([...(beforeBranches[col] ?? []), ...currentBranches]);
|
const commitBranches = uniqueStrings([...(beforeBranches[col] ?? []), ...currentBranches]);
|
||||||
after[col] = commit.parents.length > 0 ? commit.parents[0] : null;
|
const firstParent = commit.parents[0] ?? null;
|
||||||
afterBranches[col] = after[col] ? commitBranches.slice() : [];
|
after[col] = firstParent;
|
||||||
|
afterBranches[col] = firstParent
|
||||||
|
? (branchMembership.get(firstParent) ?? commitBranches).slice()
|
||||||
|
: [];
|
||||||
|
|
||||||
const fromCommit = new Set<number>([col]);
|
const fromCommit = new Set<number>([col]);
|
||||||
for (let p = 1; p < commit.parents.length; p++) {
|
for (let p = 1; p < commit.parents.length; p++) {
|
||||||
let slot = after.indexOf(null);
|
let slot = after.indexOf(null);
|
||||||
if (slot === -1) { slot = after.length; after.push(null); afterBranches.push([]); }
|
if (slot === -1) { slot = after.length; after.push(null); afterBranches.push([]); }
|
||||||
after[slot] = commit.parents[p];
|
after[slot] = commit.parents[p];
|
||||||
afterBranches[slot] = [];
|
afterBranches[slot] = (branchMembership.get(commit.parents[p]) ?? commitBranches).slice();
|
||||||
fromCommit.add(slot);
|
fromCommit.add(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ type TelemetryLevel = "info" | "warn" | "error";
|
|||||||
const MAX_MESSAGE_LENGTH = 2_048;
|
const MAX_MESSAGE_LENGTH = 2_048;
|
||||||
let telemetryEnabled = false;
|
let telemetryEnabled = false;
|
||||||
let configuration: Promise<unknown> = Promise.resolve();
|
let configuration: Promise<unknown> = Promise.resolve();
|
||||||
|
let frontendShuttingDown = false;
|
||||||
|
|
||||||
|
export function beginFrontendShutdown() {
|
||||||
|
frontendShuttingDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Telemetry must never contain repository contents or identifying local data.
|
// Telemetry must never contain repository contents or identifying local data.
|
||||||
function sanitize(message: string): string {
|
function sanitize(message: string): string {
|
||||||
@@ -42,6 +47,9 @@ function randomHex(bytes: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function tracedInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
export async function tracedInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||||
|
if (frontendShuttingDown) {
|
||||||
|
throw new Error("Application is shutting down");
|
||||||
|
}
|
||||||
if (!telemetryEnabled) return invoke<T>(command, args);
|
if (!telemetryEnabled) return invoke<T>(command, args);
|
||||||
const startedAtMs = Date.now();
|
const startedAtMs = Date.now();
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user