@@ -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+ # Remove projects whose folders are not present on disk. If the project
667+ # is not present on disk then it has no relevance when looking for
668+ # untracked files and folders, since for those to exist the path must
669+ # exist as well.
670+ ppaths = [p for p in ppaths if p .exists ()]
671+ # Since west tolerates nested projects (i.e. a project inside the folder
672+ # of another project) we must sort the project paths to ensure that we
673+ # hit the "enclosing" project first when iterating.
674+ ppaths .sort ()
675+
676+ def _find_untracked (folder ):
677+ '''There are three cases for each element in a folder:
678+ - It's a project -> Do nothing, ignore the folder.
679+ - There are no projects inside -> add to untracked list.
680+ - It's not a project folder but there are some projects inside it -> recurse.
681+ The folder argument cannot be inside a project, otherwise all bets are off.
682+ '''
683+ self .dbg (f'looking for untracked files/folders in: { folder } ' )
684+ for e in [e .absolute () for e in folder .iterdir ()]:
685+ if not e .is_dir () or e .is_symlink ():
686+ untracked .append (e )
687+ continue
688+ self .dbg (f'processing folder: { e } ' )
689+ for ppath in ppaths :
690+ if e .samefile (ppath ):
691+ # We hit a project root folder, skip it.
692+ break
693+ elif e in ppath .parents :
694+ self .dbg (f'recursing into: { e } ' )
695+ _find_untracked (e )
696+ break
697+ else :
698+ # This is not a project and there is no project inside.
699+ # Add to untracked elements.
700+ untracked .append (e )
701+ continue
702+
703+ # Avoid using Path.walk() since that returns all files and folders under
704+ # a particular folder, which is overkill in our case. Instead, recurse
705+ # only when required.
706+ _find_untracked (Path (self .topdir ))
707+
708+ # Remove the .west folder, which is maintained by west
709+ try :
710+ untracked .remove ((Path (self .topdir ) / Path (WEST_DIR )).resolve ())
711+ except ValueError :
712+ self .die (f'Folder { WEST_DIR } not found in workspace' )
713+
714+ # Sort the results for displaying to the user.
715+ untracked .sort ()
716+ for u in untracked :
717+ self .inf (u .relative_to (Path .cwd (), walk_up = True ))
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