Skip to content

Commit b3da602

Browse files
committed
app: manifest: Add the ability to display "untracked" files and folders
This new command-line switch implements something similar to `git status`'s untracked files display. The code examines the workspace and prints out all files and folders that are not inside of a west project. Signed-off-by: Carles Cufi <[email protected]>
1 parent 6202160 commit b3da602

File tree

2 files changed

+143
-1
lines changed

2 files changed

+143
-1
lines changed

src/west/app/project.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

tests/test_project.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,69 @@ def test_list_sha(west_update_tmpdir):
239239
assert cmd('list -f {sha}').startswith("N/A")
240240

241241

242+
def test_manifest_untracked(west_update_tmpdir):
243+
244+
def check(expected):
245+
out_lines = cmd("manifest --untracked").splitlines()
246+
assert out_lines == expected
247+
248+
# Ensure that all projects are active
249+
projs = cmd('list -f {name}').splitlines()
250+
assert projs == ['manifest', 'Kconfiglib', 'tagged_repo', 'net-tools']
251+
252+
# Disable Kconfiglib
253+
cmd('config manifest.group-filter -- -Kconfiglib-group')
254+
255+
# Ensure that Kconfiglib is inactive
256+
projs = cmd('list -f {name}').splitlines()
257+
assert projs == ['manifest', 'tagged_repo', 'net-tools']
258+
projs = cmd('list --all -f {name}').splitlines()
259+
assert projs == ['manifest', 'Kconfiglib', 'tagged_repo', 'net-tools']
260+
261+
topdir = Path(west_update_tmpdir)
262+
print(f'{topdir} type of topdir: {type(topdir)}')
263+
264+
# No untracked files yet
265+
check(list())
266+
267+
(topdir / "folder").mkdir()
268+
# Untracked folder
269+
check(['folder'])
270+
271+
(topdir / "unt").mkdir()
272+
(topdir / "unt" / "file.yml").touch()
273+
# Ensure we show the folder as untracked, not the file
274+
check(['folder', 'unt'])
275+
276+
(topdir / "subdir" / "new").mkdir()
277+
(topdir / "subdir" / "new" / "afile.txt").touch()
278+
check(['folder', 'subdir/new', 'unt'])
279+
280+
# Add a file to an existing project, ignored by --untracked
281+
(topdir / "net-tools" / "test_manifest_untracked.file").touch()
282+
check(['folder', 'subdir/new', 'unt'])
283+
284+
kconfiglib = Path(topdir / "subdir" / "Kconfiglib")
285+
# Same but with an inactive project
286+
(kconfiglib / "test_manifest_untracked.file").touch()
287+
check(['folder', 'subdir/new', 'unt'])
288+
289+
# Empty a project so it's not a Git repo anymore
290+
(topdir / "net-tools" / ".git").rename(topdir / "net-tools" / "former-git")
291+
# Should make no difference
292+
check(['folder', 'subdir/new', 'unt'])
293+
294+
# Same with an inactive project
295+
(kconfiglib / ".git").rename(kconfiglib / "former-git")
296+
# Should make no difference
297+
check(['folder', 'subdir/new', 'unt'])
298+
299+
# Even if we make the whole inactive project disappear it should make no
300+
# difference at all, except that the renamed folder will show up.
301+
(kconfiglib).rename(topdir / "subdir" / "other")
302+
check(['folder', 'subdir/new', 'subdir/other', 'unt'])
303+
304+
242305
def test_manifest_freeze(west_update_tmpdir):
243306
# We should be able to freeze manifests.
244307
actual = cmd('manifest --freeze').splitlines()

0 commit comments

Comments
 (0)