11
11
12
12
namespace Documentation . Mover ;
13
13
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
+
14
18
public class Move ( IFileSystem readFileSystem , IFileSystem writeFileSystem , DocumentationSet documentationSet , ILoggerFactory loggerFactory )
15
19
{
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 = [ ] ;
19
20
private const string ChangeFormatString = "Change \e [31m{0}\e [0m to \e [32m{1}\e [0m at \e [34m{2}:{3}:{4}\e [0m" ;
20
21
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 = [ ] ;
23
25
24
- public ReadOnlyCollection < LinkModification > LinkModifications => _linkModifications . AsReadOnly ( ) ;
26
+ public IReadOnlyDictionary < ChangeSet , List < LinkModification > > LinkModifications => _linkModifications . AsReadOnly ( ) ;
27
+ public IReadOnlyCollection < ChangeSet > Changes => _changes . Keys ;
25
28
26
29
public async Task < int > Execute ( string source , string target , bool isDryRun , Cancel ctx = default )
27
30
{
28
31
if ( isDryRun )
29
32
_logger . LogInformation ( "Running in dry-run mode" ) ;
30
33
31
- if ( ! ValidateInputs ( source , target , out var from , out var to ) )
34
+ if ( ! ValidateInputs ( source , target , out var fromFiles , out var toFiles ) )
32
35
return 1 ;
33
36
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
+ }
36
46
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 ;
38
51
39
52
var sourceContent = await readFileSystem . File . ReadAllTextAsync ( sourcePath , ctx ) ;
40
53
@@ -61,7 +74,10 @@ public async Task<int> Execute(string source, string target, bool isDryRun, Canc
61
74
var newLink = $ "[{ match . Groups [ 1 ] . Value } ]({ newPath } )";
62
75
var lineNumber = sourceContent . Substring ( 0 , match . Index ) . Count ( c => c == '\n ' ) + 1 ;
63
76
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 (
65
81
match . Value ,
66
82
newLink ,
67
83
sourcePath ,
@@ -71,103 +87,164 @@ public async Task<int> Execute(string source, string target, bool isDryRun, Canc
71
87
return newLink ;
72
88
} ) ;
73
89
74
- _changes . Add ( ( sourcePath , sourceContent , change ) ) ;
90
+ _changes [ changeSet ] = [ new Change ( changeSet . From , sourceContent , change ) ] ;
75
91
76
92
foreach ( var ( _, markdownFile ) in documentationSet . MarkdownFiles )
77
93
{
78
94
await ProcessMarkdownFile (
79
- sourcePath ,
80
- targetPath ,
95
+ changeSet ,
81
96
markdownFile ,
82
97
ctx
83
98
) ;
84
99
}
85
100
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 )
87
106
{
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
+ }
96
118
}
97
119
98
120
if ( isDryRun )
99
121
return 0 ;
100
122
101
-
102
123
try
103
124
{
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
+ }
109
139
}
110
140
catch ( Exception )
111
141
{
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
+ }
116
158
throw ;
117
159
}
160
+
118
161
return 0 ;
119
162
}
120
163
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 )
122
165
{
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 ) ;
125
173
126
- if ( ! from . Extension . Equals ( ".md" , StringComparison . OrdinalIgnoreCase ) )
174
+ //from does not exist at all
175
+ if ( ! fromFile . Exists && ! fromDirectory . Exists )
127
176
{
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") ;
129
180
return false ;
130
181
}
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
+ }
131
190
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 ) ) ;
134
194
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 ] ;
139
207
}
140
-
141
- if ( ! from . Exists )
208
+ //moving folder
209
+ else if ( fromDirectory . Exists )
142
210
{
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
+ }
146
216
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 ( ) ;
151
229
}
152
230
153
231
return true ;
154
232
}
155
233
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 )
161
235
{
236
+ var source = changeSet . From . FullName ;
237
+ var target = changeSet . To . FullName ;
238
+
162
239
var content = await readFileSystem . File . ReadAllTextAsync ( value . FilePath , ctx ) ;
163
240
var currentDir = Path . GetDirectoryName ( value . FilePath ) ! ;
164
241
var pathInfo = GetPathInfo ( currentDir , source , target ) ;
165
242
var linkPattern = BuildLinkPattern ( pathInfo ) ;
166
243
167
244
if ( Regex . IsMatch ( content , linkPattern ) )
168
245
{
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 ) ) ;
171
248
}
172
249
}
173
250
@@ -196,12 +273,12 @@ private static string BuildLinkPattern(
196
273
$@ "\[([^\]]*)\]\((?:{ pathInfo . relativeSource } |{ pathInfo . relativeSourceWithDotSlash } |{ pathInfo . absolutStyleSource } )(?:#[^\)]*?)?\)";
197
274
198
275
private string ReplaceLinks (
276
+ ChangeSet changeSet ,
199
277
string content ,
200
278
string linkPattern ,
201
279
string absoluteStyleTarget ,
202
280
string target ,
203
- MarkdownFile value
204
- ) =>
281
+ MarkdownFile value ) =>
205
282
Regex . Replace (
206
283
content ,
207
284
linkPattern ,
@@ -227,7 +304,9 @@ MarkdownFile value
227
304
228
305
var lineNumber = content . Substring ( 0 , match . Index ) . Count ( c => c == '\n ' ) + 1 ;
229
306
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 (
231
310
match . Value ,
232
311
newLink ,
233
312
value . SourceFile . FullName ,
0 commit comments