feat(git): support credentials for remote branch rename
Allow remote branch renames to be performed with optional credentials so operations against protected remotes succeed when authentication is needed. The backend accepts username/password and uses an authenticated push path when provided, while the frontend prompts for and reuses stored credentials. - Add optional username/password to rename RPC and use authenticated push - Wire UI to queue rename, open credential dialog, and execute rename - Extend credential dialog and handling to include the rename action
This commit is contained in:
+42
-10
@@ -188,7 +188,7 @@
|
||||
|
||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||
type AppView = "management" | "repository";
|
||||
type CredentialAction = "push" | "pull" | "fetch" | "clone";
|
||||
type CredentialAction = "push" | "pull" | "fetch" | "clone" | "rename";
|
||||
type CredentialMode = "credentials" | "token";
|
||||
type PendingDiscard =
|
||||
| { kind: "file"; files: GitFileStatus[]; staged: boolean }
|
||||
@@ -410,6 +410,7 @@
|
||||
let autoRefreshInFlight = false;
|
||||
let credDialogOpen = false;
|
||||
let credDialogAction: CredentialAction | null = null;
|
||||
let pendingRemoteRename: { remote: string; oldBranch: string; newBranch: string } | null = null;
|
||||
let credDialogError = "";
|
||||
let credDialogKey: string | null = null;
|
||||
let credDialogUsername = "";
|
||||
@@ -2622,12 +2623,14 @@
|
||||
const oldRemoteBranch = branch.name.slice(slash + 1);
|
||||
if (name === oldRemoteBranch) return;
|
||||
|
||||
await runOperation(`Renaming ${branch.name} on remote`, async () => {
|
||||
applyStatus(await renameRemoteBranch(activeRepoPath, remote, oldRemoteBranch, name));
|
||||
renameBranchTarget = null;
|
||||
await refreshRefsAndCommitGraph(activeRepoPath);
|
||||
trackEvent("branch_renamed", { remote: 1 });
|
||||
});
|
||||
pendingRemoteRename = { remote, oldBranch: oldRemoteBranch, newBranch: name };
|
||||
const key = await currentCredKey("rename");
|
||||
const stored = await loadStoredCredential(key);
|
||||
if (stored && (!key || !rejectedCredentialKeys.has(key))) {
|
||||
await doActualRemoteRename(stored.username, stored.password, key, true, credentialModeFor(stored));
|
||||
} else {
|
||||
await openCredentialDialog("rename", key, stored);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3231,10 +3234,11 @@
|
||||
|
||||
// Resolve the keychain key (host/org) from the exact remote URL used by the
|
||||
// operation. Push URLs may intentionally differ from fetch URLs.
|
||||
async function currentCredKey(action: "push" | "pull" | "fetch" = "fetch"): Promise<string | null> {
|
||||
async function currentCredKey(action: "push" | "pull" | "fetch" | "rename" = "fetch"): Promise<string | null> {
|
||||
if (!activeRepoPath) return null;
|
||||
try {
|
||||
const url = await getRemoteUrl(activeRepoPath, selectedRemote || undefined, action === "push");
|
||||
const remote = action === "rename" ? pendingRemoteRename?.remote : selectedRemote;
|
||||
const url = await getRemoteUrl(activeRepoPath, remote || undefined, action === "push" || action === "rename");
|
||||
return url ? orgKeyFromUrl(url) : null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -3273,7 +3277,7 @@
|
||||
// so a temporary 401/403 cannot erase a valid token; the key is only skipped
|
||||
// for the rest of this session until the user replaces it successfully.
|
||||
function handleRemoteResult(
|
||||
action: "push" | "pull" | "fetch",
|
||||
action: "push" | "pull" | "fetch" | "rename",
|
||||
key: string | null,
|
||||
fromStore: boolean,
|
||||
username: string,
|
||||
@@ -3414,6 +3418,33 @@
|
||||
handleRemoteResult("push", key, fromStore, username, mode);
|
||||
}
|
||||
|
||||
async function doActualRemoteRename(
|
||||
username: string,
|
||||
password: string,
|
||||
key: string | null,
|
||||
fromStore: boolean,
|
||||
mode: CredentialMode,
|
||||
) {
|
||||
const rename = pendingRemoteRename;
|
||||
if (!activeRepoPath || !rename) return;
|
||||
errorMessage = "";
|
||||
await runOperation(`Renaming ${rename.remote}/${rename.oldBranch} on remote`, async () => {
|
||||
applyStatus(await renameRemoteBranch(
|
||||
activeRepoPath,
|
||||
rename.remote,
|
||||
rename.oldBranch,
|
||||
rename.newBranch,
|
||||
username,
|
||||
password,
|
||||
));
|
||||
renameBranchTarget = null;
|
||||
pendingRemoteRename = null;
|
||||
await refreshRefsAndCommitGraph(activeRepoPath);
|
||||
trackEvent("branch_renamed", { remote: 1 });
|
||||
});
|
||||
handleRemoteResult("rename", key, fromStore, username, mode);
|
||||
}
|
||||
|
||||
async function handleCredentialSubmit(
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -3436,6 +3467,7 @@
|
||||
if (credDialogAction === "pull") await doActualPull(username, password, key, false, mode);
|
||||
else if (credDialogAction === "push") await doActualPush(username, password, key, false, mode);
|
||||
else if (credDialogAction === "fetch") await doActualFetch(username, password, key, false, mode);
|
||||
else if (credDialogAction === "rename") await doActualRemoteRename(username, password, key, false, mode);
|
||||
else if (credDialogAction === "clone" && pendingClone) {
|
||||
await cloneRepo(
|
||||
pendingClone.remoteUrl,
|
||||
|
||||
Reference in New Issue
Block a user