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:
parent
b6981c4369
commit
78880af9f7
2 changed files with 273 additions and 74 deletions
|
|
@ -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[]> {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue