fix changelog settings logging every single setting in Equicord (#424)

* add serialization for settings and a myraid of other fixes rewrites.

* test dummy settings

* fix issue with commits not being logged, rewrite some elements

* remove dummy setting

---------

Co-authored-by: Jake <jake@Georges-MacBook-Air.local>
This commit is contained in:
Jake 2025-09-29 16:49:01 +01:00 • committed by GitHub
parent b6981c4369
commit 78880af9f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 273 additions and 74 deletions

View file

@ -35,9 +35,144 @@ const LAST_SEEN_HASH_KEY = "EquicordChangelog_LastSeenHash";
const KNOWN_PLUGINS_KEY = "EquicordChangelog_KnownPlugins";
const KNOWN_SETTINGS_KEY = "EquicordChangelog_KnownSettings";
const LAST_REPO_CHECK_KEY = "EquicordChangelog_LastRepoCheck";
const GITHUB_COMPARE_ENDPOINT = "https://api.github.com/repos";
type KnownPluginSettingsMap = Map<string, Set<string>>;
function normalizeRepoUrl(repoUrl: string | null | undefined): string | null {
if (!repoUrl) return null;
try {
const normalized = repoUrl.replace(/^git\+/, "");
const url = new URL(normalized);
if (!url.hostname.endsWith("github.com")) return null;
const segments = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean);
if (segments.length < 2) return null;
return `${segments[0]}/${segments[1]}`;
} catch {
return null;
}
}
async function fetchCommitsBetween(
repoSlug: string,
fromHash: string,
toHash: string,
): Promise<ChangelogEntry[]> {
if (!repoSlug || typeof fetch !== "function") return [];
try {
const res = await fetch(
`${GITHUB_COMPARE_ENDPOINT}/${repoSlug}/compare/${fromHash}...${toHash}`,
{
headers: {
Accept: "application/vnd.github+json",
"Cache-Control": "no-cache",
},
},
);
if (!res.ok) return [];
const data = await res.json();
if (!data || !Array.isArray(data.commits)) return [];
return data.commits.map((commit: any) => {
const message: string = commit?.commit?.message ?? "";
const summary = message.split("\n")[0] || "No message";
const authorName =
commit?.commit?.author?.name ||
commit?.author?.login ||
"Unknown";
const timestamp = commit?.commit?.author?.date
? Date.parse(commit.commit.author.date)
: undefined;
return {
hash: commit?.sha || "",
author: authorName,
message: summary,
timestamp: Number.isNaN(timestamp) ? undefined : timestamp,
} as ChangelogEntry;
});
} catch (err) {
console.warn("Failed to fetch commits between hashes", err);
return [];
}
}
function toStringSet(value: unknown): Set<string> {
const result = new Set<string>();
const addValue = (entry: unknown) => {
if (entry === undefined || entry === null) return;
result.add(typeof entry === "string" ? entry : String(entry));
};
if (value instanceof Set) {
value.forEach(addValue);
} else if (value instanceof Map) {
value.forEach(addValue);
} else if (Array.isArray(value)) {
value.forEach(addValue);
} else if (typeof value === "string") {
addValue(value);
} else if (value && typeof value === "object") {
Object.values(value as Record<string, unknown>).forEach(addValue);
}
return result;
}
function normalizeKnownSettings(value: unknown): KnownPluginSettingsMap {
const map: KnownPluginSettingsMap = new Map();
const assign = (plugin: unknown, settings: unknown) => {
if (plugin === undefined || plugin === null) return;
map.set(String(plugin), toStringSet(settings));
};
if (!value) {
return map;
}
if (value instanceof Map) {
value.forEach((settings, plugin) => assign(plugin, settings));
return map;
}
if (Array.isArray(value)) {
value.forEach(entry => {
if (Array.isArray(entry) && entry.length > 0) {
assign(entry[0], entry[1]);
}
});
return map;
}
if (typeof value === "object") {
Object.entries(value as Record<string, unknown>).forEach(
([plugin, settings]) => assign(plugin, settings),
);
}
return map;
}
function serializeKnownSettings(
map: KnownPluginSettingsMap,
): Record<string, string[]> {
return Object.fromEntries(
Array.from(map.entries()).map(([plugin, settings]) => [
plugin,
Array.from(settings),
]),
);
}
async function persistKnownSettings(
map: KnownPluginSettingsMap,
): Promise<void> {
await DataStore.set(KNOWN_SETTINGS_KEY, serializeKnownSettings(map));
}
function isMapLike(value: any): value is Map<string, string[]> {
return (
value &&
@ -204,28 +339,34 @@ function getCurrentSettings(pluginList: string[]): KnownPluginSettingsMap {
export async function getKnownSettings(): Promise<KnownPluginSettingsMap> {
const mapData = (await DataStore.get(KNOWN_SETTINGS_KEY)) as any;
let map: KnownPluginSettingsMap;
if (mapData === undefined) {
const knownPlugins = await getKnownPlugins();
const Plugins = [...Object.keys(plugins), ...Array.from(knownPlugins)];
map = getCurrentSettings(Plugins);
await DataStore.set(KNOWN_SETTINGS_KEY, map);
} else {
// convert the plain object back to a map with set value(s)
map = new Map();
if (mapData && typeof mapData === "object") {
for (const [plugin, settings] of Object.entries(mapData)) {
if (Array.isArray(settings)) {
map.set(plugin, new Set(settings));
} else if (settings && typeof settings === "object") {
// should fix serialization issues
map.set(plugin, new Set(Object.values(settings)));
}
}
}
const pluginNames = [
...new Set([
...Object.keys(plugins),
...Array.from(knownPlugins),
]),
];
const initialMap = getCurrentSettings(pluginNames);
await persistKnownSettings(initialMap);
return initialMap;
}
return map;
const normalized = normalizeKnownSettings(mapData);
if (
mapData instanceof Map ||
Array.isArray(mapData) ||
(mapData &&
typeof mapData === "object" &&
Object.values(mapData).some(value =>
value instanceof Set || value instanceof Map,
))
) {
await persistKnownSettings(normalized);
}
return normalized;
}
export async function getNewSettings(): Promise<Map<string, string[]>> {
@ -234,8 +375,11 @@ export async function getNewSettings(): Promise<Map<string, string[]>> {
const newSettings = new Map<string, string[]>();
map.forEach((settings, plugin) => {
const known = knownSettings.get(plugin);
if (!known) return;
const filteredSettings = [...settings].filter(
setting => !knownSettings.get(plugin)?.has(setting),
setting => !known.has(setting),
);
if (filteredSettings.length > 0) {
newSettings.set(plugin, filteredSettings);
@ -245,32 +389,37 @@ export async function getNewSettings(): Promise<Map<string, string[]>> {
return newSettings;
}
export async function getCommitsSinceLastSeen(
repoUrl: string,
): Promise<ChangelogEntry[]> {
const lastSeenHash = await getLastSeenHash();
if (!lastSeenHash || lastSeenHash === "unknown" || lastSeenHash === gitHash)
return [];
const repoSlug = normalizeRepoUrl(repoUrl);
if (!repoSlug) return [];
return fetchCommitsBetween(repoSlug, lastSeenHash, gitHash);
}
export async function updateKnownSettings(): Promise<void> {
const currentSettings = getCurrentSettings(Object.keys(plugins));
const knownSettings = await getKnownSettings();
const allSettings = new Map();
const mergedSettings: KnownPluginSettingsMap = new Map();
new Set([...currentSettings.keys(), ...knownSettings.keys()]).forEach(
plugin => {
allSettings.set(
mergedSettings.set(
plugin,
new Set([
...(currentSettings.get(plugin) || []),
...(knownSettings.get(plugin) || []),
...(currentSettings.get(plugin) || []),
]),
);
},
);
// convert map to serialized storage format
const serializableSettings = Object.fromEntries(
Array.from(allSettings.entries()).map(([plugin, settings]) => [
plugin,
Array.from(settings),
]),
);
await DataStore.set(KNOWN_SETTINGS_KEY, serializableSettings);
await persistKnownSettings(mergedSettings);
}
export async function getNewPlugins(): Promise<string[]> {

View file

@ -24,6 +24,7 @@ import {
clearIndividualLog,
formatTimestamp,
getChangelogHistory,
getCommitsSinceLastSeen,
getLastRepositoryCheckHash,
getNewPlugins,
getNewSettings,
@ -295,6 +296,32 @@ function ChangelogContent() {
}
}, []);
const ensureLocalUpdateLogged = React.useCallback(async () => {
if (repoPending || repoErr) return false;
const repoUrl = repo;
if (!repoUrl) return false;
try {
const commits = await getCommitsSinceLastSeen(repoUrl);
if (commits.length === 0) return false;
const newPlgs = await getNewPlugins();
const updatedPlgs = await getUpdatedPlugins();
const newSettings = await getNewSettings();
await saveUpdateSession(commits, newPlgs, updatedPlgs, newSettings);
setChangelog(commits);
setNewPlugins(newPlgs);
setUpdatedPlugins(updatedPlgs);
await loadChangelogHistory();
return true;
} catch (err) {
console.error("Failed to log local update:", err);
return false;
}
}, [repo, repoErr, repoPending, loadChangelogHistory]);
// check if the repository was recently refreshed
React.useEffect(() => {
const checkRecentStatus = async () => {
@ -339,52 +366,66 @@ function ChangelogContent() {
if (lastRepoCheck === currentRepoHash) {
setIsLoading(false);
setRecentlyChecked(true);
Toasts.show({
message: "Already up to date with repository",
id: Toasts.genId(),
type: Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM,
},
});
const logged = await ensureLocalUpdateLogged();
if (!logged) {
setChangelog([]);
Toasts.show({
message: "Already up to date with repository",
id: Toasts.genId(),
type: Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM,
},
});
}
return;
}
if (updates.ok && updates.value) {
setChangelog(updates.value);
if (updates.value.length > 0) {
setChangelog(updates.value);
// Load current new/updated plugins and settings
const newPlgs = await getNewPlugins();
const updatedPlgs = await getUpdatedPlugins();
const newSettings = await getNewSettings();
setNewPlugins(newPlgs);
setUpdatedPlugins(updatedPlgs);
const newPlgs = await getNewPlugins();
const updatedPlgs = await getUpdatedPlugins();
const newSettings = await getNewSettings();
setNewPlugins(newPlgs);
setUpdatedPlugins(updatedPlgs);
// always save the current fetch session to history to track repo state
await saveUpdateSession(
updates.value,
newPlgs,
updatedPlgs,
newSettings,
true, // forceLog = true for repository fetches
);
await loadChangelogHistory();
setRecentlyChecked(true);
await saveUpdateSession(
updates.value,
newPlgs,
updatedPlgs,
newSettings,
true,
);
await loadChangelogHistory();
setRecentlyChecked(true);
Toasts.show({
message:
updates.value.length > 0
? `Found ${updates.value.length} commit${updates.value.length === 1 ? "" : "s"} from repository`
Toasts.show({
message: `Found ${updates.value.length} commit${updates.value.length === 1 ? "" : "s"} from repository`,
id: Toasts.genId(),
type: Toasts.Type.SUCCESS,
options: {
position: Toasts.Position.BOTTOM,
},
});
} else {
const logged = await ensureLocalUpdateLogged();
setRecentlyChecked(true);
Toasts.show({
message: logged
? "Logged commits from your latest update"
: "Repository is up to date with your local copy",
id: Toasts.genId(),
type:
updates.value.length > 0
? Toasts.Type.SUCCESS
: Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM,
},
});
id: Toasts.genId(),
type: logged ? Toasts.Type.SUCCESS : Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM,
},
});
if (!logged) {
setChangelog([]);
}
}
} else if (!updates.ok) {
throw new Error(
updates.error?.message || "Failed to fetch from repository",
@ -414,18 +455,27 @@ function ChangelogContent() {
React.useEffect(() => {
const loadInitialData = async () => {
if (!repoPending && !repoErr) {
// load new plugins first, then fetch the commits
await loadNewPlugins();
await fetchChangelog();
const logged = await ensureLocalUpdateLogged();
if (!logged) {
await fetchChangelog();
} else {
setIsLoading(false);
}
} else if (!repoPending) {
// perseverance
await loadNewPlugins();
setIsLoading(false);
}
};
loadInitialData();
}, [repoPending, repoErr, fetchChangelog, loadNewPlugins]);
}, [
repoPending,
repoErr,
fetchChangelog,
loadNewPlugins,
ensureLocalUpdateLogged,
]);
const toggleLogExpanded = (logId: string) => {
const newExpanded = new Set(expandedLogs);