Skip to content

Commit 9f7a29b

Browse files
authored
fix: check conflicts during retrieve W-20797131 (#6756)
* refactor: trackingService effects are standalone * fix: conflict check for retrieves
1 parent 8035aaf commit 9f7a29b

7 files changed

Lines changed: 120 additions & 106 deletions

File tree

packages/salesforcedx-vscode-metadata/src/commands/retrieveManifest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const retrieveManifestEffect = (manifestUri?: URI) =>
3030
yield* componentSetService.getComponentSetFromManifest(manifestPath)
3131
);
3232

33-
yield* retrieveComponentSet({ componentSet });
33+
yield* retrieveComponentSet({ componentSet, ignoreConflicts: false });
3434
}).pipe(Effect.withSpan('retrieveManifest', { attributes: { manifestUri } }), Effect.provide(AllServicesLayer));
3535

3636
/** Retrieve manifest from the default org */

packages/salesforcedx-vscode-metadata/src/commands/retrieveSourcePath.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,23 @@ export const retrieveSourcePaths = async (sourceUri: URI | undefined, uris: URI[
6767
const resolvedUris = uris?.length ? uris : [resolvedSourceUri];
6868
await Effect.runPromise(
6969
retrieveSourcePathsEffect(resolvedSourceUri, resolvedUris).pipe(
70+
Effect.catchTag('SourceTrackingConflictError', error =>
71+
displayErrorMessage(nls.localize('retrieve_source_conflicts_detected', error.conflicts.join(',')))
72+
),
7073
Effect.catchAll(error =>
7174
Effect.gen(function* () {
72-
const api = yield* (yield* ExtensionProviderService).getServicesApi;
73-
const channelService = yield* api.services.ChannelService;
7475
const errorMessage = error instanceof Error ? error.message : String(error);
75-
yield* channelService.appendToChannel(`Retrieve failed: ${errorMessage}`);
76-
yield* Effect.promise(() => vscode.window.showErrorMessage(errorMessage));
77-
}).pipe(Effect.provide(AllServicesLayer), Effect.as(undefined))
76+
yield* displayErrorMessage(`Retrieve failed: ${errorMessage}`);
77+
}).pipe(Effect.as(undefined))
7878
),
7979
Effect.provide(AllServicesLayer)
8080
)
8181
);
8282
};
83+
84+
const displayErrorMessage = Effect.fn('displayErrorMessage')(function* (msg: string) {
85+
const api = yield* (yield* ExtensionProviderService).getServicesApi;
86+
const channelService = yield* api.services.ChannelService;
87+
yield* channelService.appendToChannel(`Retrieve failed: ${msg}`);
88+
yield* Effect.promise(() => vscode.window.showErrorMessage(msg));
89+
});

packages/salesforcedx-vscode-metadata/src/commands/retrieveStart/projectRetrieveStart.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ const projectRetrieveStartEffect = (ignoreConflicts: boolean) =>
4040
{ concurrency: 'unbounded' }
4141
);
4242

43+
if (!ignoreConflicts) {
44+
// check conflicts up here to avoid deletes (since the normal conflict check is in the retrieve service)
45+
yield* sourceTrackingService.checkConflicts(tracking);
46+
}
47+
4348
const result = yield* Effect.tryPromise({
4449
try: () => tracking.maybeApplyRemoteDeletesToLocal(true),
4550
catch: e =>
@@ -64,7 +69,7 @@ const projectRetrieveStartEffect = (ignoreConflicts: boolean) =>
6469
return;
6570
}
6671

67-
yield* retrieveComponentSet({ componentSet });
72+
yield* retrieveComponentSet({ componentSet, ignoreConflicts: true });
6873
}).pipe(
6974
Effect.withSpan('projectRetrieveStart', { attributes: { ignoreConflicts } }),
7075
Effect.provide(AllServicesLayer)

packages/salesforcedx-vscode-metadata/src/messages/i18n.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const messages = {
5858
retrieve_select_manifest: 'You can run SFDX: Retrieve Source in Manifest from Org only on a manifest file.',
5959
retrieve_completed_with_errors_message: 'Retrieve completed with errors. Check output for details.',
6060
retrieve_no_components_message: 'No components found to retrieve',
61+
retrieve_source_conflicts_detected: 'Conflicts detected. Resolve conflicts before retrieving. \n Conflicts: %s',
6162
retrieve_failed: 'Failed to retrieve: %s',
6263
error_source_tracking_service_failed: 'Failed to initialize source tracking service.',
6364
error_source_tracking_components_failed: 'Failed to retrieve components using source tracking: %s',

packages/salesforcedx-vscode-metadata/src/shared/retrieve/retrieveComponentSet.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import { formatRetrieveOutput } from './formatRetrieveOutput';
1616
/** Retrieve a ComponentSet, handling empty sets, cancellation, and output formatting */
1717
export const retrieveComponentSet = Effect.fn('retrieveComponentSet')(function* (options: {
1818
componentSet: ComponentSet;
19+
ignoreConflicts?: boolean;
1920
}) {
20-
const { componentSet } = options;
21+
const { componentSet, ignoreConflicts } = options;
2122
const api = yield* (yield* ExtensionProviderService).getServicesApi;
2223
const [channelService, retrieveService, componentSetService] = yield* Effect.all(
2324
[api.services.ChannelService, api.services.MetadataRetrieveService, api.services.ComponentSetService],
@@ -27,7 +28,7 @@ export const retrieveComponentSet = Effect.fn('retrieveComponentSet')(function*
2728
const componentCount = componentSet.size;
2829
yield* channelService.appendToChannel(`Retrieving ${componentCount} component${componentCount === 1 ? '' : 's'}...`);
2930

30-
const result = yield* retrieveService.retrieveComponentSet(componentSet);
31+
const result = yield* retrieveService.retrieveComponentSet(componentSet, { ignoreConflicts });
3132

3233
// Handle cancellation
3334
if (typeof result === 'string') {

packages/salesforcedx-vscode-services/src/core/metadataRetrieveService.ts

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { ConnectionService } from './connectionService';
2828
import { MetadataRegistryService } from './metadataRegistryService';
2929
import { ProjectService } from './projectService';
3030
import { unknownToErrorCause } from './shared';
31-
import { SourceTrackingService } from './sourceTrackingService';
31+
import { SourceTrackingService, type SourceTrackingOptions } from './sourceTrackingService';
3232

3333
export class MetadataRetrieveError extends Data.TaggedError('MetadataRetrieveError')<{
3434
readonly cause: unknown;
@@ -63,7 +63,7 @@ const buildComponentSet = (members: MetadataMember[]) =>
6363
});
6464
}).pipe(Effect.withSpan('buildComponentSet'));
6565

66-
const retrieve = (members: MetadataMember[]) =>
66+
const retrieve = (members: MetadataMember[], options?: SourceTrackingOptions) =>
6767
Effect.gen(function* () {
6868
const [connection, project, registryAccess] = yield* Effect.all(
6969
[
@@ -77,6 +77,17 @@ const retrieve = (members: MetadataMember[]) =>
7777

7878
const componentSet = yield* buildComponentSet(members);
7979

80+
const tracking = yield* Effect.flatMap(SourceTrackingService, svc => svc.getSourceTracking(options));
81+
if (tracking) {
82+
yield* Effect.promise(() => tracking.reReadLocalTrackingCache()).pipe(
83+
Effect.withSpan('STL.ReReadLocalTrackingCache')
84+
);
85+
86+
if (!options?.ignoreConflicts) {
87+
yield* Effect.flatMap(SourceTrackingService, svc => svc.checkConflicts(tracking));
88+
}
89+
}
90+
8091
const title = `Retrieving ${members.map(m => `${m.type}: ${m.fullName === '*' ? 'all' : m.fullName}`).join(', ')}`;
8192
return yield* performRetrieveOperation(componentSet, connection, project, registryAccess, title);
8293
}).pipe(Effect.withSpan('retrieve', { attributes: { members } }));
@@ -90,41 +101,39 @@ const performRetrieveOperation = (
90101
title: string
91102
) =>
92103
Effect.gen(function* () {
93-
const retrieveFiber = yield* Effect.fork(
94-
Effect.tryPromise({
95-
try: async () => {
96-
const retrieveOperation = new MetadataApiRetrieve({
97-
usernameOrConnection: connection,
98-
components: componentSet,
99-
output: project.getDefaultPackage().fullPath,
100-
format: 'source',
101-
merge: true,
102-
registry: registryAccess
103-
});
104-
105-
const retrieveResult = await vscode.window.withProgress(
106-
{
107-
location: vscode.ProgressLocation.Notification,
108-
title,
109-
cancellable: true
110-
},
111-
async (_, token) => {
112-
token.onCancellationRequested(async () => {
113-
await retrieveOperation.cancel();
114-
await Effect.runPromise(Fiber.interrupt(retrieveFiber));
115-
});
116-
await retrieveOperation.start();
117-
return await retrieveOperation.pollStatus();
118-
}
119-
);
120-
return retrieveResult;
121-
},
122-
catch: e => {
123-
console.error(e);
124-
return new MetadataRetrieveError(unknownToErrorCause(e));
125-
}
126-
}).pipe(Effect.withSpan('retrieve (API call)'))
127-
);
104+
const retrieveFiber = yield* Effect.tryPromise({
105+
try: async () => {
106+
const retrieveOperation = new MetadataApiRetrieve({
107+
usernameOrConnection: connection,
108+
components: componentSet,
109+
output: project.getDefaultPackage().fullPath,
110+
format: 'source',
111+
merge: true,
112+
registry: registryAccess
113+
});
114+
115+
const retrieveResult = await vscode.window.withProgress(
116+
{
117+
location: vscode.ProgressLocation.Notification,
118+
title,
119+
cancellable: true
120+
},
121+
async (_, token) => {
122+
token.onCancellationRequested(async () => {
123+
await retrieveOperation.cancel();
124+
await Effect.runPromise(Fiber.interrupt(retrieveFiber));
125+
});
126+
await retrieveOperation.start();
127+
return await retrieveOperation.pollStatus();
128+
}
129+
);
130+
return retrieveResult;
131+
},
132+
catch: e => {
133+
console.error(e);
134+
return new MetadataRetrieveError(unknownToErrorCause(e));
135+
}
136+
}).pipe(Effect.withSpan('retrieve (API call)'), Effect.fork);
128137

129138
const retrieveOutcome = yield* Effect.matchCauseEffect(Fiber.join(retrieveFiber), {
130139
onFailure: cause =>
@@ -145,7 +154,7 @@ const performRetrieveOperation = (
145154
});
146155

147156
/** Retrieve metadata using a ComponentSet directly */
148-
const retrieveComponentSet = (components: ComponentSet) =>
157+
const retrieveComponentSet = (components: ComponentSet, options?: SourceTrackingOptions) =>
149158
Effect.gen(function* () {
150159
yield* Effect.annotateCurrentSpan({ components: components.size });
151160
const [connection, project, registryAccess, configAggregator] = yield* Effect.all(
@@ -161,6 +170,17 @@ const retrieveComponentSet = (components: ComponentSet) =>
161170

162171
yield* setComponentSetProperties(components, project, configAggregator);
163172

173+
const tracking = yield* Effect.flatMap(SourceTrackingService, svc => svc.getSourceTracking(options));
174+
if (tracking) {
175+
yield* Effect.promise(() => tracking.reReadLocalTrackingCache()).pipe(
176+
Effect.withSpan('STL.ReReadLocalTrackingCache')
177+
);
178+
179+
if (!options?.ignoreConflicts) {
180+
yield* Effect.flatMap(SourceTrackingService, svc => svc.checkConflicts(tracking));
181+
}
182+
}
183+
164184
const title = `Retrieving ${components.size} component${components.size === 1 ? '' : 's'}`;
165185
return yield* performRetrieveOperation(components, connection, project, registryAccess, title);
166186
}).pipe(Effect.withSpan('retrieveComponentSet', { attributes: { componentCount: components.size } }));

packages/salesforcedx-vscode-services/src/core/sourceTrackingService.ts

Lines changed: 39 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { DeployResult, RetrieveResult } from '@salesforce/source-deploy-ret
99
import { type SourceTracking } from '@salesforce/source-tracking';
1010
import * as Data from 'effect/Data';
1111
import * as Effect from 'effect/Effect';
12-
import * as Layer from 'effect/Layer';
1312
import * as SubscriptionRef from 'effect/SubscriptionRef';
1413
import { ChannelService } from '../vscode/channelService';
1514
import { SettingsService } from '../vscode/settingsService';
@@ -113,69 +112,50 @@ const checkConflicts = (tracking: SourceTracking) =>
113112
);
114113
});
115114

116-
export class SourceTrackingService extends Effect.Service<SourceTrackingService>()('SourceTrackingService', {
117-
succeed: {
118-
getSourceTrackingOrThrow: (options?: SourceTrackingOptions) => getTrackingOrThrow(options),
119-
getSourceTracking: (options?: SourceTrackingOptions) => getTracking(options),
120-
checkConflicts: (tracking: SourceTracking) => checkConflicts(tracking),
121-
updateTrackingFromRetrieve: (result: RetrieveResult) =>
122-
Effect.gen(function* () {
123-
yield* Effect.annotateCurrentSpan({ files: result.getFileResponses().map(r => r.filePath) });
124-
const tracking = yield* getTracking({ ignoreConflicts: true });
125-
return tracking
126-
? yield* Effect.tryPromise({
127-
try: () => tracking.updateTrackingFromRetrieve(result),
115+
/** safe to pass a result to. If tracking is not enabled, this will be a no-op */
116+
const updateTrackingFromRetrieve = (result: RetrieveResult) =>
117+
Effect.gen(function* () {
118+
yield* Effect.annotateCurrentSpan({ files: result.getFileResponses().map(r => r.filePath) });
119+
const tracking = yield* getTracking({ ignoreConflicts: true });
120+
return tracking
121+
? yield* Effect.tryPromise({
122+
try: () => tracking.updateTrackingFromRetrieve(result),
123+
catch: error => {
124+
console.error(error);
125+
return new SourceTrackingError(unknownToErrorCause(error));
126+
}
127+
}).pipe(Effect.withSpan('trackingUpdate'))
128+
: yield* Effect.succeed(undefined);
129+
}).pipe(Effect.withSpan('SourceTrackingService.updateTrackingFromRetrieve'));
130+
131+
/** safe to pass a result to. If tracking is not enabled, this will be a no-op */
132+
const updateTrackingFromDeploy = (result: DeployResult) =>
133+
Effect.gen(function* () {
134+
const tracking = yield* getTracking({ ignoreConflicts: true });
135+
return tracking
136+
? yield* Effect.all(
137+
[
138+
Effect.tryPromise({
139+
try: () => tracking.updateTrackingFromDeploy(result),
128140
catch: error => {
129141
console.error(error);
130142
return new SourceTrackingError(unknownToErrorCause(error));
131143
}
132-
}).pipe(Effect.withSpan('trackingUpdate'))
133-
: yield* Effect.succeed(undefined);
134-
}).pipe(
135-
Effect.withSpan('SourceTrackingService.updateTrackingFromRetrieve'),
136-
Effect.provide(
137-
Layer.mergeAll(
138-
ConfigService.Default,
139-
SettingsService.Default,
140-
WorkspaceService.Default,
141-
ProjectService.Default,
142-
ConnectionService.Default,
143-
MetadataRegistryService.Default
144-
)
144+
}).pipe(Effect.withSpan('trackingUpdate in STL')),
145+
Effect.annotateCurrentSpan({ files: result.getFileResponses().map(r => r.filePath) })
146+
],
147+
{ concurrency: 'unbounded' }
145148
)
146-
),
147-
/** safe to pass a result to. If tracking is not enabled, this will be a no-op */
148-
updateTrackingFromDeploy: (result: DeployResult) =>
149-
Effect.gen(function* () {
150-
const tracking = yield* getTracking({ ignoreConflicts: true });
151-
return tracking
152-
? yield* Effect.all(
153-
[
154-
Effect.tryPromise({
155-
try: () => tracking.updateTrackingFromDeploy(result),
156-
catch: error => {
157-
console.error(error);
158-
return new SourceTrackingError(unknownToErrorCause(error));
159-
}
160-
}).pipe(Effect.withSpan('trackingUpdate in STL')),
161-
Effect.annotateCurrentSpan({ files: result.getFileResponses().map(r => r.filePath) })
162-
],
163-
{ concurrency: 'unbounded' }
164-
)
165-
: yield* Effect.succeed(undefined);
166-
}).pipe(
167-
Effect.withSpan('SourceTrackingService.updateTrackingFromDeploy'),
168-
Effect.provide(
169-
Layer.mergeAll(
170-
ConfigService.Default,
171-
SettingsService.Default,
172-
WorkspaceService.Default,
173-
ProjectService.Default,
174-
ConnectionService.Default,
175-
MetadataRegistryService.Default
176-
)
177-
)
178-
)
149+
: yield* Effect.succeed(undefined);
150+
}).pipe(Effect.withSpan('SourceTrackingService.updateTrackingFromDeploy'));
151+
152+
export class SourceTrackingService extends Effect.Service<SourceTrackingService>()('SourceTrackingService', {
153+
succeed: {
154+
getSourceTrackingOrThrow: (options?: SourceTrackingOptions) => getTrackingOrThrow(options),
155+
getSourceTracking: (options?: SourceTrackingOptions) => getTracking(options),
156+
checkConflicts: (tracking: SourceTracking) => checkConflicts(tracking),
157+
updateTrackingFromRetrieve,
158+
updateTrackingFromDeploy
179159
} as const,
180160
dependencies: [
181161
ConnectionService.Default,

0 commit comments

Comments
 (0)