@@ -584,6 +584,13 @@ def __init__(self):
584584 If this file uses imports, it will not contain all the
585585 manifest data.
586586
587+ - --untracked print all files and folders inside the workspace that
588+ are not tracked or managed by west. This effectively means any
589+ file or folder that lays outside all of the projects' folders on
590+ disk. This is similar to `git status` for untracked files. The
591+ output format is relative to the current working directory and is
592+ stable and suitable as input for scripting.
593+
587594 If the manifest file does not use imports, and all project
588595 revisions are SHAs, the --freeze and --resolve output will
589596 be identical after a "west update".
@@ -604,6 +611,9 @@ def do_add_parser(self, parser_adder):
604611 exiting with an error if there are issues''' )
605612 group .add_argument ('--path' , action = 'store_true' ,
606613 help = "print the top level manifest file's path" )
614+ group .add_argument ('--untracked' , action = 'store_true' ,
615+ help = '''print any files and folders not managed or
616+ tracked by west''' )
607617
608618 group = parser .add_argument_group ('options for --resolve and --freeze' )
609619 group .add_argument ('-o' , '--out' ,
@@ -624,6 +634,8 @@ def do_run(self, args, user_args):
624634 elif args .freeze :
625635 self ._die_if_manifest_project_filter ('freeze' )
626636 self ._dump (args , manifest .as_frozen_yaml (** dump_kwargs ))
637+ elif args .untracked :
638+ self ._untracked ()
627639 elif args .path :
628640 self .inf (manifest .path )
629641 else :
@@ -640,6 +652,70 @@ def _die_if_manifest_project_filter(self, action):
640652 'the manifest while projects are made inactive by the '
641653 'project filter.' )
642654
655+ def _untracked (self ):
656+ ''' "Performs a top-down search of the west topdir,
657+ ignoring every folder that corresponds to a west project.
658+ '''
659+ ppaths = []
660+ untracked = []
661+ for project in self ._projects (None ):
662+ # We do not check for self.manifest.is_active(project) because
663+ # inactive projects are still considered "tracked folders".
664+ ppaths .append (Path (project .abspath ))
665+
666+ # Since west tolerates nested projects (i.e. a project inside the folder
667+ # of another project) we must sort the project paths to ensure that we
668+ # hit the "enclosing" project first when iterating.
669+ ppaths .sort ()
670+
671+ def _find_untracked (folder ):
672+ '''There are three cases for each element in a folder:
673+ - It's a project -> Do nothing, ignore the folder.
674+ - There are no projects inside -> add to untracked list.
675+ - It's not a project folder but there are some projects inside it -> recurse.
676+ The folder argument cannot be inside a project, otherwise all bets are off.
677+ '''
678+ self .dbg (f'looking for untracked files/folders in: { folder } ' )
679+ for e in [e .absolute () for e in folder .iterdir ()]:
680+ if not e .is_dir () or e .is_symlink ():
681+ untracked .append (e )
682+ continue
683+ self .dbg (f'processing folder: { e } ' )
684+ for ppath in ppaths :
685+ # We cannot use samefile() because it requires the file
686+ # to exist (not always the case with inactive or even
687+ # uncloned projects).
688+ if ppath == e :
689+ # We hit a project root folder, skip it.
690+ break
691+ elif e in ppath .parents :
692+ self .dbg (f'recursing into: { e } ' )
693+ _find_untracked (e )
694+ break
695+ else :
696+ # This is not a project and there is no project inside.
697+ # Add to untracked elements.
698+ untracked .append (e )
699+ continue
700+
701+ # Avoid using Path.walk() since that returns all files and folders under
702+ # a particular folder, which is overkill in our case. Instead, recurse
703+ # only when required.
704+ _find_untracked (Path (self .topdir ))
705+
706+ # Remove the .west folder, which is maintained by west
707+ try :
708+ untracked .remove ((Path (self .topdir ) / Path (WEST_DIR )).resolve ())
709+ except ValueError :
710+ self .die (f'Folder { WEST_DIR } not found in workspace' )
711+
712+ # Sort the results for displaying to the user.
713+ untracked .sort ()
714+ for u in untracked :
715+ # We cannot use Path.relative_to(p, walk_up=True) because the
716+ # walk_up parameter was only added in 3.12
717+ self .inf (os .path .relpath (u , Path .cwd ()))
718+
643719 def _dump (self , args , to_dump ):
644720 if args .out :
645721 with open (args .out , 'w' ) as f :
@@ -881,7 +957,10 @@ def __init__(self):
881957 'status' ,
882958 '"git status" for one or more projects' ,
883959 '''Runs "git status" for each of the specified projects.
884- Unknown arguments are passed to "git status".''' ,
960+ Unknown arguments are passed to "git status".
961+
962+ Note: If you are looking to find untracked files and folders
963+ in the workspace use "west manifest --untracked".''' ,
885964 accepts_unknown_args = True ,
886965 )
887966
0 commit comments