This commit is contained in:
NuklearRabbit
2026-07-24 20:29:23 +02:00
commit 66060348da
107 changed files with 14771 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
'use strict';
const fs = require('node:fs/promises');
const path = require('node:path');
const { matchRemoteToRepository, repositoryKey } = require('../shared/repository-match.cjs');
const SKIP_DIRECTORIES = new Set([
'.git', '.svn', '.hg', 'node_modules', '.next', '.nuxt', 'dist', 'build', 'coverage',
'.cache', '.venv', 'venv', '__pycache__', '$RECYCLE.BIN', 'System Volume Information'
]);
async function mapLimit(items, limit, mapper) {
const output = new Array(items.length);
let cursor = 0;
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
while (cursor < items.length) {
const index = cursor++;
output[index] = await mapper(items[index], index);
}
});
await Promise.all(workers);
return output;
}
class RepositoryService {
constructor(store, gitService, giteaService, diagnostics = null) {
this.store = store;
this.git = gitService;
this.gitea = giteaService;
this.diagnostics = diagnostics;
this.lastKnownLocalPaths = [];
}
async discoverInRoot(root, maxDepth = 4) {
const found = [];
const seen = new Set();
const visit = async (directory, depth) => {
let real;
try { real = await fs.realpath(directory); } catch { return; }
if (seen.has(real)) return;
seen.add(real);
const gitMarker = path.join(directory, '.git');
const marker = await fs.stat(gitMarker).catch(() => null);
if (marker) {
found.push(real);
return;
}
if (depth >= maxDepth) return;
let entries;
try { entries = await fs.readdir(real, { withFileTypes: true }); } catch { return; }
await mapLimit(entries
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink() && !SKIP_DIRECTORIES.has(entry.name)), 12,
(entry) => visit(path.join(real, entry.name), depth + 1));
};
await visit(root, 0);
return found;
}
async discoverAll(roots) {
const grouped = await mapLimit((roots || []).filter(Boolean), 4, (root) => this.discoverInRoot(root));
return [...new Set(grouped.flat())];
}
async getLocalDescriptors(paths) {
return mapLimit(paths, 5, async (localPath) => {
try {
const status = await this.git.status(localPath);
return { localPath: status.root, remoteUrl: status.remoteUrl, status };
} catch (error) {
return { localPath, remoteUrl: '', status: null, error: error.message };
}
});
}
getWatchPaths() {
return [...this.lastKnownLocalPaths];
}
async refresh() {
const started = Date.now();
const remoteRepositories = this.store.data.gitea.baseUrl && this.store.getToken()
? await this.gitea.listRepositories()
: [];
const discoveredPaths = await this.discoverAll(this.store.data.workspaceRoots);
const mappedPaths = Object.values(this.store.data.repositoryMappings || {});
const localPaths = [...new Set([...discoveredPaths, ...mappedPaths])];
const localDescriptors = await this.getLocalDescriptors(localPaths);
this.lastKnownLocalPaths = localDescriptors.filter((item) => item.status).map((item) => item.status.root);
const usedLocalPaths = new Set();
const repositories = [];
for (const remote of remoteRepositories) {
const key = repositoryKey(remote);
const explicitPath = this.store.data.repositoryMappings[key];
let local = explicitPath ? localDescriptors.find((item) => path.resolve(item.localPath) === path.resolve(explicitPath)) : null;
if (!local) local = localDescriptors.find((item) => !usedLocalPaths.has(item.localPath) && matchRemoteToRepository(item.remoteUrl, [remote]));
if (local) usedLocalPaths.add(local.localPath);
const profiles = this.store.getDeploymentProfiles(remote.full_name).map((profile) => ({
...profile,
state: this.store.getDeploymentState(profile.id)
}));
repositories.push(this.decorate(remote, local, profiles));
}
for (const local of localDescriptors.filter((item) => !usedLocalPaths.has(item.localPath))) {
const name = path.basename(local.localPath);
repositories.push({
id: `local:${local.localPath}`,
name,
fullName: name,
owner: { login: 'local' },
description: 'Local repository not matched to Gitea',
private: true,
defaultBranch: local.status?.branch.head || 'main',
htmlUrl: null,
cloneUrl: null,
sshUrl: null,
preferredCloneUrl: null,
localPath: local.localPath,
localStatus: local.status,
linkState: 'unmatched-local',
deploymentProfiles: [],
readyToDeploy: false,
favorite: false,
attention: Boolean(local.error),
attentionReason: local.error || null
});
}
const sorted = repositories.sort((a, b) => {
const score = (repo) => (repo.attention ? 100 : 0)
+ (repo.localStatus?.counts.changed ? 50 : 0)
+ (repo.localStatus?.branch.ahead ? 30 : 0)
+ (repo.readyToDeploy ? 20 : 0)
+ (repo.favorite ? 5 : 0);
return score(b) - score(a) || a.fullName.localeCompare(b.fullName);
});
await this.diagnostics?.debug('repositories.refresh.completed', {
durationMs: Date.now() - started,
remoteCount: remoteRepositories.length,
discoveredCount: discoveredPaths.length,
linkedCount: sorted.filter((item) => item.localPath).length,
attentionCount: sorted.filter((item) => item.attention).length,
readyToDeployCount: sorted.filter((item) => item.readyToDeploy).length
});
return sorted;
}
decorate(remote, local, profiles) {
const status = local?.status || null;
const hasChanges = Boolean(status?.counts.changed);
const ahead = status?.branch.ahead || 0;
const behind = status?.branch.behind || 0;
const conflict = Boolean(status?.counts.conflicts);
const profileForBranch = profiles.find((profile) => profile.branch === status?.branch.head);
const readyToDeploy = Boolean(profileForBranch && status?.head && status?.branch.upstream && !hasChanges && ahead === 0 && behind === 0);
const key = String(remote.full_name || '').toLowerCase();
const preferredCloneUrl = this.store.data.preferences.preferredCloneProtocol === 'ssh'
? (remote.ssh_url || remote.clone_url)
: (remote.clone_url || remote.ssh_url);
return {
id: remote.id,
name: remote.name,
fullName: remote.full_name,
owner: remote.owner,
description: remote.description || '',
private: remote.private,
defaultBranch: remote.default_branch || 'main',
htmlUrl: remote.html_url,
cloneUrl: remote.clone_url,
sshUrl: remote.ssh_url,
preferredCloneUrl,
updatedAt: remote.updated_at,
localPath: local?.localPath || null,
localStatus: status,
linkState: local ? 'linked' : 'remote-only',
deploymentProfiles: profiles,
readyToDeploy,
favorite: (this.store.data.favorites || []).includes(key),
attention: conflict || behind > 0 || Boolean(local?.error),
attentionReason: conflict ? 'Merge conflict' : behind > 0 ? `${behind} commit${behind === 1 ? '' : 's'} behind remote` : local?.error || null
};
}
}
module.exports = { RepositoryService, SKIP_DIRECTORIES, mapLimit };