@@ -43,6 +43,8 @@ class Program
4343 static string domain = buildDomain ;
4444 static bool isYamlPipeline = false ;
4545 static List < string > yamlFileNames = new List < string > ( ) ;
46+ // For holding duplicate template file names found in yamlFileNames. Allows avoiding file overwrites.
47+ static HashSet < string > yamlFileNameDuplicates = new HashSet < string > ( ) ;
4648
4749 public class BuildDef
4850 {
@@ -164,7 +166,7 @@ public static async Task PopulateProjectList()
164166 // Documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-server-rest-5.0
165167 // https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/list?view=azure-devops-rest-5.1
166168
167- string restUrl = String . Format ( "https://{0}/{1}/_apis/projects" , domain , organization ) ;
169+ string restUrl = string . Format ( "https://{0}/{1}/_apis/projects" , domain , organization ) ;
168170 Console . WriteLine ( "Getting build definitions from " + restUrl + "\n " ) ;
169171
170172 using ( HttpClient client = new HttpClient ( ) )
@@ -207,7 +209,7 @@ public static async Task PopulateBuildDefinitionList(string project = "SDK_v4")
207209
208210 try
209211 {
210- string restUrl = String . Format ( "https://{0}/{1}/{2}/_apis/build/definitions?includeAllProperties=true&api-version=5.0" , buildDomain , organization , project ) ;
212+ string restUrl = string . Format ( "https://{0}/{1}/{2}/_apis/build/definitions?includeAllProperties=true&api-version=5.0" , buildDomain , organization , project ) ;
211213
212214 using ( HttpClient client = new HttpClient ( ) )
213215 {
@@ -249,7 +251,7 @@ public static async Task GetReleaseDefinitions(string project = "SDK_v4")
249251
250252 try
251253 {
252- string restUrl = String . Format ( "https://{0}/{1}/{2}/_apis/release/definitions?api-version=5.0" , releaseDomain , organization , project ) ;
254+ string restUrl = string . Format ( "https://{0}/{1}/{2}/_apis/release/definitions?api-version=5.0" , releaseDomain , organization , project ) ;
253255
254256 using ( HttpClient client = new HttpClient ( ) )
255257 {
@@ -295,7 +297,7 @@ public static async Task WriteDefinitionToFile(string project, string definition
295297
296298 try
297299 {
298- string restUrl = String . Format ( "https://{0}/{1}/{2}/_apis/{3}/definitions/{4}?api-version=5.0" ,
300+ string restUrl = string . Format ( "https://{0}/{1}/{2}/_apis/{3}/definitions/{4}?api-version=5.0" ,
299301 domain , organization , project , definitionType , buildDef . id ) ;
300302
301303 using ( HttpClient client = new HttpClient ( ) )
@@ -357,6 +359,7 @@ public static async Task WriteDefinitionToFile(string project, string definition
357359 public static bool CheckForYamlPipeline ( JObject json )
358360 {
359361 yamlFileNames . Clear ( ) ;
362+ yamlFileNameDuplicates . Clear ( ) ;
360363
361364 if ( json != null && json [ "process" ] != null && json [ "process" ] [ "yamlFilename" ] != null )
362365 {
@@ -367,10 +370,14 @@ public static bool CheckForYamlPipeline(JObject json)
367370 return false ;
368371 }
369372
373+ /// <summary>
374+ /// Download the root .yml file, that is, the file referenced in the definition json.
375+ /// Then recursively find all template references and download those .yml template files.
376+ /// </summary>
377+ /// <param name="json"></param>
378+ /// <param name="directory"></param>
370379 public static void DownloadYamlFilesToDirectory ( JObject json , string directory )
371380 {
372- // Download the one .yml file referenced in the definition json.
373- // Then walk the calling chain, recursively downloading any other .yml files.
374381 if ( json != null && json [ "repository" ] != null && json [ "repository" ] [ "properties" ] != null )
375382 {
376383 // Get the git repo URL for this Azure project.
@@ -396,7 +403,7 @@ public static void DownloadYamlFilesToDirectory(JObject json, string directory)
396403
397404 string githubFileUrl = string . Empty ;
398405 string logIndent = string . Empty ;
399- string currentOffsetFromBaseUrl = String . Empty ;
406+ string currentOffsetFromBaseUrl = string . Empty ;
400407
401408 if ( yamlFileNames . Count > 0 )
402409 {
@@ -412,6 +419,7 @@ public static void DownloadYamlFilesToDirectory(JObject json, string directory)
412419 // Look for template references to other .yml files. Add them to yamlFileNames list.
413420 // Loop.
414421 string filePath = yamlFileNames . FirstOrDefault ( ) ;
422+
415423 currentOffsetFromBaseUrl = filePath . Contains ( '/' ) ? filePath . Substring ( 0 , filePath . LastIndexOf ( '/' ) ) : "" ;
416424
417425 if ( string . IsNullOrWhiteSpace ( githubFileUrl ) )
@@ -421,9 +429,24 @@ public static void DownloadYamlFilesToDirectory(JObject json, string directory)
421429 logIndent = " " ; // 8 spaces
422430 }
423431
424- string fileName = githubFileUrl . Substring ( githubFileUrl . LastIndexOf ( '/' ) + 1 ) ;
425- string outputFilePath = $ "{ directory } \\ { fileName } ";
426- bool succeeded = DownloadFileFromGithub ( githubFileUrl , outputFilePath , logIndent ) ;
432+ // Check whether this file has the same name as another. If so, it wil need its own directory to avoid a file overwrite.
433+ string outputFilePath ;
434+ string fileName ;
435+ string duplicateFileName = yamlFileNameDuplicates . FirstOrDefault ( x => filePath . EndsWith ( x , StringComparison . OrdinalIgnoreCase ) ) ;
436+ if ( ! string . IsNullOrWhiteSpace ( duplicateFileName ) )
437+ {
438+ // It does. Create a folder for it.
439+ fileName = duplicateFileName . Replace ( '/' , '\\ ' ) ;
440+ string newDir = $ "{ directory } \\ { fileName . Substring ( 0 , fileName . LastIndexOf ( '\\ ' ) ) } ";
441+ System . IO . Directory . CreateDirectory ( newDir ) ;
442+ }
443+ else
444+ {
445+ fileName = githubFileUrl . Substring ( githubFileUrl . LastIndexOf ( '/' ) + 1 ) ;
446+ }
447+ outputFilePath = $ "{ directory } \\ { fileName } ";
448+
449+ bool succeeded = DownloadFileFromGithub ( githubFileUrl , outputFilePath , fileName , logIndent ) ;
427450 yamlFileNames . RemoveAt ( 0 ) ;
428451
429452 if ( succeeded )
@@ -478,7 +501,7 @@ private static string GetGithubRepoUrlMinusPath(JObject json)
478501 return repoUrl ;
479502 }
480503
481- private static bool DownloadFileFromGithub ( string githubFileUrl , string outputFilePath , string logIndent )
504+ private static bool DownloadFileFromGithub ( string githubFileUrl , string outputFilePath , string fileName , string logIndent )
482505 {
483506 // https://gist.github.com/EvanSnapp/ddf7f7f793474ea9631cbc0960295983
484507 // https://github.com/zayenCh/DownloadFile/blob/master/Downloader.cs
@@ -495,17 +518,16 @@ private static bool DownloadFileFromGithub(string githubFileUrl, string outputFi
495518 {
496519 string contents = webClient . DownloadString ( new Uri ( githubFileUrl ) ) ;
497520 File . WriteAllText ( outputFilePath , contents ) ;
498- string fileName = outputFilePath . Substring ( outputFilePath . LastIndexOf ( "\\ " ) + 1 ) ;
499521 Console . WriteLine ( $ "{ logIndent } * { fileName } ") ;
500522 BuildCsvString ( column , fileName ) ;
501523
502524 return true ;
503525 }
504526 catch ( Exception ex )
505527 {
506- string fileName = outputFilePath . Substring ( outputFilePath . LastIndexOf ( "\\ " ) + 1 ) ;
507528 Console . WriteLine ( $ "{ logIndent } * { fileName } **") ;
508529 BuildCsvString ( column , fileName + "**" ) ;
530+ // We couldn't get the file contents, so write the error message to the file instead.
509531 string contents = $ "* { ex . Message . ToString ( ) } \r \n { githubFileUrl } ";
510532 File . WriteAllText ( outputFilePath , contents ) ;
511533 if ( ex . Message . ToString ( ) . Contains ( "(404)" ) )
@@ -535,12 +557,13 @@ private static void AddYamlTemplateReferencesFromFile(
535557 int index = name . IndexOf ( "#" ) ; // Find and remove any other comment.
536558 if ( index >= 0 ) name = name . Remove ( index ) . TrimEnd ( ) ;
537559 name = name . TrimStart ( '-' ) . Replace ( match , "" ) . Trim ( ) ; // TrimStart() handles the case of "- template:".
538- if ( ! String . IsNullOrWhiteSpace ( name ) )
560+ if ( ! string . IsNullOrWhiteSpace ( name ) )
539561 {
540562 // Normalize the relative path.
541563 name = Path . GetFullPath ( ( Path . Combine ( "/" , currentOffsetFromRootUrl , name ) ) ) . Replace ( @"C:\" , "" ) . TrimStart ( '\\ ' ) . Replace ( @"\" , "/" ) ;
542564 if ( ! yamlFileNames . Contains ( name ) )
543565 {
566+ AddToYamlFileNameDuplicates ( name ) ;
544567 yamlFileNames . Add ( name ) ;
545568 }
546569 }
@@ -550,6 +573,53 @@ private static void AddYamlTemplateReferencesFromFile(
550573 return ;
551574 }
552575
576+ /// <summary>
577+ /// This checks filePath to see whether the file name duplicates a file name already in
578+ /// yamlFileNames. If so, it adds the two files to yamlFileNameDuplicates.
579+ /// For files in yamlFileNameDuplicates we make an exception to folder flattening and
580+ /// create separate download folders to prevent one file overwriting the other.
581+ /// </summary>
582+ /// <param name="filePath"></param>
583+ private static void AddToYamlFileNameDuplicates ( string filePath )
584+ {
585+ if ( yamlFileNames . Contains ( filePath ) )
586+ {
587+ // This is a duplicate reference to the same file. We don't dedupe these. Skip it.
588+ return ;
589+ }
590+
591+ string [ ] filePathArray = filePath . Split ( '/' ) ;
592+ filePathArray = filePathArray . Reverse ( ) . ToArray ( ) ;
593+
594+ foreach ( string yamlFileName in yamlFileNames )
595+ {
596+ string [ ] yamlFileNameArray = yamlFileName . Split ( '/' ) ;
597+ yamlFileNameArray = yamlFileNameArray . Reverse ( ) . ToArray ( ) ;
598+
599+ for ( int i = 0 ; i < filePathArray . Length && i < yamlFileNameArray . Length ; i ++ )
600+ {
601+ if ( ! filePathArray [ i ] . Equals ( yamlFileNameArray [ i ] , StringComparison . OrdinalIgnoreCase ) )
602+ {
603+ if ( i == 0 ) break ; // Names are not dupes.
604+
605+ string outFilePath = "" ;
606+ string outyamlPath = "" ;
607+ int j = i ;
608+ while ( j >= 0 )
609+ {
610+ outFilePath += filePathArray [ j ] + "/" ;
611+ outyamlPath += yamlFileNameArray [ j ] + "/" ;
612+ j -- ;
613+ }
614+
615+ yamlFileNameDuplicates . Add ( outFilePath . Trim ( '/' ) ) ;
616+ yamlFileNameDuplicates . Add ( outyamlPath . Trim ( '/' ) ) ;
617+ return ; // No more dupes that are not accounted for, so quit.
618+ }
619+ }
620+ }
621+ }
622+
553623 private static int CompareProjects ( Proj x , Proj y )
554624 {
555625 if ( x == null )
0 commit comments