2
2
using System . CommandLine . NamingConventionBinder ;
3
3
using System . Globalization ;
4
4
using System . Text ;
5
+ using System . Text . RegularExpressions ;
5
6
using LibGit2Sharp ;
6
7
using Microsoft . TeamFoundation . SourceControl . WebApi ;
7
8
using Microsoft . VisualStudio . Services . Common ;
@@ -26,6 +27,14 @@ public static Task<int> Main(string[] args)
26
27
new Option < string ? > ( "--out-dir" ) { Description = "The directory path at which to create a new Git repository. Defaults to the last segment in the root path under the current directory." } ,
27
28
new Option < int ? > ( "--min-changeset" ) { Description = "The changeset defining the initial commit. Defaults to the first changeset under the given source path." } ,
28
29
new Option < int ? > ( "--max-changeset" ) { Description = "The last changeset to migrate. Defaults to the most recent changeset under the given source path." } ,
30
+ new Option < ImmutableArray < string > > (
31
+ "--directories" ,
32
+ parseArgument : result => result . Tokens . Select ( token => token . Value ) . ToImmutableArray ( ) )
33
+ {
34
+ Arity = ArgumentArity . OneOrMore ,
35
+ AllowMultipleArgumentsPerToken = true ,
36
+ Description = "If this option is used, only the files within the specified directories (relative to the root path) will be migrated. If a file moves into this filter, the migrated result will appear with no prior history. If a file moves out of this filter, it will appear to be deleted. Wildcards are not currently supported." ,
37
+ } ,
29
38
new Option < ImmutableArray < RootPathChange > > (
30
39
"--root-path-changes" ,
31
40
parseArgument : result => result . Tokens . Select ( token => ParseRootPathChange ( token . Value ) ) . ToImmutableArray ( ) )
@@ -65,13 +74,22 @@ public static async Task<int> MigrateAsync(
65
74
string ? outDir = null ,
66
75
int ? minChangeset = null ,
67
76
int ? maxChangeset = null ,
77
+ ImmutableArray < string > directories = default ,
68
78
ImmutableArray < RootPathChange > rootPathChanges = default ,
69
79
string ? pat = null )
70
80
{
71
81
try
72
82
{
83
+ if ( directories . IsDefault ) directories = ImmutableArray < string > . Empty ;
73
84
if ( rootPathChanges . IsDefault ) rootPathChanges = ImmutableArray < RootPathChange > . Empty ;
74
85
86
+ directories = ImmutableArray . CreateRange (
87
+ directories ,
88
+ directory => '/' + Regex . Replace ( directory . Trim ( '/' , '\\ ' ) , @"[/\\]+" , "/" ) ) ;
89
+
90
+ if ( directories is [ ] || directories . Contains ( "/" ) )
91
+ directories = ImmutableArray . Create ( "" ) ;
92
+
75
93
var outputDirectory = Path . GetFullPath (
76
94
new [ ] { outDir , PathUtils . GetLeaf ( rootPath ) , projectCollectionUrl . Segments . LastOrDefault ( ) }
77
95
. First ( name => ! string . IsNullOrEmpty ( name ) ) ! ) ;
@@ -90,28 +108,38 @@ pat is not null
90
108
91
109
using var client = await connection . GetClientAsync < TfvcHttpClient > ( ) ;
92
110
93
- Console . WriteLine ( "Downloading changeset and label metadata..." ) ;
111
+ Console . WriteLine ( "Downloading changeset metadata..." ) ;
94
112
95
- var ( changesets , allLabels ) = await (
113
+ var changesets = ( await Task . WhenAll ( directories . Select ( directory =>
96
114
client . GetChangesetsAsync (
97
115
maxCommentLength : int . MaxValue ,
98
116
top : int . MaxValue ,
99
117
orderby : "ID asc" ,
100
118
searchCriteria : new TfvcChangesetSearchCriteria
101
119
{
102
120
FollowRenames = true ,
103
- ItemPath = rootPath ,
121
+ ItemPath = rootPath + directory ,
104
122
FromId = minChangeset ?? 0 ,
105
123
ToId = maxChangeset ?? 0 ,
106
- } ) ,
124
+ } ) ) ) . ConfigureAwait ( false ) )
125
+ . SelectMany ( changesets => changesets )
126
+ . DistinctBy ( changeset => changeset . ChangesetId )
127
+ . OrderBy ( changeset => changeset . ChangesetId )
128
+ . ToList ( ) ;
129
+
130
+ Console . WriteLine ( "Downloading labels..." ) ;
131
+
132
+ var allLabels = ( await Task . WhenAll ( directories . Select ( directory =>
107
133
client . GetLabelsAsync (
108
- new TfvcLabelRequestData
109
- {
110
- MaxItemCount = int . MaxValue ,
111
- LabelScope = rootPath ,
112
- } ,
113
- top : int . MaxValue )
114
- ) . ConfigureAwait ( false ) ;
134
+ new TfvcLabelRequestData
135
+ {
136
+ MaxItemCount = int . MaxValue ,
137
+ LabelScope = rootPath + directory ,
138
+ } ,
139
+ top : int . MaxValue ) ) ) . ConfigureAwait ( false ) )
140
+ . SelectMany ( labels => labels )
141
+ . DistinctBy ( label => label . Id )
142
+ . ToList ( ) ;
115
143
116
144
var authorsLookup = await LoadAuthors (
117
145
authors ,
@@ -135,14 +163,15 @@ pat is not null
135
163
var commitsByChangeset = new Dictionary < int , List < ( Commit Commit , BranchIdentity Branch , bool WasCreatedForChangeset ) > > ( ) ;
136
164
137
165
await using var mappingStateAndItemsEnumerator =
138
- EnumerateMappingStatesAsync ( client , rootPathChanges , changesets , initialBranch )
166
+ EnumerateMappingStatesAsync ( client , directories , rootPathChanges , changesets , initialBranch )
139
167
. SelectAwait ( async state => (
140
168
MappingState : state ,
141
169
// Make no attempt to reason about applying TFS item changes over time. Ask for the full set of files.
142
170
Items : await DownloadItemsAsync (
143
171
client ,
144
- PathUtils . GetNonOverlappingPaths (
145
- state . BranchMappingsInDependentOperationOrder . Select ( branchMapping => branchMapping . Mapping . RootDirectory ) ) ,
172
+ from branchMapping in state . BranchMappingsInDependentOperationOrder
173
+ from directory in directories
174
+ select branchMapping . Mapping . RootDirectory + directory ,
146
175
state . Changeset ) ) )
147
176
. WithLookahead ( )
148
177
. GetAsyncEnumerator ( ) ;
@@ -438,6 +467,7 @@ private static void ReportProgress(int changeset, int total, TimedProgress timed
438
467
439
468
private static async IAsyncEnumerable < MappingState > EnumerateMappingStatesAsync (
440
469
TfvcHttpClient client ,
470
+ ImmutableArray < string > directories ,
441
471
ImmutableArray < RootPathChange > rootPathChanges ,
442
472
IReadOnlyList < TfvcChangesetRef > changesets ,
443
473
BranchIdentity initialBranch )
@@ -451,7 +481,7 @@ private static async IAsyncEnumerable<MappingState> EnumerateMappingStatesAsync(
451
481
452
482
await using var changesetChangesEnumerator = changesets
453
483
. Skip ( 1 )
454
- . SelectAwait ( changeset => client . GetChangesetChangesAsync ( changeset . ChangesetId , top : int . MaxValue - 1 ) )
484
+ . SelectAwait ( changeset => GetChangesAsync ( client , changeset . ChangesetId , trunk . Path , directories ) )
455
485
. WithLookahead ( )
456
486
. GetAsyncEnumerator ( ) ;
457
487
@@ -606,6 +636,36 @@ private static ImmutableDictionary<string, Identity> LoadAuthors(Stream authorsF
606
636
return builder . ToImmutable ( ) ;
607
637
}
608
638
639
+ private static async Task < List < TfvcChange > > GetChangesAsync ( TfvcHttpClient client , int changesetId , string rootPath , ImmutableArray < string > directories )
640
+ {
641
+ var changes = await client . GetChangesetChangesAsync ( changesetId , top : int . MaxValue - 1 ) ;
642
+
643
+ changes . RemoveAll ( change =>
644
+ ! ItemPathPassesFilter ( change . Item . Path , rootPath , directories )
645
+ && ! ItemPathPassesFilter ( change . SourceServerItem , rootPath , directories ) ) ;
646
+
647
+ return changes ;
648
+ }
649
+
650
+ private static bool ItemPathPassesFilter ( string itemPath , string rootPath , ImmutableArray < string > directories )
651
+ {
652
+ if ( ! PathUtils . IsAbsolute ( rootPath ) )
653
+ throw new ArgumentException ( "Root path must be absolute." , nameof ( rootPath ) ) ;
654
+
655
+ if ( PathUtils . IsOrContains ( rootPath , itemPath ) )
656
+ {
657
+ var remainingPath = itemPath . AsSpan ( rootPath . Length ) ;
658
+
659
+ foreach ( var directory in directories )
660
+ {
661
+ if ( PathUtils . IsOrContains ( directory , remainingPath ) )
662
+ return true ;
663
+ }
664
+ }
665
+
666
+ return false ;
667
+ }
668
+
609
669
private static async Task < ImmutableArray < TfvcItem > > DownloadItemsAsync ( TfvcHttpClient client , IEnumerable < string > scopePaths , int changeset )
610
670
{
611
671
var union = PathUtils . GetNonOverlappingPaths ( scopePaths ) ;
0 commit comments