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}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
} from "@lucide/svelte";
|
||||
|
||||
interface Props {
|
||||
action: "push" | "pull" | "fetch" | "clone" | "rename" | "delete";
|
||||
action: "push" | "pull" | "fetch" | "clone" | "rename" | "delete" | "submodule";
|
||||
error: string;
|
||||
isBusy: boolean;
|
||||
initialUsername?: string;
|
||||
@@ -47,9 +47,11 @@
|
||||
password.trim().length > 0 &&
|
||||
username.trim().length > 0,
|
||||
);
|
||||
let actionLabel = $derived(action === "push" ? "Push" : action === "fetch" ? "Fetch" : action === "clone" ? "Clone" : action === "rename" ? "Rename" : action === "delete" ? "Delete" : "Pull");
|
||||
let actionLabel = $derived(action === "submodule" ? "Submodule" : action === "push" ? "Push" : action === "fetch" ? "Fetch" : action === "clone" ? "Clone" : action === "rename" ? "Rename" : action === "delete" ? "Delete" : "Pull");
|
||||
let actionTitle = $derived(
|
||||
action === "push"
|
||||
action === "submodule"
|
||||
? "Authenticate submodule"
|
||||
: action === "push"
|
||||
? "Authenticate push"
|
||||
: action === "rename"
|
||||
? "Authenticate remote rename"
|
||||
|
||||
@@ -1,22 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { Boxes, Plus, RefreshCw, X, ExternalLink, GitCommitHorizontal, LoaderCircle } from "@lucide/svelte";
|
||||
import type { GitSubmodule } from "../types";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { FolderOpen, Boxes, Plus, RefreshCw, X, ExternalLink, GitCommitHorizontal, LoaderCircle } from "@lucide/svelte";
|
||||
import type { GitSubmodule, GitTag } from "../types";
|
||||
interface Props {
|
||||
repoPath: string;
|
||||
modules: GitSubmodule[]; isLoading: boolean; isBusy: boolean; error: string;
|
||||
language: "de" | "en"; recursive: boolean;
|
||||
onRecursive: (value: boolean) => void;
|
||||
onRefresh: () => void; onClose: () => void;
|
||||
onAdd: (url: string, path: string, branch: string) => Promise<boolean>;
|
||||
onAction: (module: GitSubmodule, action: "update" | "stage" | "sync") => Promise<boolean>;
|
||||
onAction: (module: GitSubmodule, action: "update" | "stage" | "sync" | "fetch") => Promise<boolean>;
|
||||
onLoadTags: (module: GitSubmodule) => Promise<GitTag[]>;
|
||||
onCheckout: (module: GitSubmodule, revision: string, kind: "tag" | "commit") => Promise<boolean>;
|
||||
onOpen: (module: GitSubmodule) => void;
|
||||
}
|
||||
let { modules, isLoading, isBusy, error, language, recursive, onRecursive, onRefresh, onClose, onAdd, onAction, onOpen }: Props = $props();
|
||||
let { repoPath, modules, isLoading, isBusy, error, language, recursive, onRecursive, onRefresh, onClose, onAdd, onAction, onOpen, onLoadTags, onCheckout }: Props = $props();
|
||||
const t = (de: string, en: string) => language === "de" ? de : en;
|
||||
let selectedPath = $state("");
|
||||
let adding = $state(false);
|
||||
let url = $state("");
|
||||
let destination = $state("");
|
||||
let parentFolder = $state("");
|
||||
let folderName = $state("");
|
||||
let folderNameEdited = $state(false);
|
||||
let browseError = $state("");
|
||||
let browsing = $state(false);
|
||||
const suggestedName = $derived((url.trim().split(/[?#]/, 1)[0]?.replace(/[\\/]+$/g, "").split(/[\\/:]/).filter(Boolean).pop() ?? "").replace(/\.git$/i, ""));
|
||||
const effectiveName = $derived(folderNameEdited ? folderName.trim() : suggestedName);
|
||||
const relativeParent = $derived(parentFolder.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, ""));
|
||||
const destination = $derived([relativeParent === "." ? "" : relativeParent, effectiveName].filter(Boolean).join("/"));
|
||||
const validDestination = $derived(Boolean(effectiveName) && !/[\\/:<>"|?*\x00-\x1f]/.test(effectiveName) && ![".", ".."].includes(effectiveName) && !effectiveName.startsWith("-") && !relativeParent.startsWith("/") && !relativeParent.includes(":") && relativeParent.split("/").every(part => part !== ".." && !part.startsWith("-")));
|
||||
async function chooseSubmoduleFolder() {
|
||||
browseError = ""; browsing = true;
|
||||
try {
|
||||
const chosen = await openDialog({ title: t("Zielordner im Repository auswählen", "Select destination inside repository"), directory: true, multiple: false, defaultPath: repoPath });
|
||||
if (typeof chosen !== "string") return;
|
||||
const root = repoPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const folder = chosen.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const windows = /^[a-z]:/i.test(root) || root.startsWith("//");
|
||||
const compareRoot = windows ? root.toLowerCase() : root;
|
||||
const compareFolder = windows ? folder.toLowerCase() : folder;
|
||||
if (compareFolder !== compareRoot && !compareFolder.startsWith(compareRoot + "/")) {
|
||||
browseError = t("Bitte einen Ordner innerhalb des Hauptrepositorys auswählen.", "Choose a folder inside the parent repository.");
|
||||
return;
|
||||
}
|
||||
parentFolder = folder.slice(root.length).replace(/^\//, "");
|
||||
} catch (error) { browseError = String(error); }
|
||||
finally { browsing = false; }
|
||||
}
|
||||
let branch = $state("");
|
||||
let revisionKind = $state<"tag" | "commit">("tag");
|
||||
let revision = $state("");
|
||||
let tags = $state<GitTag[]>([]);
|
||||
let tagsLoading = $state(false);
|
||||
let tagsError = $state("");
|
||||
let tagRequest = 0;
|
||||
async function loadTags(module: GitSubmodule) {
|
||||
const request = ++tagRequest;
|
||||
tagsLoading = true; tagsError = "";
|
||||
try { const loaded = await onLoadTags(module); if (request === tagRequest) tags = loaded; }
|
||||
catch (error) { if (request === tagRequest) tagsError = String(error); }
|
||||
finally { if (request === tagRequest) tagsLoading = false; }
|
||||
}
|
||||
$effect(() => {
|
||||
const module = selected;
|
||||
revision = ""; tags = []; tagsError = "";
|
||||
if (module?.local_commit) void loadTags(module);
|
||||
else { tagRequest++; tagsLoading = false; }
|
||||
return () => { tagRequest++; };
|
||||
});
|
||||
async function fetchRevisions() {
|
||||
const module = selected;
|
||||
if (module && await onAction(module, "fetch")) await loadTags(module);
|
||||
}
|
||||
let selected = $derived(modules.find(m => m.path === selectedPath) ?? modules[0]);
|
||||
let disabled = $derived(isBusy || isLoading);
|
||||
function statusLabel(m: GitSubmodule) {
|
||||
@@ -31,7 +86,7 @@
|
||||
(node.querySelector<HTMLElement>("button:not(:disabled)") ?? node).focus();
|
||||
const trap = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Tab") return;
|
||||
const controls = Array.from(node.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), [tabindex="0"]'));
|
||||
const controls = Array.from(node.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), select:not(:disabled), [tabindex="0"]'));
|
||||
const first = controls[0]; const last = controls[controls.length - 1];
|
||||
if (!first) { event.preventDefault(); return; }
|
||||
if (event.shiftKey && (document.activeElement === first || document.activeElement === node)) { event.preventDefault(); last.focus(); }
|
||||
@@ -42,8 +97,9 @@
|
||||
}
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (await onAdd(url.trim(), destination.trim(), branch.trim())) {
|
||||
adding = false; url = ""; destination = ""; branch = "";
|
||||
if (!validDestination || browsing) return;
|
||||
if (await onAdd(url.trim(), destination, branch.trim())) {
|
||||
adding = false; url = ""; parentFolder = ""; folderName = ""; folderNameEdited = false; branch = ""; browseError = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -67,12 +123,17 @@
|
||||
<form class="submodule-add" onsubmit={submit}>
|
||||
<h3>{t("Submodul hinzufügen", "Add submodule")}</h3>
|
||||
<label>{t("Repository-URL", "Repository URL")}<input bind:value={url} required disabled={disabled} placeholder="https://github.com/team/repository.git" /></label>
|
||||
<label for="submodule-parent">{t("Zielordner · relativ zum Repository", "Destination folder · relative to repository")}</label>
|
||||
<div class="submodule-folder-picker"><input id="submodule-parent" bind:value={parentFolder} disabled={disabled || browsing} placeholder={t(". (Repository-Hauptordner) oder libs", ". (repository root) or libs")} /><button class="btn-secondary" type="button" disabled={disabled || browsing} onclick={chooseSubmoduleFolder}><FolderOpen size={15} />{t("Durchsuchen", "Browse")}</button></div>
|
||||
{#if browseError}<div class="submodule-error" role="alert">{browseError}</div>{/if}
|
||||
<div class="submodule-fields">
|
||||
<label>{t("Pfad im Repository", "Path in repository")}<input bind:value={destination} required disabled={disabled} placeholder="libs/repository" /></label>
|
||||
<label>{t("Ordnername", "Folder name")}<input value={folderNameEdited ? folderName : suggestedName} oninput={event => { folderName = event.currentTarget.value; folderNameEdited = Boolean(folderName.trim()) && folderName !== suggestedName; }} disabled={disabled} placeholder={t("Wird aus der Repository-URL übernommen", "Taken from the repository URL")} /></label>
|
||||
<label>{t("Tracking-Branch · optional", "Tracking branch · optional")}<input bind:value={branch} disabled={disabled} placeholder={t("Standard-Branch", "Default branch")} /></label>
|
||||
</div>
|
||||
<p>{t("Zielpfad", "Destination")}: <code>{destination || "—"}</code></p>
|
||||
{#if destination && !validDestination}<p class="submodule-error" role="alert">{t("Bitte einen relativen Zielordner und einen gültigen Ordnernamen eingeben.", "Enter a relative destination folder and a valid folder name.")}</p>{/if}
|
||||
<p>{t("Die .gitmodules-Datei und der neue Verweis werden zum Commit vorgemerkt.", "The .gitmodules file and new reference will be staged for commit.")}</p>
|
||||
<div class="submodule-actions"><button class="btn-secondary" type="button" disabled={disabled} onclick={() => adding = false}>{t("Abbrechen", "Cancel")}</button><button class="btn-primary" disabled={disabled || !url.trim() || !destination.trim()}>{t("Submodul hinzufügen", "Add submodule")}</button></div>
|
||||
<div class="submodule-actions"><button class="btn-secondary" type="button" disabled={disabled} onclick={() => adding = false}>{t("Abbrechen", "Cancel")}</button><button class="btn-primary" disabled={disabled || browsing || !url.trim() || !validDestination}>{t("Submodul hinzufügen", "Add submodule")}</button></div>
|
||||
</form>
|
||||
{/if}
|
||||
{#if isLoading && !modules.length}
|
||||
@@ -100,6 +161,20 @@
|
||||
{:else if selected.local_commit !== selected.recorded_commit}{t("Der lokale Commit weicht vom gespeicherten Verweis ab. Checke den gespeicherten Stand aus oder stage den lokalen Verweis im übergeordneten Repository.", "The local commit differs from the recorded reference. Check out the recorded commit or stage the local reference in the parent repository.")}
|
||||
{:else}{t("Der lokale Stand entspricht dem gespeicherten Commit.", "The local state matches the recorded commit.")}{/if}
|
||||
</div>
|
||||
{#if selected.local_commit}
|
||||
<form class="submodule-revision" onsubmit={event => { event.preventDefault(); if (selected && revision.trim()) void onCheckout(selected, revision.trim(), revisionKind); }}>
|
||||
<div class="revision-heading"><strong>{t("Commit oder Tag wechseln", "Change commit or tag")}</strong><button class="btn-sm" type="button" disabled={disabled || tagsLoading} onclick={fetchRevisions}><RefreshCw size={13} />{t("Tags & Commits abrufen", "Fetch tags & commits")}</button></div>
|
||||
<label>{t("Auswahl", "Selection")}<select bind:value={revisionKind} onchange={() => revision = ""} disabled={disabled}><option value="tag">Tag</option><option value="commit">Commit</option></select></label>
|
||||
{#if revisionKind === "tag"}
|
||||
<label>Tag<select bind:value={revision} disabled={disabled || tagsLoading || !tags.length}><option value="">{tagsLoading ? t("Tags werden geladen…", "Loading tags…") : tags.length ? t("Tag auswählen", "Select tag") : t("Keine lokalen Tags", "No local tags")}</option>{#each tags as tag (tag.name)}<option value={tag.name}>{tag.name}</option>{/each}</select></label>
|
||||
{:else}
|
||||
<label>{t("Commit-Hash", "Commit hash")}<input bind:value={revision} disabled={disabled} placeholder="a81c2f4" spellcheck="false" required pattern={"[a-fA-F0-9]{4,64}"} /></label>
|
||||
{/if}
|
||||
{#if tagsError}<p class="submodule-error" role="alert">{tagsError}</p>{/if}
|
||||
<button class="btn-secondary" disabled={disabled || selected.dirty || selected.conflicted || !revision.trim() || (revisionKind === "tag" && tagsLoading)}>{t("Ausgewählten Stand auschecken", "Check out selected revision")}</button>
|
||||
<p>{t("Danach den neuen Verweis stagen und im übergeordneten Repository committen.", "Then stage the new reference and commit it in the parent repository.")}</p>
|
||||
</form>
|
||||
{/if}
|
||||
<div class="submodule-actions">
|
||||
<button class="btn-primary" disabled={disabled || selected.dirty || selected.conflicted} onclick={() => selected && onAction(selected, "update")}>{selected.local_commit ? t("Gespeicherten Stand auschecken", "Check out recorded commit") : t("Initialisieren", "Initialize")}</button>
|
||||
{#if selected.local_commit && selected.local_commit !== selected.recorded_commit}<button class="btn-secondary" disabled={disabled || selected.conflicted} onclick={() => selected && onAction(selected, "stage")}>{t("Verweis stagen", "Stage reference")}</button>{/if}
|
||||
@@ -135,6 +210,11 @@
|
||||
dd { margin: 0; display: flex; align-items: center; gap: 9px; flex-wrap: wrap; font-size: 12px; }
|
||||
dd span { margin-left: auto; color: var(--color-ink-muted); }
|
||||
.submodule-notice { padding: 12px; margin: 20px 0; background: var(--color-surface); border-radius: 6px; font-size: 12px; line-height: 1.6; color: var(--color-ink-muted); }
|
||||
.submodule-revision { border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); padding: 16px 0; margin-bottom: 16px; }
|
||||
.revision-heading { display: flex; justify-content: space-between; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.submodule-revision label { display: flex; flex-direction: column; gap: 6px; margin: 12px 0; font-size: 12px; }
|
||||
.submodule-revision input, .submodule-revision select { width: 100%; min-width: 0; }
|
||||
.submodule-revision p { font-size: 12px; color: var(--color-ink-muted); margin: 10px 0 0; line-height: 1.5; }
|
||||
.submodule-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.submodule-actions button { white-space: normal; }
|
||||
.submodule-footer { border-top: 1px solid var(--color-border); padding: 14px 20px; font-size: 11px; line-height: 1.5; color: var(--color-ink-muted); }
|
||||
@@ -144,6 +224,9 @@
|
||||
.submodule-add label { display: flex; flex-direction: column; gap: 7px; font-size: 12px; margin: 13px 0; min-width: 0; }
|
||||
.submodule-add input { width: 100%; min-width: 0; }
|
||||
.submodule-add p { font-size: 12px; color: var(--color-ink-muted); margin: 4px 0 16px; }
|
||||
.submodule-folder-picker { display: flex; gap: 8px; align-items: center; }
|
||||
.submodule-folder-picker input { flex: 1; }
|
||||
.submodule-folder-picker button { flex-shrink: 0; }
|
||||
.submodule-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@media (max-width: 650px) { .submodule-workspace, .submodule-fields { grid-template-columns: 1fr; } .submodule-list { border-right: 0; } .submodule-content { padding: 12px; } }
|
||||
</style>
|
||||
|
||||
+8
-4
@@ -737,9 +737,13 @@ export function pullRequestAiGenerate(path: string, remote: string, sourceBranch
|
||||
export function listSubmodules(path: string, recursive = true): Promise<GitSubmodule[]> {
|
||||
return invoke("list_submodules", { path, recursive });
|
||||
}
|
||||
export function addSubmodule(path: string, url: string, destination: string, branch?: string): Promise<void> {
|
||||
return invoke("add_submodule", { path, url, destination, branch: branch || null });
|
||||
export function addSubmodule(path: string, url: string, destination: string, branch?: string, username?: string, password?: string): Promise<void> {
|
||||
return invoke("add_submodule", { path, url, destination, branch: branch || null, username: username ?? null, password: password ?? null });
|
||||
}
|
||||
export function submoduleAction(path: string, modulePath: string, action: "update" | "stage" | "sync" | "initialize", recursive: boolean): Promise<void> {
|
||||
return invoke("submodule_action", { path, modulePath, action, recursive });
|
||||
export function submoduleAction(path: string, modulePath: string, action: "update" | "stage" | "sync" | "initialize" | "fetch", recursive: boolean, username?: string, password?: string): Promise<void> {
|
||||
return invoke("submodule_action", { path, modulePath, action, recursive, username: username ?? null, password: password ?? null });
|
||||
}
|
||||
|
||||
export function checkoutSubmoduleRevision(path: string, modulePath: string, revision: string, kind: "tag" | "commit"): Promise<void> {
|
||||
return invoke("checkout_submodule_revision", { path, modulePath, revision, kind });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user