@@ -20,15 +20,29 @@ public static class EnvironmentFlow
2020 /// Release stages and long-lived branches of the project with the code (the connection's code project, when the
2121 /// code lives elsewhere than the work items) and of <paramref name="project"/> itself.
2222 /// </summary>
23- public static async Task < List < ( string Key , string ? Guess ) > > DetectAsync ( Session session , string project , CancellationToken ct )
23+ public static async Task < List < ( string Key , string ? Guess ) > > DetectAsync ( Session session , string project , CancellationToken ct ) =>
24+ ( await DetectWithSourcesAsync ( session , project , ct ) ) . Select ( d => ( d . Key , d . Guess ) ) . ToList ( ) ;
25+
26+ /// <summary>
27+ /// As <see cref="DetectAsync"/>, with where each one comes from ("$/Project/Trunk/Main, last changeset 2026-10-20";
28+ /// "release Deploy, stage 1, deployed 2026-10-24 from …"), so two of the same name can be told apart.
29+ /// </summary>
30+ public static async Task < List < ( string Key , string ? Guess , string Source ) > > DetectWithSourcesAsync ( Session session , string project , CancellationToken ct )
2431 {
32+ var sources = new Dictionary < string , List < string > > ( StringComparer . OrdinalIgnoreCase ) ;
33+ void From ( string key , string source )
34+ {
35+ if ( ! sources . TryGetValue ( key , out var list ) ) sources [ key ] = list = [ ] ;
36+ if ( ! list . Contains ( source ) ) list . Add ( source ) ;
37+ }
38+ static string Day ( DateTime ? d ) => d ? . ToLocalTime ( ) . ToString ( "yyyy-MM-dd" , System . Globalization . CultureInfo . InvariantCulture ) ?? "?" ;
2539 var projects = new [ ] { session . Config . CodeProject , project } . Where ( p => ! string . IsNullOrWhiteSpace ( p ) ) . Select ( p => p ! )
2640 . Distinct ( StringComparer . OrdinalIgnoreCase ) . ToList ( ) ;
2741 var keys = new List < string > ( ) ;
2842 // Long-lived branches of the repositories in use (a commit in the last half year); feature and work branches are
2943 // filtered by ProposeFlow. With several repositories each branch is named with its repository ("D365FO:staging"),
3044 // since the same branch name can mean different environments - or none - in different repositories.
31- var active = new List < ( string Id , string Name ) > ( ) ;
45+ var active = new List < ( string Id , string Name , string Project , DateTime Last ) > ( ) ;
3246 foreach ( var p in projects )
3347 {
3448 try
@@ -40,19 +54,24 @@ public static class EnvironmentFlow
4054 var last = await session . Client . GetListAsync ( $ "_apis/git/repositories/{ id } /commits?searchCriteria.$top=1", ct ) ;
4155 if ( last . Count > 0 && DateTime . TryParse ( Json . Str ( Json . Obj ( last [ 0 ] , "committer" ) , "date" ) , System . Globalization . CultureInfo . InvariantCulture ,
4256 System . Globalization . DateTimeStyles . AdjustToUniversal , out var when ) && when > DateTime . UtcNow . AddDays ( - 183 ) )
43- active . Add ( ( id , Json . Str ( repo , "name" ) ?? id ) ) ;
57+ active . Add ( ( id , Json . Str ( repo , "name" ) ?? id , p , when ) ) ;
4458 }
4559 }
4660 catch ( AdoApiException ) { } // no Git, or no Code (Read) scope
4761 }
4862 var several = active . Count > 1 ;
49- foreach ( var ( id , name ) in active )
63+ foreach ( var ( id , name , inProject , last ) in active )
5064 {
5165 try
5266 {
5367 var refs = await session . Client . GetListAsync ( $ "_apis/git/repositories/{ id } /refs?filter=heads/&$top=200", ct ) ;
54- keys . AddRange ( refs . Select ( r => Code . Short ( Json . Str ( r , "name" ) ) ?? "" ) . Where ( n => n . Length > 0 && ! n . Contains ( '/' ) )
55- . Select ( n => several ? $ "{ name } :{ n } " : n ) ) ;
68+ foreach ( var n in refs . Select ( r => Code . Short ( Json . Str ( r , "name" ) ) ?? "" ) . Where ( n => n . Length > 0 && ! n . Contains ( '/' ) ) )
69+ {
70+ var key = several ? $ "{ name } :{ n } " : n ;
71+ keys . Add ( key ) ;
72+ From ( key , Ui . T ( $ "Git: branch { n } of repository { name } ({ inProject } ; last commit in the repository { Day ( last ) } )",
73+ $ "Git: gałąź { n } repozytorium { name } ({ inProject } ; ostatni commit w repozytorium { Day ( last ) } )") ) ;
74+ }
5675 }
5776 catch ( AdoApiException ) { }
5877 }
@@ -67,11 +86,13 @@ public static class EnvironmentFlow
6786 {
6887 var path = Json . Str ( b , "path" ) ?? "" ;
6988 var name = path [ ( path . LastIndexOf ( '/' ) + 1 ) ..] ;
70- if ( name . Length == 0 || tfvc . Any ( t => t . Key . Equals ( name , StringComparison . OrdinalIgnoreCase ) ) ) continue ;
89+ if ( name . Length == 0 ) continue ;
7190 var last = await session . Client . GetListAsync ( $ "_apis/tfvc/changesets?searchCriteria.itemPath={ Uri . EscapeDataString ( path ) } &$top=1", ct ) ;
7291 if ( last . Count == 0 || ! DateTime . TryParse ( Json . Str ( last [ 0 ] , "createdDate" ) , System . Globalization . CultureInfo . InvariantCulture ,
7392 System . Globalization . DateTimeStyles . AdjustToUniversal , out var when ) || when < DateTime . UtcNow . AddDays ( - 183 ) ) continue ;
74- tfvc . Add ( ( name , TfvcGuess ( name ) ) ) ;
93+ // Two branches with one name (Main in two projects) are one key: ado_trace knows a TFVC branch by its last part.
94+ From ( name , Ui . T ( $ "TFVC: { path } (last changeset { Day ( when ) } )", $ "TFVC: { path } (ostatni changeset { Day ( when ) } )") ) ;
95+ if ( ! tfvc . Any ( t => t . Key . Equals ( name , StringComparison . OrdinalIgnoreCase ) ) ) tfvc . Add ( ( name , TfvcGuess ( name ) ) ) ;
7596 }
7697 }
7798 catch ( AdoApiException ) { } // no TFVC, or no Code (Read) scope
@@ -85,13 +106,18 @@ public static class EnvironmentFlow
85106 if ( d . When is not { } when || when < DateTime . UtcNow . AddDays ( - 183 ) ) continue ;
86107 var branch = Code . Short ( d . Branch ) ;
87108 var repo = active . FirstOrDefault ( a => string . Equals ( a . Id , d . Repository , StringComparison . OrdinalIgnoreCase ) ) . Name ;
88- keys . Add ( d . Branch ? . StartsWith ( "$/" , StringComparison . Ordinal ) == true || branch is null ? $ "{ d . Definition } /{ d . Stage } "
89- : several && repo is not null ? $ "{ repo } :{ branch } " : branch ) ;
109+ var key = d . Branch ? . StartsWith ( "$/" , StringComparison . Ordinal ) == true || branch is null ? $ "{ d . Definition } /{ d . Stage } "
110+ : several && repo is not null ? $ "{ repo } :{ branch } " : branch ;
111+ keys . Add ( key ) ;
112+ From ( key , Ui . T ( $ "release { d . Definition } , stage { d . Stage } : deployed { Day ( d . When ) } ", $ "release { d . Definition } , etap { d . Stage } : wdrożony { Day ( d . When ) } ")
113+ + ( d . Branch is { } from ? Ui . T ( $ " from { from } ", $ " z { from } ") : "" ) + $ " ({ p } )") ;
90114 }
91115 var git = TraceQueries . ProposeFlow ( keys . Distinct ( StringComparer . OrdinalIgnoreCase ) ) ;
92116 var order = new [ ] { "DEV" , "UAT" , "PROD" } ;
93117 return tfvc . OrderBy ( t => t . Guess is null ? 3 : Array . IndexOf ( order , t . Guess ) ) . ThenBy ( t => t . Key , StringComparer . OrdinalIgnoreCase )
94- . Concat ( git . Where ( g => ! tfvc . Any ( t => t . Key . Equals ( g . Key , StringComparison . OrdinalIgnoreCase ) ) ) ) . ToList ( ) ;
118+ . Concat ( git . Where ( g => ! tfvc . Any ( t => t . Key . Equals ( g . Key , StringComparison . OrdinalIgnoreCase ) ) ) )
119+ . Select ( k => ( k . Key , k . Guess , string . Join ( "; " , sources . GetValueOrDefault ( k . Key ) ?? [ ] ) ) )
120+ . DistinctBy ( k => k . Key , StringComparer . OrdinalIgnoreCase ) . ToList ( ) ;
95121 }
96122
97123 /// <summary>A TFVC branch and the branches under it.</summary>
0 commit comments