@@ -748,19 +748,11 @@ def list_repos(base_dir, format_style="default", config=None):
748748 if not repos and not available_repos :
749749 return None
750750
751- # Flat mode: render a plain list (no group-subdir tree)
751+ # Flat mode: render a tree grouped by config group
752752 if config and is_flat_mode (config ):
753- # Collect all repo names: cloned + available from config
754753 cloned_names = {r ["name" ] for r in repos }
755- all_available_names = set ()
756- for names in available_repos .values ():
757- all_available_names .update (names )
758- all_names = sorted (cloned_names | all_available_names )
759754
760- if not all_names :
761- return None
762-
763- # Build repo → [group, ...] mapping from config (non-global groups only)
755+ # Build repo → groups mapping from config (non-global groups only)
764756 repo_to_groups = defaultdict (list )
765757 for gname , gconfig in groups .items ():
766758 if gname in global_group_names :
@@ -769,27 +761,49 @@ def list_repos(base_dir, format_style="default", config=None):
769761 rname = extract_repo_name_from_url (url )
770762 repo_to_groups [rname ].append (gname )
771763
764+ # Cloned repos that belong to no config group are rendered ungrouped at the end
765+ ungrouped_cloned = sorted (r for r in cloned_names if not repo_to_groups .get (r ))
766+
767+ all_groups = set (available_repos .keys ()) - global_group_names
768+ if not all_groups and not ungrouped_cloned :
769+ return None
770+
772771 lines = []
773- for i , repo_name in enumerate (all_names ):
774- is_last = i == len (all_names ) - 1
775- prefix = "└──" if is_last else "├──"
776- group_list = repo_to_groups .get (repo_name , [])
777- group_label = (
778- " " + typer .style (f"[{ ', ' .join (group_list )} ]" , fg = typer .colors .CYAN )
779- if group_list
780- else ""
781- )
782- if config :
783- if repo_name in cloned_names and repo_name in all_available_names :
772+ sorted_groups = sorted (all_groups )
773+ total_sections = len (sorted_groups ) + (1 if ungrouped_cloned else 0 )
774+ for i , group in enumerate (sorted_groups ):
775+ is_last_group = i == total_sections - 1
776+ group_prefix = "└──" if is_last_group else "├──"
777+ group_label = typer .style (f"{ group } /" , fg = typer .colors .CYAN , bold = True )
778+ lines .append (f"{ group_prefix } { group_label } " )
779+
780+ available_in_group = set (available_repos .get (group , []))
781+ cloned_in_group = {
782+ r for r in cloned_names if group in repo_to_groups .get (r , [])
783+ }
784+ all_repos_in_group = sorted (available_in_group | cloned_in_group )
785+
786+ for j , repo_name in enumerate (all_repos_in_group ):
787+ is_last_repo = j == len (all_repos_in_group ) - 1
788+ continuation = " " if is_last_group else "│ "
789+ repo_prefix = "└──" if is_last_repo else "├──"
790+ is_cloned = repo_name in cloned_names
791+ is_available = repo_name in available_in_group
792+ if is_cloned and is_available :
784793 status = typer .style ("✓" , fg = typer .colors .GREEN )
785- elif repo_name in cloned_names :
794+ elif is_cloned :
786795 status = typer .style ("?" , fg = typer .colors .MAGENTA )
787796 else :
788797 status = typer .style ("○" , fg = typer .colors .YELLOW )
789- lines .append (f"{ prefix } { status } { repo_name } { group_label } " )
790- else :
791- lines .append (f"{ prefix } { repo_name } { group_label } " )
792- return "\n " .join (lines )
798+ lines .append (f"{ continuation } { repo_prefix } { status } { repo_name } " )
799+
800+ for k , repo_name in enumerate (ungrouped_cloned ):
801+ is_last = k == len (ungrouped_cloned ) - 1
802+ prefix = "└──" if is_last else "├──"
803+ status = typer .style ("?" , fg = typer .colors .MAGENTA )
804+ lines .append (f"{ prefix } { status } { repo_name } " )
805+
806+ return "\n " .join (lines ) if lines else None
793807
794808 if format_style == "tree" :
795809 # Tree format with group as parent
0 commit comments