feat: add safe Gitea sync and signed updates
This commit is contained in:
@@ -26,7 +26,9 @@ class RepositoryMonitor {
|
||||
this.watchers = new Map();
|
||||
this.changed = new Set();
|
||||
this.lastCheckedAt = new Map();
|
||||
this.lastFetchedAt = new Map();
|
||||
this.watchTimer = null;
|
||||
this.fetchRunning = false;
|
||||
}
|
||||
|
||||
setPaths(paths) {
|
||||
@@ -46,6 +48,13 @@ class RepositoryMonitor {
|
||||
for (const existing of [...this.lastCheckedAt.keys()]) {
|
||||
if (!watched.has(existing)) this.lastCheckedAt.delete(existing);
|
||||
}
|
||||
for (const existing of [...this.lastFetchedAt.keys()]) {
|
||||
if (!watched.has(existing)) this.lastFetchedAt.delete(existing);
|
||||
}
|
||||
const now = Date.now();
|
||||
for (const localPath of this.paths) {
|
||||
if (!this.lastFetchedAt.has(localPath)) this.lastFetchedAt.set(localPath, now);
|
||||
}
|
||||
this.syncWatchers();
|
||||
}
|
||||
|
||||
@@ -105,6 +114,60 @@ class RepositoryMonitor {
|
||||
return sinceLastCheck >= SAFETY_CHECK_INTERVAL_MS;
|
||||
}
|
||||
|
||||
fetchIntervalMs() {
|
||||
const minutes = Number(this.store.data.preferences.fetchIntervalMinutes);
|
||||
return Number.isFinite(minutes) && minutes > 0 ? Math.min(minutes, 240) * 60_000 : 0;
|
||||
}
|
||||
|
||||
shouldFetch(localPath, now) {
|
||||
const interval = this.fetchIntervalMs();
|
||||
return interval > 0
|
||||
&& !this.paused.has(localPath)
|
||||
&& now - (this.lastFetchedAt.get(localPath) || now) >= interval;
|
||||
}
|
||||
|
||||
async recordStatus(localPath, status, reason) {
|
||||
const next = this.git.statusFingerprint(status);
|
||||
const previous = this.fingerprints.get(localPath);
|
||||
this.fingerprints.set(localPath, next);
|
||||
if (previous && previous !== next) {
|
||||
await this.diagnostics?.debug('repository-monitor.changed', { localPath, head: status.head, branch: status.branch?.head, counts: status.counts, reason });
|
||||
this.onChange?.({ localPath, status, reason });
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRemoteUpdates(now = Date.now()) {
|
||||
if (this.fetchRunning) return;
|
||||
const queue = this.paths.filter((localPath) => this.shouldFetch(localPath, now));
|
||||
if (!queue.length) return;
|
||||
this.fetchRunning = true;
|
||||
try {
|
||||
const workers = Array.from({ length: Math.min(2, queue.length) }, async () => {
|
||||
while (queue.length) {
|
||||
const localPath = queue.shift();
|
||||
// Mark the attempt before awaiting the network. A failing remote should
|
||||
// not be retried every local poll interval.
|
||||
this.lastFetchedAt.set(localPath, Date.now());
|
||||
try {
|
||||
const result = await this.git.fetch(localPath);
|
||||
await this.recordStatus(localPath, result.status, 'remote-state-changed');
|
||||
await this.diagnostics?.debug('repository-monitor.fetch.completed', {
|
||||
localPath,
|
||||
branch: result.status?.branch?.head,
|
||||
ahead: result.status?.branch?.ahead,
|
||||
behind: result.status?.branch?.behind,
|
||||
});
|
||||
} catch (error) {
|
||||
await this.diagnostics?.warning('repository-monitor.fetch.failed', { localPath, message: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(workers);
|
||||
} finally {
|
||||
this.fetchRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
pause(localPath) { if (localPath) this.paused.add(localPath); }
|
||||
resume(localPath) { if (localPath) this.paused.delete(localPath); }
|
||||
|
||||
@@ -128,6 +191,7 @@ class RepositoryMonitor {
|
||||
}
|
||||
|
||||
async tick() {
|
||||
void this.fetchRemoteUpdates().catch((error) => this.diagnostics?.warning('repository-monitor.fetch-cycle.failed', error));
|
||||
if (this.running || !this.paths.length) return;
|
||||
this.running = true;
|
||||
try {
|
||||
@@ -140,13 +204,7 @@ class RepositoryMonitor {
|
||||
this.lastCheckedAt.set(localPath, Date.now());
|
||||
try {
|
||||
const status = await this.git.status(localPath);
|
||||
const next = this.git.statusFingerprint(status);
|
||||
const previous = this.fingerprints.get(localPath);
|
||||
this.fingerprints.set(localPath, next);
|
||||
if (previous && previous !== next) {
|
||||
await this.diagnostics?.debug('repository-monitor.changed', { localPath, head: status.head, branch: status.branch?.head, counts: status.counts });
|
||||
this.onChange?.({ localPath, status, reason: 'working-tree-changed' });
|
||||
}
|
||||
await this.recordStatus(localPath, status, 'working-tree-changed');
|
||||
} catch (error) {
|
||||
const next = `error:${error.message}`;
|
||||
const previous = this.fingerprints.get(localPath);
|
||||
|
||||
Reference in New Issue
Block a user