feat: Synchronize deletions when pulling from source control (#12170)

Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
Danny Martini
2025-01-20 16:53:55 +01:00
committed by GitHub
parent f167578b32
commit 967ee4b89b
21 changed files with 900 additions and 365 deletions

View File

@@ -322,8 +322,44 @@ export class SourceControlService {
};
}
private getConflicts(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((file) => file.conflict || file.status === 'modified');
}
private getWorkflowsToImport(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((e) => e.type === 'workflow' && e.status !== 'deleted');
}
private getWorkflowsToDelete(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((e) => e.type === 'workflow' && e.status === 'deleted');
}
private getCredentialsToImport(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((e) => e.type === 'credential' && e.status !== 'deleted');
}
private getCredentialsToDelete(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((e) => e.type === 'credential' && e.status === 'deleted');
}
private getTagsToImport(files: SourceControlledFile[]): SourceControlledFile | undefined {
return files.find((e) => e.type === 'tags' && e.status !== 'deleted');
}
private getTagsToDelete(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((e) => e.type === 'tags' && e.status === 'deleted');
}
private getVariablesToImport(files: SourceControlledFile[]): SourceControlledFile | undefined {
return files.find((e) => e.type === 'variables' && e.status !== 'deleted');
}
private getVariablesToDelete(files: SourceControlledFile[]): SourceControlledFile[] {
return files.filter((e) => e.type === 'variables' && e.status === 'deleted');
}
async pullWorkfolder(
userId: User['id'],
user: User,
options: PullWorkFolderRequestDto,
): Promise<{ statusCode: number; statusResult: SourceControlledFile[] }> {
await this.sanityCheck();
@@ -334,58 +370,51 @@ export class SourceControlService {
preferLocalVersion: false,
})) as SourceControlledFile[];
// filter out items that will not effect a local change and thus should not
// trigger a conflict warning in the frontend
const filteredResult = statusResult.filter((e) => {
// locally created credentials will not create a conflict on pull
if (e.status === 'created' && e.location === 'local') {
return false;
}
// remotely deleted credentials will not delete local credentials
if (e.type === 'credential' && e.status === 'deleted') {
return false;
}
return true;
});
if (!options.force) {
const possibleConflicts = filteredResult?.filter(
(file) => (file.conflict || file.status === 'modified') && file.type === 'workflow',
);
if (options.force !== true) {
const possibleConflicts = this.getConflicts(statusResult);
if (possibleConflicts?.length > 0) {
await this.gitService.resetBranch();
return {
statusCode: 409,
statusResult: filteredResult,
statusResult,
};
}
}
const workflowsToBeImported = statusResult.filter(
(e) => e.type === 'workflow' && e.status !== 'deleted',
);
const workflowsToBeImported = this.getWorkflowsToImport(statusResult);
await this.sourceControlImportService.importWorkflowFromWorkFolder(
workflowsToBeImported,
userId,
user.id,
);
const credentialsToBeImported = statusResult.filter(
(e) => e.type === 'credential' && e.status !== 'deleted',
const workflowsToBeDeleted = this.getWorkflowsToDelete(statusResult);
await this.sourceControlImportService.deleteWorkflowsNotInWorkfolder(
user,
workflowsToBeDeleted,
);
const credentialsToBeImported = this.getCredentialsToImport(statusResult);
await this.sourceControlImportService.importCredentialsFromWorkFolder(
credentialsToBeImported,
userId,
user.id,
);
const credentialsToBeDeleted = this.getCredentialsToDelete(statusResult);
await this.sourceControlImportService.deleteCredentialsNotInWorkfolder(
user,
credentialsToBeDeleted,
);
const tagsToBeImported = statusResult.find((e) => e.type === 'tags');
const tagsToBeImported = this.getTagsToImport(statusResult);
if (tagsToBeImported) {
await this.sourceControlImportService.importTagsFromWorkFolder(tagsToBeImported);
}
const tagsToBeDeleted = this.getTagsToDelete(statusResult);
await this.sourceControlImportService.deleteTagsNotInWorkfolder(tagsToBeDeleted);
const variablesToBeImported = statusResult.find((e) => e.type === 'variables');
const variablesToBeImported = this.getVariablesToImport(statusResult);
if (variablesToBeImported) {
await this.sourceControlImportService.importVariablesFromWorkFolder(variablesToBeImported);
}
const variablesToBeDeleted = this.getVariablesToDelete(statusResult);
await this.sourceControlImportService.deleteVariablesNotInWorkfolder(variablesToBeDeleted);
// #region Tracking Information
this.eventService.emit(
@@ -396,7 +425,7 @@ export class SourceControlService {
return {
statusCode: 200,
statusResult: filteredResult,
statusResult,
};
}
@@ -536,7 +565,7 @@ export class SourceControlService {
type: 'workflow',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
conflict: options.direction === 'push' ? false : true,
file: item.filename,
updatedAt: item.updatedAt ?? new Date().toISOString(),
});
@@ -617,7 +646,7 @@ export class SourceControlService {
type: 'credential',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
conflict: options.direction === 'push' ? false : true,
file: item.filename,
updatedAt: new Date().toISOString(),
});
@@ -669,26 +698,47 @@ export class SourceControlService {
}
});
if (
varMissingInLocal.length > 0 ||
varMissingInRemote.length > 0 ||
varModifiedInEither.length > 0
) {
if (options.direction === 'pull' && varRemoteIds.length === 0) {
// if there's nothing to pull, don't show difference as modified
} else {
sourceControlledFiles.push({
id: 'variables',
name: 'variables',
type: 'variables',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: getVariablesPath(this.gitFolder),
updatedAt: new Date().toISOString(),
});
}
}
varMissingInLocal.forEach((item) => {
sourceControlledFiles.push({
id: item.id,
name: item.key,
type: 'variables',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: getVariablesPath(this.gitFolder),
updatedAt: new Date().toISOString(),
});
});
varMissingInRemote.forEach((item) => {
sourceControlledFiles.push({
id: item.id,
name: item.key,
type: 'variables',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
// if the we pull and the file is missing in the remote, we will delete
// it locally, which is communicated by marking this as a conflict
conflict: options.direction === 'push' ? false : true,
file: getVariablesPath(this.gitFolder),
updatedAt: new Date().toISOString(),
});
});
varModifiedInEither.forEach((item) => {
sourceControlledFiles.push({
id: item.id,
name: item.key,
type: 'variables',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: getVariablesPath(this.gitFolder),
updatedAt: new Date().toISOString(),
});
});
return {
varMissingInLocal,
varMissingInRemote,
@@ -743,32 +793,44 @@ export class SourceControlService {
) === -1,
);
if (
tagsMissingInLocal.length > 0 ||
tagsMissingInRemote.length > 0 ||
tagsModifiedInEither.length > 0 ||
mappingsMissingInLocal.length > 0 ||
mappingsMissingInRemote.length > 0
) {
if (
options.direction === 'pull' &&
tagMappingsRemote.tags.length === 0 &&
tagMappingsRemote.mappings.length === 0
) {
// if there's nothing to pull, don't show difference as modified
} else {
sourceControlledFiles.push({
id: 'mappings',
name: 'tags',
type: 'tags',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: getTagsPath(this.gitFolder),
updatedAt: lastUpdatedTag[0]?.updatedAt.toISOString(),
});
}
}
tagsMissingInLocal.forEach((item) => {
sourceControlledFiles.push({
id: item.id,
name: item.name,
type: 'tags',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: getTagsPath(this.gitFolder),
updatedAt: lastUpdatedTag[0]?.updatedAt.toISOString(),
});
});
tagsMissingInRemote.forEach((item) => {
sourceControlledFiles.push({
id: item.id,
name: item.name,
type: 'tags',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction === 'push' ? false : true,
file: getTagsPath(this.gitFolder),
updatedAt: lastUpdatedTag[0]?.updatedAt.toISOString(),
});
});
tagsModifiedInEither.forEach((item) => {
sourceControlledFiles.push({
id: item.id,
name: item.name,
type: 'tags',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: getTagsPath(this.gitFolder),
updatedAt: lastUpdatedTag[0]?.updatedAt.toISOString(),
});
});
return {
tagsMissingInLocal,
tagsMissingInRemote,