Release ForgeFlow 0.8.9 Git Validator
This commit is contained in:
+100
-1
@@ -180,6 +180,7 @@ const ui = {
|
||||
servers: [],
|
||||
serverInspection: null,
|
||||
gitRecovery: null,
|
||||
gitValidation: null,
|
||||
diffHunks: null,
|
||||
conflictState: null,
|
||||
branchProtection: null,
|
||||
@@ -487,6 +488,7 @@ function selectRepository(id, shouldRender = true) {
|
||||
ui.branches = [];
|
||||
ui.stashes = [];
|
||||
ui.gitRecovery = null;
|
||||
ui.gitValidation = null;
|
||||
ui.branchProtection = null;
|
||||
const repository = selectedRepository();
|
||||
ui.selectedProfileId = selectedProfile(repository)?.id || null;
|
||||
@@ -968,6 +970,34 @@ function renderRepositorySettings(repository) {
|
||||
return `<div class="tab-page"><section class="settings-group"><h2>Repository identity</h2><div class="form-grid"><div class="field full"><label>Gitea repository</label><input class="input" value="${attr(repository.fullName)}" readonly/></div><div class="field full"><label>Local working tree</label><input class="input mono" value="${attr(repository.localPath || automaticTarget || "Not linked")}" readonly/></div><div class="field full"><label>Current origin</label><input class="input mono" value="${attr(currentOrigin)}" readonly/></div>${desiredOrigin ? `<div class="field full"><label>Current Gitea SSH origin</label><input class="input mono" value="${attr(desiredOrigin)}" readonly/></div>` : ""}</div><div class="card-actions"><button class="button" data-action="${repository.localPath ? "open-path" : "link-repo"}">${icon("folder")}${repository.localPath ? "Open project folder" : "Link local folder"}</button>${originNeedsRepair ? `<button class="button primary" data-action="repair-origin">${icon("link")}Use current Gitea origin</button>` : ""}${repository.localPath ? `<button class="button" data-action="scan-git-recovery">${icon("pulse")}Scan Git health</button><button class="button danger" data-action="unlink-repo">${icon("link")}Remove link</button>` : `<button class="button primary" data-action="clone-repo">${icon("cloud")}${escapeHtml(clonePrimaryLabel(repository))}</button><button class="button ghost" data-action="clone-repo-custom">Choose another location</button>`}</div></section><section class="settings-group"><div class="section-heading"><div><h2>Open pull requests</h2><span class="meta">Live from Gitea</span></div><button class="button" data-action="load-pull-requests">${icon("refresh")}Refresh</button></div>${pullRequests.length ? `<div class="tool-list">${pullRequests.map((pull) => `<div class="tool-row"><div><strong>#${pull.number} · ${escapeHtml(pull.title)}</strong><span>${escapeHtml(pull.head?.ref || pull.head?.label || "source")} → ${escapeHtml(pull.base?.ref || pull.base?.label || "target")} · ${formatDate(pull.updated_at || pull.created_at)}</span></div><button class="button" data-action="open-pull-request-url" data-url="${attr(pull.html_url || "")}">Open</button></div>`).join("")}</div>` : '<div class="empty-state compact"><p>No open pull requests.</p></div>'}</section><section class="settings-group"><h2>Repository behavior</h2><div class="notice">${icon("shield")}Origin repair changes only the Git remote URL. Git health scans the actual Git directory, repairs only proven stale lock files and never changes source files or commits.</div></section></div>`;
|
||||
}
|
||||
|
||||
function renderGitValidator(repository) {
|
||||
const report = ui.gitValidation;
|
||||
if (!report)
|
||||
return `<div class="validator-empty panel">${projectIllustration("diagnostics")}<div><div class="eyebrow">Repository assurance</div><h2>Validate Git best practices</h2><p>Inspect repository identity, branch governance, tracked secrets, file hygiene and safe local synchronization settings.</p><button class="button primary" data-action="git-validator-scan">${icon("shield")}Run Git Validator</button></div></div>`;
|
||||
const tone =
|
||||
report.score >= 90 ? "success" : report.score >= 70 ? "warning" : "danger";
|
||||
const safeFixes = report.checks.filter(
|
||||
(check) => check.fixAction && check.safe,
|
||||
);
|
||||
const groups = report.checks.reduce((grouped, check) => {
|
||||
(grouped[check.category] ||= []).push(check);
|
||||
return grouped;
|
||||
}, {});
|
||||
return `<div class="validator-page"><section class="validator-hero panel ${tone}"><div class="validator-score"><strong>${report.score}</strong><span>/ 100</span></div><div><div class="eyebrow">Git assurance score</div><h2>${escapeHtml(report.grade)}</h2><p>${report.summary.passed} passed · ${report.summary.warnings} recommendations · ${report.summary.errors} critical</p></div>${projectIllustration("diagnostics")}<div class="validator-actions"><button class="button" data-action="git-validator-scan">${icon("refresh")}Scan again</button>${safeFixes.length ? `<button class="button primary" data-action="git-validator-repair-safe">${icon("wrench")}Apply ${safeFixes.length} safe fix${safeFixes.length === 1 ? "" : "es"}</button>` : ""}</div></section><div class="validator-groups">${Object.entries(
|
||||
groups,
|
||||
)
|
||||
.map(
|
||||
([category, checks]) =>
|
||||
`<section class="panel validator-group"><div class="panel-header"><h3>${escapeHtml(category)}</h3><span class="meta">${checks.filter((check) => check.status === "pass").length}/${checks.length} passed</span></div><div class="validator-checks">${checks
|
||||
.map((check) => {
|
||||
const checkIndex = report.checks.indexOf(check);
|
||||
return `<article class="validator-check ${check.status}"><span class="validator-check-icon">${icon(check.status === "pass" ? "check" : check.status === "error" ? "error" : "warning")}</span><div><strong>${escapeHtml(check.title)}</strong><p>${escapeHtml(check.detail)}</p></div>${check.fixAction ? `<button class="button ${check.safe ? "" : "primary"}" data-action="git-validator-repair" data-check-index="${checkIndex}">${icon("wrench")}${check.safe ? "Fix safely" : "Review & fix"}</button>` : `<span class="status-pill ${check.status === "pass" ? "success" : check.status === "error" ? "danger" : "warning"}">${check.status === "pass" ? "Best practice" : "Review"}</span>`}</article>`;
|
||||
})
|
||||
.join("")}</div></section>`,
|
||||
)
|
||||
.join("")}</div></div>`;
|
||||
}
|
||||
|
||||
function renderRepositoryWorkspace(repository) {
|
||||
const status = repository.localStatus;
|
||||
const profile = selectedProfile(repository);
|
||||
@@ -998,6 +1028,7 @@ function renderRepositoryWorkspace(repository) {
|
||||
history: renderHistory,
|
||||
deployments: renderRepositoryDeployments,
|
||||
gittools: renderGitTools,
|
||||
validator: renderGitValidator,
|
||||
settings: renderRepositorySettings,
|
||||
}[ui.repositoryTab] || renderChanges
|
||||
)(repository);
|
||||
@@ -1009,6 +1040,7 @@ function renderRepositoryWorkspace(repository) {
|
||||
["history", "History"],
|
||||
["deployments", "Deployments"],
|
||||
["gittools", "Git tools"],
|
||||
["validator", "Git Validator"],
|
||||
["settings", "Project settings"],
|
||||
]
|
||||
.map(
|
||||
@@ -1669,7 +1701,19 @@ app.addEventListener("click", async (event) => {
|
||||
ui.repositoryTab = target.dataset.tab;
|
||||
if (ui.repositoryTab === "gittools" && !ui.branches.length)
|
||||
await loadGitTools(repository);
|
||||
else if (ui.repositoryTab === "settings") {
|
||||
else if (ui.repositoryTab === "validator" && !ui.gitValidation) {
|
||||
setLoading(true, "Validating Git and Gitea best practices…");
|
||||
try {
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(
|
||||
repository.fullName,
|
||||
);
|
||||
} catch (error) {
|
||||
showToast("Git Validator failed", error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
render();
|
||||
} else if (ui.repositoryTab === "settings") {
|
||||
try {
|
||||
ui.pullRequests = await window.forgeflow.pullRequests(
|
||||
repository.fullName,
|
||||
@@ -1681,6 +1725,61 @@ app.addEventListener("click", async (event) => {
|
||||
}
|
||||
render();
|
||||
} else render();
|
||||
} else if (action === "git-validator-scan") {
|
||||
setLoading(true, "Validating Git and Gitea best practices…");
|
||||
try {
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(
|
||||
repository.fullName,
|
||||
);
|
||||
showToast(
|
||||
"Git validation complete",
|
||||
`${ui.gitValidation.score}/100 · ${ui.gitValidation.grade}`,
|
||||
ui.gitValidation.summary.errors ? "error" : "success",
|
||||
);
|
||||
} catch (error) {
|
||||
showToast("Git Validator failed", error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (action === "git-validator-repair") {
|
||||
const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)];
|
||||
if (!check?.fixAction) return;
|
||||
if (!check.safe && !confirm(check.confirmation || `Apply ${check.title}?`))
|
||||
return;
|
||||
setLoading(true, `Repairing ${check.title}…`);
|
||||
try {
|
||||
await window.forgeflow.gitValidatorRepair(repository.fullName, check);
|
||||
await refreshRepositories(false, true);
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(
|
||||
repository.fullName,
|
||||
);
|
||||
showToast("Git best practice repaired", check.title, "success");
|
||||
} catch (error) {
|
||||
showToast("Repair failed", error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (action === "git-validator-repair-safe") {
|
||||
const checks = (ui.gitValidation?.checks || []).filter(
|
||||
(check) => check.fixAction && check.safe,
|
||||
);
|
||||
setLoading(true, `Applying ${checks.length} safe Git fixes…`);
|
||||
let repaired = 0;
|
||||
try {
|
||||
for (const check of checks) {
|
||||
await window.forgeflow.gitValidatorRepair(repository.fullName, check);
|
||||
repaired += 1;
|
||||
}
|
||||
await refreshRepositories(false, true);
|
||||
ui.gitValidation = await window.forgeflow.gitValidatorScan(
|
||||
repository.fullName,
|
||||
);
|
||||
showToast("Safe Git fixes applied", `${repaired} repaired.`, "success");
|
||||
} catch (error) {
|
||||
showToast("Safe repair stopped", error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (action === "toggle-favorite") {
|
||||
ui.boot.state = await window.forgeflow.favoriteRepository(
|
||||
repository.fullName,
|
||||
|
||||
Reference in New Issue
Block a user