@salesforce/source-tracking 0.2.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,48 @@
1
- import { FileResponse } from '@salesforce/source-deploy-retrieve';
1
+ import { FileResponse, SourceComponent } from '@salesforce/source-deploy-retrieve';
2
+ export interface ChangeOptions {
3
+ origin: 'local' | 'remote';
4
+ state: 'add' | 'delete' | 'modify' | 'nondelete';
5
+ format: 'ChangeResult' | 'SourceComponent' | 'string' | 'ChangeResultWithPaths';
6
+ }
2
7
  export declare type RemoteSyncInput = Pick<FileResponse, 'fullName' | 'filePath' | 'type' | 'state'>;
8
+ export declare type StatusOutputRow = Pick<FileResponse, 'fullName' | 'filePath' | 'type'> & {
9
+ conflict?: boolean;
10
+ ignored?: boolean;
11
+ } & Pick<ChangeOptions, 'origin' | 'state'>;
12
+ export interface LocalUpdateOptions {
13
+ files?: string[];
14
+ deletedFiles?: string[];
15
+ }
16
+ export declare type RemoteChangeElement = {
17
+ name: string;
18
+ type: string;
19
+ deleted?: boolean;
20
+ modified?: boolean;
21
+ };
22
+ /**
23
+ * Summary type that supports both local and remote change types
24
+ */
25
+ export declare type ChangeResult = Partial<RemoteChangeElement> & {
26
+ origin: 'local' | 'remote';
27
+ filenames?: string[];
28
+ ignored?: boolean;
29
+ };
30
+ export declare type MemberRevision = {
31
+ serverRevisionCounter: number;
32
+ lastRetrievedFromServer: number | null;
33
+ memberType: string;
34
+ isNameObsolete: boolean;
35
+ };
36
+ export declare type SourceMember = {
37
+ MemberType: string;
38
+ MemberName: string;
39
+ IsNameObsolete: boolean;
40
+ RevisionCounter: number;
41
+ ignored?: boolean;
42
+ };
43
+ export interface ConflictError {
44
+ message: string;
45
+ name: 'conflict';
46
+ conflicts: ChangeResult[];
47
+ }
48
+ export declare type ChangeOptionType = ChangeResult | SourceComponent | string;
@@ -1,36 +1,11 @@
1
1
  import { Org, SfdxProject } from '@salesforce/core';
2
2
  import { AsyncCreatable } from '@salesforce/kit';
3
3
  import { ComponentSet, SourceComponent, FileResponse } from '@salesforce/source-deploy-retrieve';
4
- import { RemoteChangeElement } from './shared/remoteSourceTrackingService';
5
- import { RemoteSyncInput } from './shared/types';
6
- export declare const getKeyFromObject: (element: RemoteChangeElement | ChangeResult) => string;
7
- export declare const getKeyFromStrings: (metadataType: string, metadataName: string) => string;
8
- export declare type ChangeOptionType = ChangeResult | SourceComponent | string;
9
- export interface ChangeOptions {
10
- origin: 'local' | 'remote';
11
- state: 'add' | 'delete' | 'modify' | 'nondelete';
12
- format: 'ChangeResult' | 'SourceComponent' | 'string';
13
- }
14
- export interface LocalUpdateOptions {
15
- files?: string[];
16
- deletedFiles?: string[];
17
- }
18
- /**
19
- * Summary type that supports both local and remote change types
20
- */
21
- export declare type ChangeResult = Partial<RemoteChangeElement> & {
22
- origin: 'local' | 'remote';
23
- filenames?: string[];
24
- };
25
- export interface ConflictError {
26
- message: string;
27
- name: 'conflict';
28
- conflicts: ChangeResult[];
29
- }
4
+ import { RemoteSyncInput, StatusOutputRow, ChangeOptions, ChangeResult, ChangeOptionType, LocalUpdateOptions } from './shared/types';
30
5
  export interface SourceTrackingOptions {
31
6
  org: Org;
32
7
  project: SfdxProject;
33
- /** defaults to sfdxProject sourceApiVersion unless provided */
8
+ /** @deprecated not used defaults to sfdxProject sourceApiVersion unless provided */
34
9
  apiVersion?: string;
35
10
  }
36
11
  /**
@@ -41,19 +16,34 @@ export interface SourceTrackingOptions {
41
16
  */
42
17
  export declare class SourceTracking extends AsyncCreatable {
43
18
  private orgId;
19
+ private project;
44
20
  private projectPath;
45
21
  private packagesDirs;
46
22
  private username;
47
23
  private logger;
48
24
  private localRepo;
49
25
  private remoteSourceTrackingService;
26
+ private forceIgnore;
50
27
  constructor(options: SourceTrackingOptions);
51
28
  init(): Promise<void>;
52
29
  localChangesAsComponentSet(): Promise<ComponentSet>;
30
+ /**
31
+ * Does most of the work for the force:source:status command.
32
+ * Outputs need a bit of massage since this aims to provide nice json.
33
+ *
34
+ * @param local you want local status
35
+ * @param remote you want remote status
36
+ * @returns StatusOutputRow[]
37
+ */
38
+ getStatus({ local, remote }: {
39
+ local: boolean;
40
+ remote: boolean;
41
+ }): Promise<StatusOutputRow[]>;
53
42
  /**
54
43
  * Get metadata changes made locally and in the org.
55
44
  *
56
45
  * @returns local and remote changed metadata
46
+ *
57
47
  */
58
48
  getChanges<T extends ChangeOptionType>(options?: ChangeOptions): Promise<T[]>;
59
49
  /**
@@ -103,22 +93,26 @@ export declare class SourceTracking extends AsyncCreatable {
103
93
  * Sets the files to max revision so that no changes appear
104
94
  */
105
95
  resetRemoteTracking(serverRevision?: number): Promise<number>;
96
+ /**
97
+ * Compares local and remote changes to detect conflicts
98
+ */
99
+ getConflicts(): Promise<ChangeResult[]>;
106
100
  /**
107
101
  * uses SDR to translate remote metadata records into local file paths (which only typically have the filename).
108
102
  *
109
103
  * @input elements: ChangeResult[]
110
104
  * @input excludeUnresolvables: boolean Filter out components where you can't get the name and type (that is, it's probably not a valid source component)
105
+ * @input resolveDeleted: constructs a virtualTree instead of the actual filesystem--useful when the files no longer exist
106
+ * @input useFsForceIgnore: (default behavior) use forceIgnore from the filesystem. If false, uses the base forceIgnore from SDR
111
107
  */
112
- populateTypesAndNames({ elements, excludeUnresolvable, resolveDeleted, }: {
113
- elements: ChangeResult[];
114
- excludeUnresolvable?: boolean;
115
- resolveDeleted?: boolean;
116
- }): ChangeResult[];
117
- getConflicts(): Promise<ChangeResult[]>;
108
+ private populateTypesAndNames;
109
+ private getLocalStatusRows;
118
110
  /**
119
111
  * uses SDR to translate remote metadata records into local file paths
120
112
  */
121
113
  private populateFilePaths;
122
114
  private ensureRelative;
115
+ private getLocalChangesAsFilenames;
116
+ private localChangesToOutputRow;
117
+ private remoteChangesToOutputRows;
123
118
  }
124
- export declare const stringGuard: (input: string | undefined) => input is string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.stringGuard = exports.SourceTracking = exports.getKeyFromStrings = exports.getKeyFromObject = void 0;
3
+ exports.SourceTracking = void 0;
4
4
  /*
5
5
  * Copyright (c) 2021, salesforce.com, inc.
6
6
  * All rights reserved.
@@ -9,21 +9,16 @@ exports.stringGuard = exports.SourceTracking = exports.getKeyFromStrings = expor
9
9
  */
10
10
  const fs = require("fs");
11
11
  const path = require("path");
12
+ const os_1 = require("os");
12
13
  const core_1 = require("@salesforce/core");
13
14
  const kit_1 = require("@salesforce/kit");
15
+ const ts_types_1 = require("@salesforce/ts-types");
14
16
  const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve");
15
17
  const remoteSourceTrackingService_1 = require("./shared/remoteSourceTrackingService");
16
18
  const localShadowRepo_1 = require("./shared/localShadowRepo");
17
19
  const filenamesToVirtualTree_1 = require("./shared/filenamesToVirtualTree");
18
- const getKeyFromObject = (element) => {
19
- if (element.type && element.name) {
20
- return (0, remoteSourceTrackingService_1.getMetadataKey)(element.type, element.name);
21
- }
22
- throw new Error(`unable to complete key from ${JSON.stringify(element)}`);
23
- };
24
- exports.getKeyFromObject = getKeyFromObject;
25
- // external users of SDR might need to convert a fileResponse to a key
26
- exports.getKeyFromStrings = remoteSourceTrackingService_1.getMetadataKey;
20
+ const guards_1 = require("./shared/guards");
21
+ const functions_1 = require("./shared/functions");
27
22
  /**
28
23
  * Manages source tracking files (remote and local)
29
24
  *
@@ -38,13 +33,18 @@ class SourceTracking extends kit_1.AsyncCreatable {
38
33
  this.projectPath = options.project.getPath();
39
34
  this.packagesDirs = options.project.getPackageDirectories();
40
35
  this.logger = core_1.Logger.childFromRoot('SourceTracking');
36
+ this.project = options.project;
41
37
  }
42
38
  async init() {
43
- // reserved for future use. If not, can remove asyncCreatable
39
+ // reserved for future use
44
40
  }
45
41
  async localChangesAsComponentSet() {
46
- await this.ensureLocalTracking();
42
+ const [projectConfig] = await Promise.all([this.project.resolveProjectConfig(), this.ensureLocalTracking()]);
43
+ const sourceApiVersion = (0, ts_types_1.getString)(projectConfig, 'sourceApiVersion');
47
44
  const componentSet = new source_deploy_retrieve_1.ComponentSet();
45
+ if (sourceApiVersion) {
46
+ componentSet.sourceApiVersion = sourceApiVersion;
47
+ }
48
48
  const [nonDeletes, deletes] = await Promise.all([
49
49
  this.localRepo.getNonDeleteFilenames(),
50
50
  this.localRepo.getDeleteFilenames(),
@@ -68,39 +68,59 @@ class SourceTracking extends kit_1.AsyncCreatable {
68
68
  return undefined;
69
69
  }
70
70
  })
71
- .filter(sourceComponentGuard)
71
+ .filter(guards_1.sourceComponentGuard)
72
72
  .map((component) => componentSet.add(component));
73
73
  deletes
74
74
  .flatMap((filename) => resolverForDeletes.getComponentsFromPath(filename))
75
- .filter(sourceComponentGuard)
76
- .map((component) => componentSet.add(component, true));
75
+ .filter(guards_1.sourceComponentGuard)
76
+ .map((component) => componentSet.add(component, source_deploy_retrieve_1.DestructiveChangesType.POST));
77
77
  return componentSet;
78
78
  }
79
+ /**
80
+ * Does most of the work for the force:source:status command.
81
+ * Outputs need a bit of massage since this aims to provide nice json.
82
+ *
83
+ * @param local you want local status
84
+ * @param remote you want remote status
85
+ * @returns StatusOutputRow[]
86
+ */
87
+ async getStatus({ local, remote }) {
88
+ let results = [];
89
+ if (local) {
90
+ results = results.concat(await this.getLocalStatusRows());
91
+ }
92
+ if (remote) {
93
+ await this.ensureRemoteTracking(true);
94
+ const [remoteDeletes, remoteModifies] = await Promise.all([
95
+ this.getChanges({ origin: 'remote', state: 'delete', format: 'ChangeResult' }),
96
+ this.getChanges({ origin: 'remote', state: 'nondelete', format: 'ChangeResultWithPaths' }),
97
+ ]);
98
+ results = results.concat((await Promise.all(remoteDeletes.concat(remoteModifies).map((item) => this.remoteChangesToOutputRows(item)))).flat(1));
99
+ }
100
+ if (local && remote) {
101
+ // keys like ApexClass__MyClass.cls
102
+ const conflictFiles = (await this.getConflicts()).flatMap((conflict) => conflict.filenames).filter(guards_1.stringGuard);
103
+ results = results.map((row) => ({
104
+ ...row,
105
+ conflict: !!row.filePath && conflictFiles.includes(row.filePath),
106
+ }));
107
+ }
108
+ return results;
109
+ }
79
110
  /**
80
111
  * Get metadata changes made locally and in the org.
81
112
  *
82
113
  * @returns local and remote changed metadata
114
+ *
83
115
  */
84
116
  async getChanges(options) {
85
117
  if ((options === null || options === void 0 ? void 0 : options.origin) === 'local') {
86
- let filenames = [];
87
118
  await this.ensureLocalTracking();
88
- if (options.state === 'modify') {
89
- filenames = await this.localRepo.getModifyFilenames();
90
- }
91
- if (options.state === 'nondelete') {
92
- filenames = await this.localRepo.getNonDeleteFilenames();
93
- }
94
- if (options.state === 'delete') {
95
- filenames = await this.localRepo.getDeleteFilenames();
96
- }
97
- if (options.state === 'add') {
98
- filenames = await this.localRepo.getAddFilenames();
99
- }
119
+ const filenames = await this.getLocalChangesAsFilenames(options.state);
100
120
  if (options.format === 'string') {
101
121
  return filenames;
102
122
  }
103
- if (options.format === 'ChangeResult') {
123
+ if (options.format === 'ChangeResult' || options.format === 'ChangeResultWithPaths') {
104
124
  return filenames.map((filename) => ({
105
125
  filenames: [filename],
106
126
  origin: 'local',
@@ -120,35 +140,25 @@ class SourceTracking extends kit_1.AsyncCreatable {
120
140
  return undefined;
121
141
  }
122
142
  })
123
- .filter(sourceComponentGuard);
143
+ .filter(guards_1.sourceComponentGuard);
124
144
  }
125
145
  }
126
- if (options && options.origin === 'remote') {
146
+ if ((options === null || options === void 0 ? void 0 : options.origin) === 'remote') {
127
147
  await this.ensureRemoteTracking();
128
148
  const remoteChanges = await this.remoteSourceTrackingService.retrieveUpdates();
129
149
  this.logger.debug('remoteChanges', remoteChanges);
130
- let filteredChanges = [];
131
- if (options.state === 'add') {
132
- filteredChanges = remoteChanges.filter((change) => !change.deleted && !change.modified);
133
- }
134
- else if (options.state === 'modify') {
135
- filteredChanges = remoteChanges.filter((change) => change.modified);
136
- }
137
- else if (options.state === 'delete') {
138
- filteredChanges = remoteChanges.filter((change) => change.deleted);
139
- }
140
- else if (options.state === 'nondelete') {
141
- filteredChanges = remoteChanges.filter((change) => !change.deleted);
142
- }
150
+ const filteredChanges = remoteChanges.filter(remoteFilterByState[options.state]);
143
151
  if (options.format === 'ChangeResult') {
144
- return filteredChanges.map((change) => ({ ...change, origin: 'remote' }));
152
+ return filteredChanges.map((change) => (0, remoteSourceTrackingService_1.remoteChangeElementToChangeResult)(change));
153
+ }
154
+ if (options.format === 'ChangeResultWithPaths') {
155
+ return this.populateFilePaths(filteredChanges.map((change) => (0, remoteSourceTrackingService_1.remoteChangeElementToChangeResult)(change)));
145
156
  }
146
157
  // turn it into a componentSet to resolve filenames
147
- const remoteChangesAsComponentLike = filteredChanges.map((element) => ({
158
+ const remoteChangesAsComponentSet = new source_deploy_retrieve_1.ComponentSet(filteredChanges.map((element) => ({
148
159
  type: element === null || element === void 0 ? void 0 : element.type,
149
160
  fullName: element === null || element === void 0 ? void 0 : element.name,
150
- }));
151
- const remoteChangesAsComponentSet = new source_deploy_retrieve_1.ComponentSet(remoteChangesAsComponentLike);
161
+ })));
152
162
  const matchingLocalSourceComponentsSet = source_deploy_retrieve_1.ComponentSet.fromSource({
153
163
  fsPaths: this.packagesDirs.map((dir) => dir.path),
154
164
  include: remoteChangesAsComponentSet,
@@ -163,10 +173,7 @@ class SourceTracking extends kit_1.AsyncCreatable {
163
173
  return matchingLocalSourceComponentsSet.getSourceComponents().toArray();
164
174
  }
165
175
  }
166
- // by default return all local and remote changes
167
- // eslint-disable-next-line no-console
168
- this.logger.debug(options);
169
- return [];
176
+ throw new Error(`unsupported options: ${JSON.stringify(options)}`);
170
177
  }
171
178
  /**
172
179
  *
@@ -240,7 +247,7 @@ class SourceTracking extends kit_1.AsyncCreatable {
240
247
  if (this.localRepo) {
241
248
  return;
242
249
  }
243
- this.localRepo = await localShadowRepo_1.ShadowRepo.create({
250
+ this.localRepo = await localShadowRepo_1.ShadowRepo.getInstance({
244
251
  orgId: this.orgId,
245
252
  projectPath: this.projectPath,
246
253
  packageDirs: this.packagesDirs,
@@ -305,20 +312,79 @@ class SourceTracking extends kit_1.AsyncCreatable {
305
312
  const resetMembers = await this.remoteSourceTrackingService.reset(serverRevision);
306
313
  return resetMembers.length;
307
314
  }
315
+ /**
316
+ * Compares local and remote changes to detect conflicts
317
+ */
318
+ async getConflicts() {
319
+ // we're going to need have both initialized
320
+ await Promise.all([this.ensureRemoteTracking(), this.ensureLocalTracking()]);
321
+ // Strategy: check local changes first (since it'll be faster) to avoid callout
322
+ // early return if either local or remote is empty
323
+ const localChanges = await this.getChanges({
324
+ state: 'nondelete',
325
+ origin: 'local',
326
+ format: 'ChangeResult',
327
+ });
328
+ if (localChanges.length === 0) {
329
+ return [];
330
+ }
331
+ const remoteChanges = await this.getChanges({
332
+ origin: 'remote',
333
+ state: 'nondelete',
334
+ // remote adds won't have a filename, so we ask for it to be resolved
335
+ format: 'ChangeResultWithPaths',
336
+ });
337
+ if (remoteChanges.length === 0) {
338
+ return [];
339
+ }
340
+ // index the remoteChanges by filename
341
+ const fileNameIndex = new Map();
342
+ const metadataKeyIndex = new Map();
343
+ remoteChanges.map((change) => {
344
+ var _a;
345
+ if (change.name && change.type) {
346
+ metadataKeyIndex.set((0, functions_1.getMetadataKey)(change.name, change.type), change);
347
+ }
348
+ (_a = change.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
349
+ fileNameIndex.set(filename, change);
350
+ });
351
+ });
352
+ const conflicts = new Set();
353
+ this.populateTypesAndNames({ elements: localChanges, excludeUnresolvable: true }).map((change) => {
354
+ var _a;
355
+ const metadataKey = (0, functions_1.getMetadataKey)(change.name, change.type);
356
+ // option 1: name and type match
357
+ if (metadataKeyIndex.has(metadataKey)) {
358
+ conflicts.add({ ...metadataKeyIndex.get(metadataKey) });
359
+ }
360
+ else {
361
+ // option 2: some of the filenames match
362
+ (_a = change.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
363
+ if (fileNameIndex.has(filename)) {
364
+ conflicts.add({ ...fileNameIndex.get(filename) });
365
+ }
366
+ });
367
+ }
368
+ });
369
+ // deeply de-dupe
370
+ return Array.from(conflicts);
371
+ }
308
372
  /**
309
373
  * uses SDR to translate remote metadata records into local file paths (which only typically have the filename).
310
374
  *
311
375
  * @input elements: ChangeResult[]
312
376
  * @input excludeUnresolvables: boolean Filter out components where you can't get the name and type (that is, it's probably not a valid source component)
377
+ * @input resolveDeleted: constructs a virtualTree instead of the actual filesystem--useful when the files no longer exist
378
+ * @input useFsForceIgnore: (default behavior) use forceIgnore from the filesystem. If false, uses the base forceIgnore from SDR
313
379
  */
314
- populateTypesAndNames({ elements, excludeUnresolvable = false, resolveDeleted = false, }) {
380
+ populateTypesAndNames({ elements, excludeUnresolvable = false, resolveDeleted = false, useFsForceIgnore = true, }) {
315
381
  if (elements.length === 0) {
316
382
  return [];
317
383
  }
318
384
  this.logger.debug(`populateTypesAndNames for ${elements.length} change elements`);
319
- const filenames = elements.flatMap((element) => element.filenames).filter(exports.stringGuard);
385
+ const filenames = elements.flatMap((element) => element.filenames).filter(guards_1.stringGuard);
320
386
  // component set generated from the filenames on all local changes
321
- const resolver = new source_deploy_retrieve_1.MetadataResolver(undefined, resolveDeleted ? (0, filenamesToVirtualTree_1.filenamesToVirtualTree)(filenames) : undefined);
387
+ const resolver = new source_deploy_retrieve_1.MetadataResolver(undefined, resolveDeleted ? (0, filenamesToVirtualTree_1.filenamesToVirtualTree)(filenames) : undefined, useFsForceIgnore);
322
388
  const sourceComponents = filenames
323
389
  .flatMap((filename) => {
324
390
  try {
@@ -329,7 +395,7 @@ class SourceTracking extends kit_1.AsyncCreatable {
329
395
  return undefined;
330
396
  }
331
397
  })
332
- .filter(sourceComponentGuard);
398
+ .filter(guards_1.sourceComponentGuard);
333
399
  this.logger.debug(` matching SourceComponents have ${sourceComponents.length} items from local`);
334
400
  // make it simpler to find things later
335
401
  const elementMap = new Map();
@@ -341,15 +407,24 @@ class SourceTracking extends kit_1.AsyncCreatable {
341
407
  });
342
408
  // iterates the local components and sets their filenames
343
409
  sourceComponents.map((matchingComponent) => {
410
+ var _a;
344
411
  if ((matchingComponent === null || matchingComponent === void 0 ? void 0 : matchingComponent.fullName) && (matchingComponent === null || matchingComponent === void 0 ? void 0 : matchingComponent.type.name)) {
345
412
  const filenamesFromMatchingComponent = [matchingComponent.xml, ...matchingComponent.walkContent()];
413
+ // Set the ignored status at the component level so it can apply to all its files, some of which may not match the ignoreFile (ex: ApexClass)
414
+ this.forceIgnore = (_a = this.forceIgnore) !== null && _a !== void 0 ? _a : source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path);
415
+ const ignored = filenamesFromMatchingComponent
416
+ .filter(guards_1.stringGuard)
417
+ .filter((filename) => !filename.includes('__tests__'))
418
+ .some((filename) => this.forceIgnore.denies(filename));
346
419
  filenamesFromMatchingComponent.map((filename) => {
347
420
  if (filename && elementMap.has(filename)) {
348
421
  // add the type/name from the componentSet onto the element
349
422
  elementMap.set(filename, {
423
+ origin: 'remote',
350
424
  ...elementMap.get(filename),
351
425
  type: matchingComponent.type.name,
352
426
  name: matchingComponent.fullName,
427
+ ignored,
353
428
  });
354
429
  }
355
430
  });
@@ -359,35 +434,27 @@ class SourceTracking extends kit_1.AsyncCreatable {
359
434
  ? Array.from(new Set(elementMap.values())).filter((changeResult) => changeResult.name && changeResult.type)
360
435
  : Array.from(new Set(elementMap.values()));
361
436
  }
362
- async getConflicts() {
363
- // we're going to need have both initialized
364
- await Promise.all([this.ensureRemoteTracking(), this.ensureLocalTracking()]);
365
- const localChanges = await this.getChanges({
366
- state: 'nondelete',
367
- origin: 'local',
368
- format: 'ChangeResult',
437
+ async getLocalStatusRows() {
438
+ await this.ensureLocalTracking();
439
+ let results = [];
440
+ const localDeletes = this.populateTypesAndNames({
441
+ elements: await this.getChanges({ origin: 'local', state: 'delete', format: 'ChangeResult' }),
442
+ excludeUnresolvable: true,
443
+ resolveDeleted: true,
444
+ useFsForceIgnore: false,
369
445
  });
370
- // remote adds won't have a filename
371
- const remoteChanges = this.populateFilePaths(await this.getChanges({ origin: 'remote', state: 'nondelete', format: 'ChangeResult' }));
372
- // index them by filename
373
- const fileNameIndex = new Map();
374
- remoteChanges.map((change) => {
375
- var _a;
376
- (_a = change.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
377
- fileNameIndex.set(filename, change);
378
- });
446
+ const localAdds = this.populateTypesAndNames({
447
+ elements: await this.getChanges({ origin: 'local', state: 'add', format: 'ChangeResult' }),
448
+ excludeUnresolvable: true,
449
+ useFsForceIgnore: false,
379
450
  });
380
- const conflicts = new Set();
381
- localChanges.map((change) => {
382
- var _a;
383
- (_a = change.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
384
- if (fileNameIndex.has(filename)) {
385
- conflicts.add({ ...fileNameIndex.get(filename) });
386
- }
387
- });
451
+ const localModifies = this.populateTypesAndNames({
452
+ elements: await this.getChanges({ origin: 'local', state: 'modify', format: 'ChangeResult' }),
453
+ excludeUnresolvable: true,
454
+ useFsForceIgnore: false,
388
455
  });
389
- // deeply de-dupe
390
- return Array.from(conflicts);
456
+ results = results.concat(localAdds.flatMap((item) => this.localChangesToOutputRow(item, 'add')), localModifies.flatMap((item) => this.localChangesToOutputRow(item, 'modify')), localDeletes.flatMap((item) => this.localChangesToOutputRow(item, 'delete')));
457
+ return results;
391
458
  }
392
459
  /**
393
460
  * uses SDR to translate remote metadata records into local file paths
@@ -405,9 +472,11 @@ class SourceTracking extends kit_1.AsyncCreatable {
405
472
  const remoteChangesAsComponentSet = new source_deploy_retrieve_1.ComponentSet(remoteChangesAsComponentLike);
406
473
  this.logger.debug(` the generated component set has ${remoteChangesAsComponentSet.size.toString()} items`);
407
474
  if (remoteChangesAsComponentSet.size < elements.length) {
475
+ // iterate the elements to see which ones didn't make it into the component set
408
476
  throw new Error(`unable to generate complete component set for ${elements
409
- .map((element) => `${element.name}(${element.type})`)
410
- .join(',')}`);
477
+ .filter((element) => !remoteChangesAsComponentSet.has({ type: element === null || element === void 0 ? void 0 : element.type, fullName: element === null || element === void 0 ? void 0 : element.name }))
478
+ .map((element) => `${element.name} (${element.type})`)
479
+ .join(os_1.EOL)}`);
411
480
  }
412
481
  const matchingLocalSourceComponentsSet = source_deploy_retrieve_1.ComponentSet.fromSource({
413
482
  fsPaths: this.packagesDirs.map((dir) => dir.path),
@@ -417,16 +486,17 @@ class SourceTracking extends kit_1.AsyncCreatable {
417
486
  // make it simpler to find things later
418
487
  const elementMap = new Map();
419
488
  elements.map((element) => {
420
- elementMap.set((0, exports.getKeyFromObject)(element), element);
489
+ elementMap.set((0, functions_1.getKeyFromObject)(element), element);
421
490
  });
422
491
  // iterates the local components and sets their filenames
423
492
  for (const matchingComponent of matchingLocalSourceComponentsSet.getSourceComponents().toArray()) {
424
493
  if (matchingComponent.fullName && matchingComponent.type.name) {
425
494
  this.logger.debug(`${matchingComponent.fullName}|${matchingComponent.type.name} matches ${matchingComponent.xml} and maybe ${matchingComponent.walkContent().toString()}`);
426
- const key = (0, exports.getKeyFromStrings)(matchingComponent.type.name, matchingComponent.fullName);
495
+ const key = (0, functions_1.getMetadataKey)(matchingComponent.type.name, matchingComponent.fullName);
427
496
  elementMap.set(key, {
428
497
  ...elementMap.get(key),
429
498
  modified: true,
499
+ origin: 'remote',
430
500
  filenames: [matchingComponent.xml, ...matchingComponent.walkContent()].filter((filename) => filename),
431
501
  });
432
502
  }
@@ -436,13 +506,80 @@ class SourceTracking extends kit_1.AsyncCreatable {
436
506
  ensureRelative(filePath) {
437
507
  return path.isAbsolute(filePath) ? path.relative(this.projectPath, filePath) : filePath;
438
508
  }
509
+ async getLocalChangesAsFilenames(state) {
510
+ if (state === 'modify') {
511
+ return this.localRepo.getModifyFilenames();
512
+ }
513
+ if (state === 'nondelete') {
514
+ return this.localRepo.getNonDeleteFilenames();
515
+ }
516
+ if (state === 'delete') {
517
+ return this.localRepo.getDeleteFilenames();
518
+ }
519
+ if (state === 'add') {
520
+ return this.localRepo.getAddFilenames();
521
+ }
522
+ throw new Error(`unable to get local changes for state ${state}`);
523
+ }
524
+ localChangesToOutputRow(input, localType) {
525
+ var _a, _b, _c;
526
+ this.logger.debug('converting ChangeResult to a row', input);
527
+ const baseObject = {
528
+ type: (_a = input.type) !== null && _a !== void 0 ? _a : '',
529
+ origin: 'local',
530
+ state: localType,
531
+ fullName: (_b = input.name) !== null && _b !== void 0 ? _b : '',
532
+ // ignored property will be set in populateTypesAndNames
533
+ ignored: (_c = input.ignored) !== null && _c !== void 0 ? _c : false,
534
+ };
535
+ if (input.filenames) {
536
+ return input.filenames.map((filename) => ({
537
+ ...baseObject,
538
+ filePath: filename,
539
+ origin: 'local',
540
+ }));
541
+ }
542
+ throw new Error('no filenames found for local ChangeResult');
543
+ }
544
+ // this will eventually have async call to figure out the target file locations for remote changes
545
+ // eslint-disable-next-line @typescript-eslint/require-await
546
+ async remoteChangesToOutputRows(input) {
547
+ var _a, _b, _c, _d;
548
+ this.logger.debug('converting ChangeResult to a row', input);
549
+ this.forceIgnore = (_a = this.forceIgnore) !== null && _a !== void 0 ? _a : source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path);
550
+ const baseObject = {
551
+ type: (_b = input.type) !== null && _b !== void 0 ? _b : '',
552
+ origin: input.origin,
553
+ state: stateFromChangeResult(input),
554
+ fullName: (_c = input.name) !== null && _c !== void 0 ? _c : '',
555
+ };
556
+ // it's easy to check ignores if the filePaths exist locally
557
+ if ((_d = input.filenames) === null || _d === void 0 ? void 0 : _d.length) {
558
+ return input.filenames.map((filename) => ({
559
+ ...baseObject,
560
+ filePath: filename,
561
+ ignored: this.forceIgnore.denies(filename),
562
+ }));
563
+ }
564
+ // when the file doesn't exist locally, there are no filePaths
565
+ // So we can't say whether it's ignored or not
566
+ return [baseObject];
567
+ }
439
568
  }
440
569
  exports.SourceTracking = SourceTracking;
441
- const stringGuard = (input) => {
442
- return typeof input === 'string';
570
+ const remoteFilterByState = {
571
+ add: (change) => !change.deleted && !change.modified,
572
+ modify: (change) => change.modified === true,
573
+ delete: (change) => change.deleted === true,
574
+ nondelete: (change) => !change.deleted,
443
575
  };
444
- exports.stringGuard = stringGuard;
445
- const sourceComponentGuard = (input) => {
446
- return input instanceof source_deploy_retrieve_1.SourceComponent;
576
+ const stateFromChangeResult = (input) => {
577
+ if (input.deleted) {
578
+ return 'delete';
579
+ }
580
+ if (input.modified) {
581
+ return 'modify';
582
+ }
583
+ return 'add';
447
584
  };
448
585
  //# sourceMappingURL=sourceTracking.js.map