feat(sync): remove target avatar when source has none

Add removeProjectAvatar and removeGroupAvatar methods to keep avatars
fully in sync by clearing the target avatar when the source has none.
This commit is contained in:
2026-02-28 17:57:00 +00:00
parent 44ac2e430f
commit 78247c1d41
4 changed files with 59 additions and 2 deletions

View File

@@ -1139,6 +1139,7 @@ export class SyncManager {
} else if (config.useGroupAvatarsForProjects) {
// Project has no avatar — inherit from parent group
const groupPath = sourceFullPath.substring(0, sourceFullPath.lastIndexOf('/'));
let groupAvatarApplied = false;
if (groupPath) {
try {
const sourceGroup = await this.fetchGroupRaw(sourceConn, groupPath);
@@ -1147,6 +1148,7 @@ export class SyncManager {
if (groupMeta.avatarUrl) {
logger.syncLog('info', `Applying group avatar to ${targetFullPath}`, 'api');
await this.syncProjectAvatar(sourceConn, targetConn, sourceFullPath, targetFullPath, groupMeta.avatarUrl, targetProject);
groupAvatarApplied = true;
}
}
} catch (err) {
@@ -1154,6 +1156,15 @@ export class SyncManager {
logger.syncLog('warn', `Group avatar sync failed for ${targetFullPath}: ${errMsg}`, 'api');
}
}
// If group also has no avatar, remove target avatar
if (!groupAvatarApplied && targetMeta.avatarUrl) {
await this.removeProjectAvatar(targetConn, targetFullPath, targetProject);
}
} else {
// No source avatar, no group fallback — remove target avatar if present
if (targetMeta.avatarUrl) {
await this.removeProjectAvatar(targetConn, targetFullPath, targetProject);
}
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
@@ -1199,6 +1210,8 @@ export class SyncManager {
// Sync avatar
if (sourceMeta.avatarUrl) {
await this.syncGroupAvatar(sourceConn, targetConn, sourceGroupPath, targetGroupPath, sourceMeta.avatarUrl, targetGroup);
} else if (targetMeta.avatarUrl) {
await this.removeGroupAvatar(targetConn, targetGroupPath, targetGroup);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
@@ -1383,6 +1396,25 @@ export class SyncManager {
}
}
private async removeProjectAvatar(
targetConn: interfaces.data.IProviderConnection,
targetFullPath: string,
targetRawProject: any,
): Promise<void> {
logger.syncLog('info', `Removing avatar from ${targetFullPath}`, 'api');
if (targetConn.providerType === 'gitlab') {
await this.rawApiCall(targetConn, 'PUT', `/api/v4/projects/${targetRawProject.id}`, {
avatar: '',
});
} else {
const segments = targetFullPath.split('/');
const repo = segments.pop()!;
const owner = segments[0] || '';
await this.rawApiCall(targetConn, 'DELETE',
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/avatar`);
}
}
private async syncGroupAvatar(
sourceConn: interfaces.data.IProviderConnection,
targetConn: interfaces.data.IProviderConnection,
@@ -1422,6 +1454,23 @@ export class SyncManager {
}
}
private async removeGroupAvatar(
targetConn: interfaces.data.IProviderConnection,
targetGroupPath: string,
targetRawGroup: any,
): Promise<void> {
logger.syncLog('info', `Removing avatar from group ${targetGroupPath}`, 'api');
if (targetConn.providerType === 'gitlab') {
await this.rawApiCall(targetConn, 'PUT', `/api/v4/groups/${targetRawGroup.id}`, {
avatar: '',
});
} else {
const orgName = targetGroupPath.split('/')[0] || targetGroupPath;
await this.rawApiCall(targetConn, 'DELETE',
`/api/v1/orgs/${encodeURIComponent(orgName)}/avatar`);
}
}
private uint8ArrayToBase64(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) {