feat(submodules): support auth and checkout submodule revisions
Add credential-aware submodule operations and a command to checkout a specific
tag or commit in a submodule without staging the parent repository.
- Backend (src-tauri):
- Export checkout_submodule_revision and implement checkout_revision which
validates tag vs commit inputs, verifies refs locally, and checks out the
submodule in detached mode without modifying the parent's index.
- Add optional username/password parameters to add_submodule and submodule_action
flows. Implement submodule_git to call run_git_authenticated when credentials
are supplied and classify auth failures by prefixing errors with "AUTH_FAILED:".
- Wire authenticated variants (operate_authenticated, add_authenticated) and
update fetch/update actions to use credentials where needed.
- Add unit tests covering authenticated submodule commands, auth failure
classification, and checkout-by-tag/commit behavior.
- Frontend:
- App.svelte: introduce credential prompt flow (withSubmoduleCredentials,
submit/cancel handlers), surface credential dialog on auth failures, and
wire credentialed calls for initialize/add/update/fetch operations. Hook up
checkoutSubmoduleRevision and listTags to the submodule dialog.
- SubmoduleDialog.svelte: add UI for selecting destination folder, loading
tags and checking out revisions; expose fetch action.
- CredentialDialog.svelte: include "submodule" action and adjust labels.
- Docs:
- README: document "Change commit or tag" and "Fetch tags & commits" behaviors.
The commit focuses only on enabling credentialed submodule interactions and
safe local checkouts of tags/commits; no other git behavior changes are made.
This commit is contained in:
+64
-4
@@ -55,6 +55,7 @@
|
||||
listSubmodules,
|
||||
addSubmodule,
|
||||
submoduleAction,
|
||||
checkoutSubmoduleRevision,
|
||||
checkoutBranch,
|
||||
cherryPickAbort,
|
||||
cherryPickCommit,
|
||||
@@ -440,6 +441,9 @@
|
||||
let submoduleNoticeRequest = 0;
|
||||
let pendingSubmoduleInitialization: { repoPath: string; modules: GitSubmodule[] } | null = null;
|
||||
let submoduleInitializationError = "";
|
||||
let submoduleAuthRequest: { url: string; key: string | null; credential: StoredCredential | null; resolve: (credential: StoredCredential | null) => void } | null = null;
|
||||
let submoduleAuthError = "";
|
||||
let submoduleAuthSaving = false;
|
||||
let submoduleDialogOpen = false;
|
||||
let submodules: GitSubmodule[] = [];
|
||||
let submodulesLoading = false;
|
||||
@@ -3379,6 +3383,53 @@
|
||||
deleteBranchForce = false;
|
||||
}
|
||||
|
||||
async function withSubmoduleCredentials(url: string, task: (username?: string, password?: string) => Promise<void>) {
|
||||
const key = orgKeyFromUrl(url);
|
||||
let credential = key && !rejectedCredentialKeys.has(key) ? await loadStoredCredential(key) : null;
|
||||
while (true) {
|
||||
try {
|
||||
await task(credential?.username, credential?.password);
|
||||
if (key) rejectedCredentialKeys.delete(key);
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = errorToMessage(error);
|
||||
if (!isAuthError(message)) throw error;
|
||||
if (credential && key) rejectedCredentialKeys.add(key);
|
||||
submoduleAuthError = stripAuthPrefix(message);
|
||||
const previous = credential;
|
||||
credential = await new Promise<StoredCredential | null>(resolve => {
|
||||
submoduleAuthRequest = { url, key, credential: previous, resolve };
|
||||
});
|
||||
if (!credential) throw new Error(appLanguage === "de" ? "Submodule-Anmeldung abgebrochen." : "Submodule sign-in cancelled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSubmoduleCredential(username: string, password: string, save: boolean, mode: CredentialMode) {
|
||||
const request = submoduleAuthRequest;
|
||||
if (!request || submoduleAuthSaving) return;
|
||||
submoduleAuthSaving = true;
|
||||
try {
|
||||
const credential = { username, password, mode };
|
||||
if (save && request.key) {
|
||||
await credSave(request.key, username, password, mode);
|
||||
credentialCache.set(request.key, credential);
|
||||
}
|
||||
submoduleAuthRequest = null;
|
||||
submoduleAuthError = "";
|
||||
request.resolve(credential);
|
||||
} catch (error) { submoduleAuthError = errorToMessage(error); }
|
||||
finally { submoduleAuthSaving = false; }
|
||||
}
|
||||
|
||||
function cancelSubmoduleCredential() {
|
||||
if (submoduleAuthSaving) return;
|
||||
const request = submoduleAuthRequest;
|
||||
submoduleAuthRequest = null;
|
||||
submoduleAuthError = "";
|
||||
request?.resolve(null);
|
||||
}
|
||||
|
||||
async function refreshSubmoduleNotice(path: string): Promise<GitSubmodule[] | null> {
|
||||
const request = ++submoduleNoticeRequest;
|
||||
try {
|
||||
@@ -3405,7 +3456,7 @@
|
||||
submoduleInitializationError = "";
|
||||
try {
|
||||
for (const module of pending.modules) {
|
||||
await submoduleAction(pending.repoPath, module.path, "initialize", true);
|
||||
await withSubmoduleCredentials(module.url, (username, password) => submoduleAction(pending.repoPath, module.path, "initialize", true, username, password));
|
||||
}
|
||||
pendingSubmoduleInitialization = null;
|
||||
} catch (error) {
|
||||
@@ -6764,12 +6815,14 @@
|
||||
{#if submoduleDialogOpen}
|
||||
{#await import("./lib/components/SubmoduleDialog.svelte") then module}
|
||||
<module.default
|
||||
modules={submodules} isLoading={submodulesLoading} {isBusy} error={submoduleError}
|
||||
repoPath={activeRepoPath} modules={submodules} isLoading={submodulesLoading} {isBusy} error={submoduleError}
|
||||
language={appLanguage} recursive={submoduleRecursive}
|
||||
onRecursive={(value) => { submoduleRecursive = value; void refreshSubmodules(); }}
|
||||
onRefresh={refreshSubmodules} onClose={closeSubmoduleDialog} onOpen={openSubmoduleTab}
|
||||
onAdd={(url, destination, branch) => runSubmoduleOperation(path => addSubmodule(path, url, destination, branch))}
|
||||
onAction={(selected, action) => runSubmoduleOperation(path => submoduleAction(path, selected.path, action, submoduleRecursive))}
|
||||
onLoadTags={(selected) => listTags(selected.full_path)}
|
||||
onCheckout={(selected, revision, kind) => runSubmoduleOperation(path => checkoutSubmoduleRevision(path, selected.path, revision, kind))}
|
||||
onAdd={(url, destination, branch) => runSubmoduleOperation(path => withSubmoduleCredentials(url, (username, password) => addSubmodule(path, url, destination, branch, username, password)))}
|
||||
onAction={(selected, action) => runSubmoduleOperation(path => (action === "update" || action === "fetch") ? withSubmoduleCredentials(selected.url, (username, password) => submoduleAction(path, selected.path, action, submoduleRecursive, username, password)) : submoduleAction(path, selected.path, action, submoduleRecursive))}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
@@ -6780,3 +6833,10 @@
|
||||
error={submoduleInitializationError} onInitialize={initializePendingSubmodules} onLater={dismissSubmoduleInitialization} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if submoduleAuthRequest}
|
||||
<CredentialDialog action="submodule" error={submoduleAuthError} isBusy={submoduleAuthSaving}
|
||||
initialUsername={submoduleAuthRequest.credential?.username ?? ""}
|
||||
initialMode={submoduleAuthRequest.credential ? credentialModeFor(submoduleAuthRequest.credential) : "credentials"}
|
||||
onSubmit={submitSubmoduleCredential} onCancel={cancelSubmoduleCredential} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user