@@ -21,12 +21,14 @@ import { exec, ExecException, execSync } from 'child_process';
2121import { COutlineItem } from '../tree-structure/solution-outline-item' ;
2222import path from 'path' ;
2323import * as os from 'os' ;
24- import * as fs from 'fs' ;
2524import { ActiveSolutionTracker } from '../../../solutions/active-solution-tracker' ;
25+ import semver from 'semver' ;
26+ import { extractVersion } from '../../../utils/string-utils' ;
27+ import * as fsUtils from '../../../utils/fs-utils' ;
2628
2729export class MergeCommand {
2830 public static readonly mergeFile = `${ PACKAGE_NAME } .mergeFile` ;
29- private static readonly disallowedCmdChars = / [ \r \n & | < > ^ % " ' ] / ;
31+ private static readonly disallowedCmdChars = / [ \r \n & | < > ^ % " ' ] / ;
3032
3133 constructor (
3234 private readonly commandsProvider : CommandsProvider ,
@@ -47,23 +49,12 @@ export class MergeCommand {
4749 return ;
4850 }
4951
50- let local = node . getAttribute ( 'local' ) ;
51- if ( ! local ) {
52- vscode . window . showErrorMessage ( 'Required local file is missing to perform merge.' ) ;
52+ const mergeFiles = this . discoverMergeFiles ( node ) ;
53+ if ( ! mergeFiles ) {
5354 return ;
5455 }
5556
56- let update = node . getAttribute ( 'update' ) ;
57- if ( ! update ) {
58- vscode . window . showErrorMessage ( 'Required update file is missing to perform merge.' ) ;
59- return ;
60- }
61-
62- let base = node . getAttribute ( 'base' ) ;
63- if ( ! base ) {
64- vscode . window . showErrorMessage ( 'Required base file is missing to perform merge.' ) ;
65- return ;
66- }
57+ let { local, update, base } = mergeFiles ;
6758
6859 const codePath = this . getVSCodeExecutablePath ( ) ;
6960 if ( ! codePath ) {
@@ -75,7 +66,7 @@ export class MergeCommand {
7566 let merged = local + '.merged' ;
7667
7768 // make a copy of local to create merged file
78- fs . copyFileSync ( local , merged ) ;
69+ fsUtils . copyFile ( local , merged ) ;
7970 node . setAttribute ( 'merged' , merged ) ;
8071
8172 // ensure all paths are absolute
@@ -85,53 +76,22 @@ export class MergeCommand {
8576 merged = path . resolve ( merged ) ;
8677
8778 // get the modification time of the merged file before merge
88- let mergedMTimeBefore = 0 ;
89- if ( fs . existsSync ( merged ) ) {
90- mergedMTimeBefore = fs . statSync ( merged ) . mtimeMs ;
91- }
79+ const mergedMTimeBefore = fsUtils . getFileModificationTime ( merged ) ;
9280
9381 try {
9482 const command = this . buildMergeCommand ( codePath , local , update , base , merged ) ;
9583 const exitCode = await this . doOpen3WayMerge ( command ) ;
9684
9785 // get the modification time after merge
98- let mergedMTimeAfter = 0 ;
99- if ( fs . existsSync ( merged ) ) {
100- mergedMTimeAfter = fs . statSync ( merged ) . mtimeMs ;
101- }
86+ const mergedMTimeAfter = fsUtils . getFileModificationTime ( merged ) ;
10287
10388 if ( exitCode !== 0 ) {
10489 console . warn ( `Merge exited with code ${ exitCode } . Conflicts may exist.` ) ;
10590 return ;
10691 }
10792
108- // perform post-merge file operations
10993 if ( exitCode === 0 && mergedMTimeAfter > mergedMTimeBefore ) {
110- // create .bak file of local file
111- const backupPath = `${ local } .bak` ;
112- fs . copyFileSync ( local , backupPath ) ;
113-
114- // delete local file
115- if ( fs . existsSync ( local ) ) {
116- fs . unlinkSync ( local ) ;
117- }
118-
119- // delete base file
120- if ( fs . existsSync ( base ) ) {
121- fs . unlinkSync ( base ) ;
122- }
123-
124- // rename update file to base file
125- const newBaseFileName = path . basename ( update ) . replaceAll ( 'update' , 'base' ) ;
126- const baseDirPath = path . dirname ( update ) ;
127- const newBase = path . join ( baseDirPath , newBaseFileName ) ;
128- fs . renameSync ( update , newBase ) ;
129-
130- // rename merged file to local file
131- fs . renameSync ( merged , local ) ;
132-
133- // refresh tree view to update file status
134- this . activeSolutionTracker . triggerReload ( ) ;
94+ this . performPostMergeOperations ( local , update , base , merged ) ;
13595 }
13696
13797 } catch ( err ) {
@@ -141,6 +101,53 @@ export class MergeCommand {
141101 }
142102 }
143103
104+ private discoverMergeFiles ( node : COutlineItem ) : { local : string ; update : string ; base : string } | undefined {
105+ const local = node . getAttribute ( 'local' ) ;
106+ if ( ! local ) {
107+ vscode . window . showErrorMessage ( 'Required local file is missing to perform merge.' ) ;
108+ return undefined ;
109+ }
110+
111+ const discovered = this . findNewestMergeFiles ( local ) ;
112+ const update = discovered . update ;
113+ if ( ! update ) {
114+ vscode . window . showErrorMessage ( 'Required update file is missing to perform merge.' ) ;
115+ return undefined ;
116+ }
117+
118+ const base = discovered . base ;
119+ if ( ! base ) {
120+ vscode . window . showErrorMessage ( 'Required base file is missing to perform merge.' ) ;
121+ return undefined ;
122+ }
123+
124+ return { local, update, base } ;
125+ }
126+
127+ private performPostMergeOperations ( local : string , update : string , base : string , merged : string ) : void {
128+ // create .bak file of local file
129+ const backupPath = `${ local } .bak` ;
130+ fsUtils . copyFile ( local , backupPath ) ;
131+
132+ // delete local file
133+ fsUtils . deleteFileIfExists ( local ) ;
134+
135+ // delete base file
136+ fsUtils . deleteFileIfExists ( base ) ;
137+
138+ // rename update file to base file
139+ const newBaseFileName = path . basename ( update ) . replaceAll ( 'update' , 'base' ) ;
140+ const baseDirPath = path . dirname ( update ) ;
141+ const newBase = path . join ( baseDirPath , newBaseFileName ) ;
142+ fsUtils . renameFile ( update , newBase ) ;
143+
144+ // rename merged file to local file
145+ fsUtils . renameFile ( merged , local ) ;
146+
147+ // refresh tree view to update file status
148+ this . activeSolutionTracker . triggerReload ( ) ;
149+ }
150+
144151 private getVSCodeExecutablePath ( ) : string | undefined {
145152 const platform = os . platform ( ) ;
146153
@@ -154,7 +161,7 @@ export class MergeCommand {
154161 ] ;
155162
156163 for ( const p of possiblePaths ) {
157- if ( fs . existsSync ( p ) ) {
164+ if ( fsUtils . fileExists ( p ) ) {
158165 return p ;
159166 }
160167 }
@@ -168,15 +175,15 @@ export class MergeCommand {
168175 ] ;
169176
170177 for ( const p of possiblePaths ) {
171- if ( fs . existsSync ( p ) ) {
178+ if ( fsUtils . fileExists ( p ) ) {
172179 return p ;
173180 }
174181 }
175182
176183 // fallback to using 'which code'
177184 try {
178185 const resolved = execSync ( 'which code' , { stdio : [ 'pipe' , 'pipe' , 'ignore' ] } ) . toString ( ) . trim ( ) ;
179- if ( resolved && fs . existsSync ( resolved ) ) return resolved ;
186+ if ( resolved && fsUtils . fileExists ( resolved ) ) return resolved ;
180187 } catch ( err ) {
181188 vscode . window . showWarningMessage ( `Could not resolve 'code' binary via 'which': ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
182189 return undefined ;
@@ -230,4 +237,51 @@ export class MergeCommand {
230237 } ) ;
231238 } ) ;
232239 }
240+
241+ private findNewestMergeFiles ( localPath : string ) : { update : string | undefined ; base : string | undefined } {
242+ const dir = path . dirname ( localPath ) ;
243+ const fileName = path . basename ( localPath ) ;
244+
245+ let fileNames : string [ ] ;
246+ try {
247+ fileNames = fsUtils . readDirectory ( dir ) ;
248+ } catch {
249+ return { update : undefined , base : undefined } ;
250+ }
251+ const updatePrefix = `${ fileName } .update@` ;
252+ const basePrefix = `${ fileName } .base@` ;
253+
254+ const update = this . resolveNewestMergeSibling ( fileNames , updatePrefix ) ;
255+ const base = this . resolveNewestMergeSibling ( fileNames , basePrefix ) ;
256+
257+ if ( ! update || ! base ) {
258+ return { update : undefined , base : undefined } ;
259+ }
260+
261+ return {
262+ update : update ? path . join ( dir , update ) : undefined ,
263+ base : base ? path . join ( dir , base ) : undefined ,
264+ } ;
265+ }
266+
267+ private resolveNewestMergeSibling ( fileNames : string [ ] , prefix : string ) : string | undefined {
268+ const matches = fileNames . filter ( fileName => fileName . startsWith ( prefix ) ) ;
269+ return this . selectNewestMergeSibling ( matches ) ;
270+ }
271+
272+ private selectNewestMergeSibling ( fileNames : string [ ] ) : string | undefined {
273+ if ( fileNames . length === 0 ) {
274+ return undefined ;
275+ }
276+
277+ return fileNames
278+ . map ( fileName => {
279+ const version = semver . valid ( extractVersion ( fileName ) ) ;
280+ return version ? { fileName, version } : undefined ;
281+ } )
282+ . filter ( ( candidate ) : candidate is { fileName : string ; version : string } => candidate !== undefined )
283+ . sort ( ( left , right ) => semver . rcompare ( left . version , right . version ) ) [ 0 ] ?. fileName ;
284+ }
285+
233286}
287+
0 commit comments