1111
1212namespace Documentation . Mover ;
1313
14+ public record ChangeSet ( IFileInfo From , IFileInfo To ) ;
15+ public record Change ( IFileInfo Source , string OriginalContent , string NewContent ) ;
16+ public record LinkModification ( string OldLink , string NewLink , string SourceFile , int LineNumber , int ColumnNumber ) ;
17+
1418public class Move ( IFileSystem readFileSystem , IFileSystem writeFileSystem , DocumentationSet documentationSet , ILoggerFactory loggerFactory )
1519{
16- private readonly ILogger _logger = loggerFactory . CreateLogger < Move > ( ) ;
17- private readonly List < ( string filePath , string originalContent , string newContent ) > _changes = [ ] ;
18- private readonly List < LinkModification > _linkModifications = [ ] ;
1920 private const string ChangeFormatString = "Change \e [31m{0}\e [0m to \e [32m{1}\e [0m at \e [34m{2}:{3}:{4}\e [0m" ;
2021
21- public record LinkModification ( string OldLink , string NewLink , string SourceFile , int LineNumber , int ColumnNumber ) ;
22-
22+ private readonly ILogger _logger = loggerFactory . CreateLogger < Move > ( ) ;
23+ private readonly Dictionary < ChangeSet , List < Change > > _changes = [ ] ;
24+ private readonly Dictionary < ChangeSet , List < LinkModification > > _linkModifications = [ ] ;
2325
24- public ReadOnlyCollection < LinkModification > LinkModifications => _linkModifications . AsReadOnly ( ) ;
26+ public IReadOnlyDictionary < ChangeSet , List < LinkModification > > LinkModifications => _linkModifications . AsReadOnly ( ) ;
27+ public IReadOnlyCollection < ChangeSet > Changes => _changes . Keys ;
2528
2629 public async Task < int > Execute ( string source , string target , bool isDryRun , Cancel ctx = default )
2730 {
2831 if ( isDryRun )
2932 _logger . LogInformation ( "Running in dry-run mode" ) ;
3033
31- if ( ! ValidateInputs ( source , target , out var from , out var to ) )
34+ if ( ! ValidateInputs ( source , target , out var fromFiles , out var toFiles ) )
3235 return 1 ;
3336
34- var sourcePath = from . FullName ;
35- var targetPath = to . FullName ;
37+ foreach ( var ( fromFile , toFile ) in fromFiles . Zip ( toFiles ) )
38+ {
39+ var changeSet = new ChangeSet ( fromFile , toFile ) ;
40+ _logger . LogInformation ( $ "Requested to move from '{ fromFile } ' to '{ toFile } ") ;
41+ await SetupChanges ( changeSet , ctx ) ;
42+ }
43+
44+ return await MoveAndRewriteLinks ( isDryRun , ctx ) ;
45+ }
3646
37- _logger . LogInformation ( $ "Requested to move from '{ from } ' to '{ to } ") ;
47+ private async Task SetupChanges ( ChangeSet changeSet , Cancel ctx )
48+ {
49+ var sourcePath = changeSet . From . FullName ;
50+ var targetPath = changeSet . To . FullName ;
3851
3952 var sourceContent = await readFileSystem . File . ReadAllTextAsync ( sourcePath , ctx ) ;
4053
@@ -61,7 +74,10 @@ public async Task<int> Execute(string source, string target, bool isDryRun, Canc
6174 var newLink = $ "[{ match . Groups [ 1 ] . Value } ]({ newPath } )";
6275 var lineNumber = sourceContent . Substring ( 0 , match . Index ) . Count ( c => c == '\n ' ) + 1 ;
6376 var columnNumber = match . Index - sourceContent . LastIndexOf ( '\n ' , match . Index ) ;
64- _linkModifications . Add ( new LinkModification (
77+ if ( ! _linkModifications . ContainsKey ( changeSet ) )
78+ _linkModifications [ changeSet ] = [ ] ;
79+
80+ _linkModifications [ changeSet ] . Add ( new LinkModification (
6581 match . Value ,
6682 newLink ,
6783 sourcePath ,
@@ -71,103 +87,164 @@ public async Task<int> Execute(string source, string target, bool isDryRun, Canc
7187 return newLink ;
7288 } ) ;
7389
74- _changes . Add ( ( sourcePath , sourceContent , change ) ) ;
90+ _changes [ changeSet ] = [ new Change ( changeSet . From , sourceContent , change ) ] ;
7591
7692 foreach ( var ( _, markdownFile ) in documentationSet . MarkdownFiles )
7793 {
7894 await ProcessMarkdownFile (
79- sourcePath ,
80- targetPath ,
95+ changeSet ,
8196 markdownFile ,
8297 ctx
8398 ) ;
8499 }
85100
86- foreach ( var ( oldLink , newLink , sourceFile , lineNumber , columnNumber ) in LinkModifications )
101+ }
102+
103+ private async Task < int > MoveAndRewriteLinks ( bool isDryRun , Cancel ctx )
104+ {
105+ foreach ( var ( changeSet , linkModifications ) in _linkModifications )
87106 {
88- _logger . LogInformation ( string . Format (
89- ChangeFormatString ,
90- oldLink ,
91- newLink ,
92- sourceFile == sourcePath && ! isDryRun ? targetPath : sourceFile ,
93- lineNumber ,
94- columnNumber
95- ) ) ;
107+ foreach ( var ( oldLink , newLink , sourceFile , lineNumber , columnNumber ) in linkModifications )
108+ {
109+ _logger . LogInformation ( string . Format (
110+ ChangeFormatString ,
111+ oldLink ,
112+ newLink ,
113+ sourceFile == changeSet . From . FullName && ! isDryRun ? changeSet . To . FullName : sourceFile ,
114+ lineNumber ,
115+ columnNumber
116+ ) ) ;
117+ }
96118 }
97119
98120 if ( isDryRun )
99121 return 0 ;
100122
101-
102123 try
103124 {
104- foreach ( var ( filePath , _, newContent ) in _changes )
105- await writeFileSystem . File . WriteAllTextAsync ( filePath , newContent , ctx ) ;
106- var targetDirectory = Path . GetDirectoryName ( targetPath ) ;
107- readFileSystem . Directory . CreateDirectory ( targetDirectory ! ) ;
108- readFileSystem . File . Move ( sourcePath , targetPath ) ;
125+ foreach ( var ( changeSet , changes ) in _changes )
126+ {
127+ foreach ( var ( filePath , _, newContent ) in changes )
128+ {
129+ if ( ! filePath . Directory ! . Exists )
130+ writeFileSystem . Directory . CreateDirectory ( filePath . Directory . FullName ) ;
131+ await writeFileSystem . File . WriteAllTextAsync ( filePath . FullName , newContent , ctx ) ;
132+
133+ }
134+
135+ var targetDirectory = Path . GetDirectoryName ( changeSet . To . FullName ) ;
136+ readFileSystem . Directory . CreateDirectory ( targetDirectory ! ) ;
137+ readFileSystem . File . Move ( changeSet . From . FullName , changeSet . To . FullName ) ;
138+ }
109139 }
110140 catch ( Exception )
111141 {
112- foreach ( var ( filePath , originalContent , _) in _changes )
113- await writeFileSystem . File . WriteAllTextAsync ( filePath , originalContent , ctx ) ;
114- writeFileSystem . File . Move ( targetPath , sourcePath ) ;
115- _logger . LogError ( "An error occurred while moving files. Reverting changes" ) ;
142+ if ( _changes . Count > 1 )
143+ {
144+ _logger . LogError ( "An error occurred while moving files. Can only revert a single file move at this time" ) ;
145+ throw ;
146+ }
147+
148+ foreach ( var ( changeSet , changes ) in _changes )
149+ {
150+ foreach ( var ( filePath , originalContent , _) in changes )
151+ await writeFileSystem . File . WriteAllTextAsync ( filePath . FullName , originalContent , ctx ) ;
152+ if ( ! changeSet . To . Exists )
153+ writeFileSystem . File . Move ( changeSet . To . FullName , changeSet . From . FullName ) ;
154+ else
155+ writeFileSystem . File . Copy ( changeSet . To . FullName , changeSet . From . FullName , overwrite : true ) ;
156+ _logger . LogError ( "An error occurred while moving files. Reverting changes" ) ;
157+ }
116158 throw ;
117159 }
160+
118161 return 0 ;
119162 }
120163
121- private bool ValidateInputs ( string source , string target , out IFileInfo from , out IFileInfo to )
164+ private bool ValidateInputs ( string source , string target , out IFileInfo [ ] fromFiles , out IFileInfo [ ] toFiles )
122165 {
123- from = readFileSystem . FileInfo . New ( source ) ;
124- to = readFileSystem . FileInfo . New ( target ) ;
166+ fromFiles = [ ] ;
167+ toFiles = [ ] ;
168+
169+ var fromFile = readFileSystem . FileInfo . New ( source ) ;
170+ var fromDirectory = readFileSystem . DirectoryInfo . New ( source ) ;
171+ var toFile = readFileSystem . FileInfo . New ( target ) ;
172+ var toDirectory = readFileSystem . DirectoryInfo . New ( target ) ;
125173
126- if ( ! from . Extension . Equals ( ".md" , StringComparison . OrdinalIgnoreCase ) )
174+ //from does not exist at all
175+ if ( ! fromFile . Exists && ! fromDirectory . Exists )
127176 {
128- _logger . LogError ( "Source path must be a markdown file. Directory paths are not supported yet" ) ;
177+ _logger . LogError ( ! string . IsNullOrEmpty ( fromFile . Extension )
178+ ? $ "Source file '{ fromFile } ' does not exist"
179+ : $ "Source directory '{ fromDirectory } ' does not exist") ;
129180 return false ;
130181 }
182+ //moving file
183+ if ( fromFile . Exists )
184+ {
185+ if ( ! fromFile . Extension . Equals ( ".md" , StringComparison . OrdinalIgnoreCase ) )
186+ {
187+ _logger . LogError ( "Source path must be a markdown file. Directory paths are not supported yet" ) ;
188+ return false ;
189+ }
131190
132- if ( to . Extension == string . Empty )
133- to = readFileSystem . FileInfo . New ( Path . Combine ( to . FullName , from . Name ) ) ;
191+ //if toFile has no extension assume move to folder
192+ if ( toFile . Extension == string . Empty )
193+ toFile = readFileSystem . FileInfo . New ( Path . Combine ( toDirectory . FullName , fromFile . Name ) ) ;
134194
135- if ( ! to . Extension . Equals ( ".md" , StringComparison . OrdinalIgnoreCase ) )
136- {
137- _logger . LogError ( $ "Target path '{ to . FullName } ' must be a markdown file.") ;
138- return false ;
195+ if ( ! toFile . Extension . Equals ( ".md" , StringComparison . OrdinalIgnoreCase ) )
196+ {
197+ _logger . LogError ( $ "Target path '{ toFile . FullName } ' must be a markdown file.") ;
198+ return false ;
199+ }
200+ if ( toFile . Exists )
201+ {
202+ _logger . LogError ( $ "Target file { target } already exists") ;
203+ return false ;
204+ }
205+ fromFiles = [ fromFile ] ;
206+ toFiles = [ toFile ] ;
139207 }
140-
141- if ( ! from . Exists )
208+ //moving folder
209+ else if ( fromDirectory . Exists )
142210 {
143- _logger . LogError ( $ "Source file { source } does not exist") ;
144- return false ;
145- }
211+ if ( toDirectory . Exists )
212+ {
213+ _logger . LogError ( $ "Target directory '{ toDirectory . FullName } ' already exists.") ;
214+ return false ;
215+ }
146216
147- if ( to . Exists )
148- {
149- _logger . LogError ( $ "Target file { target } already exists") ;
150- return false ;
217+ if ( toDirectory . FullName . StartsWith ( fromDirectory . FullName ) )
218+ {
219+ _logger . LogError ( $ "Can not move source directory '{ toDirectory . FullName } ' to a { toFile . FullName } ") ;
220+ return false ;
221+ }
222+
223+ fromFiles = fromDirectory . GetFiles ( "*.md" , SearchOption . AllDirectories ) ;
224+ toFiles = fromFiles . Select ( f =>
225+ {
226+ var relative = Path . GetRelativePath ( fromDirectory . FullName , f . FullName ) ;
227+ return readFileSystem . FileInfo . New ( Path . Combine ( toDirectory . FullName , relative ) ) ;
228+ } ) . ToArray ( ) ;
151229 }
152230
153231 return true ;
154232 }
155233
156- private async Task ProcessMarkdownFile (
157- string source ,
158- string target ,
159- MarkdownFile value ,
160- Cancel ctx )
234+ private async Task ProcessMarkdownFile ( ChangeSet changeSet , MarkdownFile value , Cancel ctx )
161235 {
236+ var source = changeSet . From . FullName ;
237+ var target = changeSet . To . FullName ;
238+
162239 var content = await readFileSystem . File . ReadAllTextAsync ( value . FilePath , ctx ) ;
163240 var currentDir = Path . GetDirectoryName ( value . FilePath ) ! ;
164241 var pathInfo = GetPathInfo ( currentDir , source , target ) ;
165242 var linkPattern = BuildLinkPattern ( pathInfo ) ;
166243
167244 if ( Regex . IsMatch ( content , linkPattern ) )
168245 {
169- var newContent = ReplaceLinks ( content , linkPattern , pathInfo . absoluteStyleTarget , target , value ) ;
170- _changes . Add ( ( value . FilePath , content , newContent ) ) ;
246+ var newContent = ReplaceLinks ( changeSet , content , linkPattern , pathInfo . absoluteStyleTarget , target , value ) ;
247+ _changes [ changeSet ] . Add ( new Change ( value . SourceFile , content , newContent ) ) ;
171248 }
172249 }
173250
@@ -196,12 +273,12 @@ private static string BuildLinkPattern(
196273 $@ "\[([^\]]*)\]\((?:{ pathInfo . relativeSource } |{ pathInfo . relativeSourceWithDotSlash } |{ pathInfo . absolutStyleSource } )(?:#[^\)]*?)?\)";
197274
198275 private string ReplaceLinks (
276+ ChangeSet changeSet ,
199277 string content ,
200278 string linkPattern ,
201279 string absoluteStyleTarget ,
202280 string target ,
203- MarkdownFile value
204- ) =>
281+ MarkdownFile value ) =>
205282 Regex . Replace (
206283 content ,
207284 linkPattern ,
@@ -227,7 +304,9 @@ MarkdownFile value
227304
228305 var lineNumber = content . Substring ( 0 , match . Index ) . Count ( c => c == '\n ' ) + 1 ;
229306 var columnNumber = match . Index - content . LastIndexOf ( '\n ' , match . Index ) ;
230- _linkModifications . Add ( new LinkModification (
307+ if ( ! _linkModifications . ContainsKey ( changeSet ) )
308+ _linkModifications [ changeSet ] = [ ] ;
309+ _linkModifications [ changeSet ] . Add ( new LinkModification (
231310 match . Value ,
232311 newLink ,
233312 value . SourceFile . FullName ,
0 commit comments