feat(git): add opt-in for unrelated histories during pull
Allow pulling repositories with unrelated commit histories when the user explicitly opts in. The pull argument construction was refactored and branch resolution made more robust so the backend can include the --allow-unrelated-histories flag when requested. - Extract pull argument logic and add support for allowing unrelated histories. - Prompt users in the UI to confirm merging separate histories and retry pull. - Restyle and improve the update toast UI for better layout and responsiveness.
This commit is contained in:
+150
-21
@@ -2593,32 +2593,23 @@ pub async fn pull(
|
||||
strategy: Option<String>,
|
||||
remote: Option<String>,
|
||||
branch: Option<String>,
|
||||
allow_unrelated_histories: Option<bool>,
|
||||
) -> Result<GitStatus, String> {
|
||||
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
let strategy = strategy.as_deref().unwrap_or("merge");
|
||||
let remote = remote
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let mut pull_args = vec![OsString::from("pull")];
|
||||
match strategy {
|
||||
"merge" => {
|
||||
pull_args.extend([OsString::from("--no-rebase"), OsString::from("--no-edit")])
|
||||
}
|
||||
"rebase" => pull_args.push(OsString::from("--rebase")),
|
||||
"ff-only" => pull_args.push(OsString::from("--ff-only")),
|
||||
_ => return Err("Unknown pull strategy.".to_string()),
|
||||
}
|
||||
if let Some(remote_name) = remote.as_deref() {
|
||||
validate_remote_name(&repo, remote_name, true)?;
|
||||
pull_args.push(remote_name.into());
|
||||
if let Some(branch) = branch
|
||||
.map(|v| v.trim().to_string())
|
||||
.filter(|v| !v.is_empty())
|
||||
{
|
||||
pull_args.push(branch.into());
|
||||
}
|
||||
}
|
||||
let branch = branch
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let pull_args = pull_args_for_repo(
|
||||
&repo,
|
||||
strategy.as_deref().unwrap_or("merge"),
|
||||
remote.as_deref(),
|
||||
branch.as_deref(),
|
||||
allow_unrelated_histories.unwrap_or(false),
|
||||
)?;
|
||||
let output = match (username.as_deref(), password.as_deref()) {
|
||||
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
|
||||
run_git_authenticated_output(&repo, pull_args.clone(), u, p)?
|
||||
@@ -2657,6 +2648,39 @@ pub async fn pull(
|
||||
.map_err(|err| format!("Could not pull: {err}"))?
|
||||
}
|
||||
|
||||
fn pull_args_for_repo(
|
||||
repo: &Path,
|
||||
strategy: &str,
|
||||
remote: Option<&str>,
|
||||
branch: Option<&str>,
|
||||
allow_unrelated_histories: bool,
|
||||
) -> Result<Vec<OsString>, String> {
|
||||
let mut pull_args = vec![OsString::from("pull")];
|
||||
match strategy {
|
||||
"merge" => {
|
||||
pull_args.extend([OsString::from("--no-rebase"), OsString::from("--no-edit")]);
|
||||
if allow_unrelated_histories {
|
||||
pull_args.push(OsString::from("--allow-unrelated-histories"));
|
||||
}
|
||||
}
|
||||
"rebase" => pull_args.push(OsString::from("--rebase")),
|
||||
"ff-only" => pull_args.push(OsString::from("--ff-only")),
|
||||
_ => return Err("Unknown pull strategy.".to_string()),
|
||||
}
|
||||
if let Some(remote_name) = remote.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
validate_remote_name(repo, remote_name, true)?;
|
||||
pull_args.push(remote_name.into());
|
||||
let branch = branch
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| current_branch_name(repo))?;
|
||||
pull_args.push(branch.into());
|
||||
}
|
||||
Ok(pull_args)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch(
|
||||
path: String,
|
||||
@@ -2949,7 +2973,9 @@ fn first_remote_name(repo: &Path) -> Option<String> {
|
||||
}
|
||||
|
||||
fn current_branch_name(repo: &Path) -> Result<String, String> {
|
||||
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"])?;
|
||||
// `symbolic-ref` also works before the first commit, while
|
||||
// `rev-parse --abbrev-ref HEAD` fails for an unborn HEAD.
|
||||
let branch = run_git(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"])?;
|
||||
let branch = String::from_utf8_lossy(&branch).trim().to_string();
|
||||
if branch.is_empty() || branch == "HEAD" {
|
||||
return Err("Could not determine current branch.".to_string());
|
||||
@@ -8515,6 +8541,108 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_with_selected_remote_infers_current_branch() {
|
||||
let repo = init_temp_repo("pull_selected_remote");
|
||||
let branch = git_output_test(&repo.path, ["symbolic-ref", "--short", "HEAD"]);
|
||||
run_git_test(&repo.path, ["remote", "add", "origin", "."]);
|
||||
|
||||
let args = pull_args_for_repo(&repo.path, "merge", Some("origin"), None, false)
|
||||
.expect("pull arguments should be created");
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
OsString::from("pull"),
|
||||
OsString::from("--no-rebase"),
|
||||
OsString::from("--no-edit"),
|
||||
OsString::from("origin"),
|
||||
OsString::from(branch),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_can_explicitly_allow_unrelated_histories_for_merge() {
|
||||
let repo = init_temp_repo("pull_unrelated_histories");
|
||||
let branch = git_output_test(&repo.path, ["symbolic-ref", "--short", "HEAD"]);
|
||||
run_git_test(&repo.path, ["remote", "add", "origin", "."]);
|
||||
|
||||
let args = pull_args_for_repo(&repo.path, "merge", Some("origin"), None, true)
|
||||
.expect("pull arguments should be created");
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
OsString::from("pull"),
|
||||
OsString::from("--no-rebase"),
|
||||
OsString::from("--no-edit"),
|
||||
OsString::from("--allow-unrelated-histories"),
|
||||
OsString::from("origin"),
|
||||
OsString::from(branch),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Git for Windows can fail local pull tests with a sh signal pipe error"
|
||||
)]
|
||||
async fn pull_retries_unrelated_histories_only_after_explicit_opt_in() {
|
||||
let remote = init_temp_repo("pull_unrelated_remote");
|
||||
fs::write(remote.path.join("remote.txt"), "remote history\n")
|
||||
.expect("remote file should be written");
|
||||
run_git_test(&remote.path, ["add", "remote.txt"]);
|
||||
run_git_test(&remote.path, ["commit", "-q", "-m", "remote init"]);
|
||||
let remote_branch = git_output_test(&remote.path, ["branch", "--show-current"]);
|
||||
|
||||
let local = init_temp_repo("pull_unrelated_local");
|
||||
fs::write(local.path.join("local.txt"), "local history\n")
|
||||
.expect("local file should be written");
|
||||
run_git_test(&local.path, ["add", "local.txt"]);
|
||||
run_git_test(&local.path, ["commit", "-q", "-m", "local init"]);
|
||||
run_git_test(
|
||||
&local.path,
|
||||
["remote", "add", "origin", remote.path.to_str().unwrap()],
|
||||
);
|
||||
|
||||
let error = pull(
|
||||
local.path.to_string_lossy().to_string(),
|
||||
None,
|
||||
None,
|
||||
Some("merge".to_string()),
|
||||
Some("origin".to_string()),
|
||||
Some(remote_branch.clone()),
|
||||
Some(false),
|
||||
)
|
||||
.await
|
||||
.expect_err("unrelated histories should require explicit opt-in");
|
||||
assert!(error.contains("refusing to merge unrelated histories"));
|
||||
|
||||
let status = pull(
|
||||
local.path.to_string_lossy().to_string(),
|
||||
None,
|
||||
None,
|
||||
Some("merge".to_string()),
|
||||
Some("origin".to_string()),
|
||||
Some(remote_branch),
|
||||
Some(true),
|
||||
)
|
||||
.await
|
||||
.expect("explicitly allowed histories should merge");
|
||||
|
||||
assert!(status.clean, "{:?}", status.files);
|
||||
assert!(local.path.join("local.txt").exists());
|
||||
assert!(local.path.join("remote.txt").exists());
|
||||
let parent_count =
|
||||
git_output_test(&local.path, ["rev-list", "--parents", "-n", "1", "HEAD"])
|
||||
.split_whitespace()
|
||||
.count()
|
||||
- 1;
|
||||
assert_eq!(parent_count, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
@@ -8557,6 +8685,7 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
+53
-8
@@ -2161,6 +2161,10 @@
|
||||
|| value.includes("fetch first");
|
||||
}
|
||||
|
||||
function isUnrelatedHistoriesError(message: string): boolean {
|
||||
return message.toLowerCase().includes("refusing to merge unrelated histories");
|
||||
}
|
||||
|
||||
function statusHasConflicts(value: GitStatus | null): boolean {
|
||||
return (value?.files ?? []).some((file) => file.staged === "conflicted" || file.unstaged === "conflicted");
|
||||
}
|
||||
@@ -3650,17 +3654,61 @@
|
||||
mode: CredentialMode,
|
||||
) {
|
||||
errorMessage = "";
|
||||
await runOperation("Pulling", async () => {
|
||||
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
const pulled = await pullWithUnrelatedHistoryConfirmation(username, password, "Pulling");
|
||||
if (pulled) {
|
||||
trackEvent("repository_pulled", {
|
||||
from_stored_credential: fromStore ? 1 : 0,
|
||||
changed_files: status?.files.length ?? 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
handleRemoteResult("pull", key, fromStore, username, mode);
|
||||
}
|
||||
|
||||
async function pullWithUnrelatedHistoryConfirmation(
|
||||
username: string,
|
||||
password: string,
|
||||
label: string,
|
||||
): Promise<boolean> {
|
||||
await runOperation(label, async () => {
|
||||
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
});
|
||||
|
||||
if (!errorMessage || !isUnrelatedHistoriesError(errorMessage)) return !errorMessage;
|
||||
|
||||
if (pullStrategy !== "merge") {
|
||||
errorMessage = appLanguage === "de"
|
||||
? "Lokales und entferntes Repository haben unabhängige Historien. Wähle in den Sync-Einstellungen die Merge-Strategie, um sie zusammenzuführen."
|
||||
: "The local and remote repositories have unrelated histories. Choose the Merge strategy in Sync settings to combine them.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = "";
|
||||
const confirmed = window.confirm(appLanguage === "de"
|
||||
? "Das lokale und das entfernte Repository besitzen getrennte Commit-Historien.\n\nTrotzdem zusammenführen? Dabei können Merge-Konflikte entstehen."
|
||||
: "The local and remote repositories have separate commit histories.\n\nMerge them anyway? This may produce merge conflicts.");
|
||||
if (!confirmed) {
|
||||
errorMessage = appLanguage === "de"
|
||||
? "Pull abgebrochen: Die getrennten Historien wurden nicht verändert."
|
||||
: "Pull cancelled: the separate histories were left unchanged.";
|
||||
return false;
|
||||
}
|
||||
|
||||
await runOperation(appLanguage === "de" ? "Historien zusammenführen" : "Merging histories", async () => {
|
||||
applyStatus(await pull(
|
||||
activeRepoPath,
|
||||
username,
|
||||
password,
|
||||
pullStrategy,
|
||||
selectedRemote || undefined,
|
||||
undefined,
|
||||
true,
|
||||
));
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
});
|
||||
return !errorMessage;
|
||||
}
|
||||
|
||||
async function doActualFetch(
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -3715,10 +3763,7 @@
|
||||
|
||||
if (!fromStore) credDialogError = "";
|
||||
|
||||
await runOperation("Pulling before push", async () => {
|
||||
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
});
|
||||
await pullWithUnrelatedHistoryConfirmation(username, password, "Pulling before push");
|
||||
|
||||
if (errorMessage) {
|
||||
handleRemoteResult("pull", key, fromStore, username, mode);
|
||||
|
||||
+83
-77
@@ -1486,73 +1486,61 @@
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
z-index: 70;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
width: min(430px, calc(100vw - 32px));
|
||||
padding: 14px;
|
||||
width: min(410px, calc(100vw - 28px));
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(100, 108, 255, 0.42);
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-top: 2px solid var(--color-accent);
|
||||
color: var(--color-ink);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(100,108,255,0.22), rgba(189,52,254,0.12) 42%, rgba(65,209,255,0.08)),
|
||||
rgba(12, 13, 24, 0.96);
|
||||
box-shadow: 0 24px 72px rgba(0,0,0,0.48), 0 0 0 1px rgba(255,255,255,0.04) inset;
|
||||
backdrop-filter: blur(18px);
|
||||
background: var(--app-dialog-bg);
|
||||
box-shadow: 0 18px 48px rgba(0,0,0,0.42);
|
||||
animation: update-toast-in 180ms cubic-bezier(.2,.8,.2,1) both;
|
||||
}
|
||||
.update-toast.error {
|
||||
border-color: rgba(232,96,96,0.45);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(232,96,96,0.16), rgba(100,108,255,0.11)),
|
||||
rgba(12, 13, 24, 0.96);
|
||||
border-top-color: var(--code-delete-strong);
|
||||
}
|
||||
.update-toast.installed {
|
||||
border-color: rgba(78,202,118,0.38);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(78,202,118,0.15), rgba(65,209,255,0.1), rgba(100,108,255,0.12)),
|
||||
rgba(12, 13, 24, 0.96);
|
||||
border-top-color: var(--code-add-strong);
|
||||
}
|
||||
|
||||
.update-toast-glow {
|
||||
position: absolute;
|
||||
inset: auto 18px -46px auto;
|
||||
width: 170px;
|
||||
height: 95px;
|
||||
border-radius: 999px;
|
||||
background: rgba(65,209,255,0.18);
|
||||
filter: blur(34px);
|
||||
pointer-events: none;
|
||||
.update-toast-header {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 58px;
|
||||
padding: 10px 11px;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background: var(--app-dialog-chrome);
|
||||
}
|
||||
|
||||
.update-toast-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, rgba(65,209,255,0.28), rgba(100,108,255,0.54), rgba(189,52,254,0.42));
|
||||
box-shadow: 0 14px 32px rgba(100,108,255,0.22), inset 0 1px 0 rgba(255,255,255,0.16);
|
||||
border: 1px solid color-mix(in srgb, var(--color-accent) 32%, var(--color-border));
|
||||
color: var(--color-accent);
|
||||
background: color-mix(in srgb, var(--color-accent) 9%, var(--color-surface-raised));
|
||||
}
|
||||
.update-toast-icon.busy { color: #bfefff; }
|
||||
|
||||
.update-toast-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
.update-toast.error .update-toast-icon {
|
||||
border-color: color-mix(in srgb, var(--code-delete-strong) 34%, var(--color-border));
|
||||
color: var(--code-delete-strong);
|
||||
background: var(--code-delete-bg);
|
||||
}
|
||||
.update-toast.installed .update-toast-icon {
|
||||
border-color: color-mix(in srgb, var(--code-add-strong) 34%, var(--color-border));
|
||||
color: var(--code-add-strong);
|
||||
background: var(--code-add-bg);
|
||||
}
|
||||
.update-toast-icon.busy { color: var(--color-accent); }
|
||||
|
||||
.update-toast-top {
|
||||
.update-toast-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
background: var(--app-dialog-bg);
|
||||
}
|
||||
|
||||
.update-toast-copy { min-width: 0; }
|
||||
@@ -1561,55 +1549,54 @@
|
||||
overflow: hidden;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .035em;
|
||||
text-transform: uppercase;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.update-toast h2 {
|
||||
margin: 2px 0 0;
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
margin: 3px 0 0;
|
||||
color: var(--color-ink);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.update-toast p {
|
||||
margin: 0;
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 12.5px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.update-toast-close {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
padding: 0;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
border-color: var(--color-border-subtle);
|
||||
color: var(--color-ink-dim);
|
||||
background: rgba(255,255,255,0.04);
|
||||
background: transparent;
|
||||
}
|
||||
.update-toast-close:hover:not(:disabled) {
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
color: #ffffff;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-ink);
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.update-progress {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.09);
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
.update-progress span {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
min-width: 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #4db6d6, #6f8cff, #238eb4);
|
||||
background: var(--color-accent);
|
||||
transition: width 160ms ease;
|
||||
}
|
||||
.update-progress.indeterminate span {
|
||||
@@ -1627,29 +1614,48 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 7px;
|
||||
min-height: 49px;
|
||||
padding: 8px 11px;
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
background: var(--app-dialog-chrome);
|
||||
}
|
||||
.update-toast-primary,
|
||||
.update-toast-secondary {
|
||||
min-height: 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px;
|
||||
min-height: 31px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.update-toast-primary {
|
||||
border-color: rgba(111,140,255,0.72);
|
||||
border-color: var(--color-primary);
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #5f7df2, #238eb4);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
.update-toast-primary:hover:not(:disabled) {
|
||||
border-color: rgba(77,182,214,0.74);
|
||||
border-color: var(--color-primary-dark);
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #6f8cff, #2da0c7);
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.update-toast-secondary {
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-ink-muted);
|
||||
background: rgba(255,255,255,0.05);
|
||||
background: var(--app-button-bg);
|
||||
}
|
||||
|
||||
@keyframes update-toast-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.update-toast {
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
width: calc(100vw - 16px);
|
||||
}
|
||||
.update-toast-actions > button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Workspace layout --- */
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
);
|
||||
const versionLabel = $derived(
|
||||
version && currentVersion
|
||||
? `${currentVersion} -> ${version}`
|
||||
? `${currentVersion} → ${version}`
|
||||
: version
|
||||
? `Version ${version}`
|
||||
: "New version",
|
||||
@@ -69,34 +69,32 @@
|
||||
role={state === "error" ? "alert" : "status"}
|
||||
aria-live={state === "error" ? "assertive" : "polite"}
|
||||
>
|
||||
<div class="update-toast-glow" aria-hidden="true"></div>
|
||||
|
||||
<div class="update-toast-icon" class:busy={isBusy}>
|
||||
{#if state === "downloading"}
|
||||
<LoaderCircle class="spin" size={21} aria-hidden="true" />
|
||||
{:else if state === "installed"}
|
||||
<PackageCheck size={21} aria-hidden="true" />
|
||||
{:else if state === "error"}
|
||||
<AlertCircle size={21} aria-hidden="true" />
|
||||
{:else}
|
||||
<Sparkles size={21} aria-hidden="true" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="update-toast-content">
|
||||
<div class="update-toast-top">
|
||||
<div class="update-toast-copy">
|
||||
<span class="update-toast-kicker">{versionLabel}</span>
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
|
||||
{#if !isBusy}
|
||||
<button class="update-toast-close" type="button" onclick={onDismiss} title="Close" aria-label="Close">
|
||||
<X size={15} aria-hidden="true" />
|
||||
</button>
|
||||
<header class="update-toast-header">
|
||||
<div class="update-toast-icon" class:busy={isBusy}>
|
||||
{#if state === "downloading"}
|
||||
<LoaderCircle class="spin" size={18} aria-hidden="true" />
|
||||
{:else if state === "installed"}
|
||||
<PackageCheck size={18} aria-hidden="true" />
|
||||
{:else if state === "error"}
|
||||
<AlertCircle size={18} aria-hidden="true" />
|
||||
{:else}
|
||||
<Sparkles size={18} aria-hidden="true" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="update-toast-copy">
|
||||
<span class="update-toast-kicker">Gitty update · {versionLabel}</span>
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
|
||||
{#if !isBusy}
|
||||
<button class="update-toast-close" type="button" onclick={onDismiss} title="Close" aria-label="Close">
|
||||
<X size={15} aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="update-toast-body">
|
||||
<p>{description}</p>
|
||||
|
||||
{#if showProgress}
|
||||
@@ -115,33 +113,34 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="update-toast-actions">
|
||||
{#if state === "installed"}
|
||||
<button class="update-toast-secondary" type="button" onclick={onDismiss}>
|
||||
Got it
|
||||
</button>
|
||||
{:else if state === "error"}
|
||||
<button class="update-toast-secondary" type="button" onclick={onDismiss}>
|
||||
Close
|
||||
</button>
|
||||
<button class="update-toast-primary" type="button" onclick={onInstall}>
|
||||
<Download size={14} aria-hidden="true" />
|
||||
Try again
|
||||
</button>
|
||||
{:else}
|
||||
<button class="update-toast-secondary" type="button" onclick={onLater} disabled={isBusy}>
|
||||
Later
|
||||
</button>
|
||||
<button class="update-toast-primary" type="button" onclick={onInstall} disabled={isBusy}>
|
||||
{#if isBusy}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
Installing
|
||||
{:else}
|
||||
<Download size={14} aria-hidden="true" />
|
||||
Install
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="update-toast-actions">
|
||||
{#if state === "installed"}
|
||||
<button class="update-toast-secondary" type="button" onclick={onDismiss}>
|
||||
Got it
|
||||
</button>
|
||||
{:else if state === "error"}
|
||||
<button class="update-toast-secondary" type="button" onclick={onDismiss}>
|
||||
Close
|
||||
</button>
|
||||
<button class="update-toast-primary" type="button" onclick={onInstall}>
|
||||
<Download size={14} aria-hidden="true" />
|
||||
Try again
|
||||
</button>
|
||||
{:else}
|
||||
<button class="update-toast-secondary" type="button" onclick={onLater} disabled={isBusy}>
|
||||
Later
|
||||
</button>
|
||||
<button class="update-toast-primary" type="button" onclick={onInstall} disabled={isBusy}>
|
||||
{#if isBusy}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
Installing
|
||||
{:else}
|
||||
<Download size={14} aria-hidden="true" />
|
||||
Install
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
+18
-2
@@ -405,8 +405,24 @@ export function commitAiSplit(path: string, options: CommitAiGenerateOptions): P
|
||||
});
|
||||
}
|
||||
|
||||
export function pull(path: string, username?: string, password?: string, strategy: PullStrategy = "merge", remote?: string, branch?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("pull", { path, username: username ?? null, password: password ?? null, strategy, remote: remote || null, branch: branch || null });
|
||||
export function pull(
|
||||
path: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
strategy: PullStrategy = "merge",
|
||||
remote?: string,
|
||||
branch?: string,
|
||||
allowUnrelatedHistories = false,
|
||||
): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("pull", {
|
||||
path,
|
||||
username: username ?? null,
|
||||
password: password ?? null,
|
||||
strategy,
|
||||
remote: remote || null,
|
||||
branch: branch || null,
|
||||
allowUnrelatedHistories,
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchRemote(path: string, username?: string, password?: string, prune = false, remote?: string): Promise<GitStatus> {
|
||||
|
||||
Reference in New Issue
Block a user