Skip to content

Commit f0d5513

Browse files
committed
app: manifest: Add the ability to display "untracked" files and dirs
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 directories that are not inside of a west project. Signed-off-by: Carles Cufi <[email protected]>
1 parent 6202160 commit f0d5513

File tree

2 files changed

+145
-1
lines changed

2 files changed

+145
-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 directories inside the workspace that
588+
are not tracked or managed by west. This effectively means any
589+
file or directory that is outside all of the projects' directories 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 all files and directories 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 directory 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 directories".
664+
ppaths.append(Path(project.abspath))
665+
666+
# Since west tolerates nested projects (i.e. a project inside the directory
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(directory):
672+
'''There are three cases for each element in a directory:
673+
- It's a project -> Do nothing, ignore the directory.
674+
- There are no projects inside -> add to untracked list.
675+
- It's not a project directory but there are some projects inside it -> recurse.
676+
The directory argument cannot be inside a project, otherwise all bets are off.
677+
'''
678+
self.dbg(f'looking for untracked files/directories in: {directory}')
679+
for e in [e.absolute() for e in directory.iterdir()]:
680+
if not e.is_dir() or e.is_symlink():
681+
untracked.append(e)
682+
continue
683+
self.dbg(f'processing directory: {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 directory, 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 directories under
702+
# a particular directory, which is overkill in our case. Instead, recurse
703+
# only when required.
704+
_find_untracked(Path(self.topdir))
705+
706+
# Exclude the .west directory, which is maintained by west
707+
try:
708+
untracked.remove((Path(self.topdir) / Path(WEST_DIR)).resolve())
709+
except ValueError:
710+
self.die(f'Directory {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 directories
963+
in the workspace use "west manifest --untracked".''',
885964
accepts_unknown_args=True,
886965
)
887966

tests/test_project.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,71 @@ 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, cwd=None):
245+
out_lines = cmd("manifest --untracked", cwd=cwd).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+
263+
# No untracked files yet
264+
check(list())
265+
266+
(topdir / "dir").mkdir()
267+
# Untracked dir
268+
check(['dir'])
269+
270+
(topdir / "unt").mkdir()
271+
(topdir / "unt" / "file.yml").touch()
272+
# Ensure we show the dir as untracked, not the file
273+
check(['dir', 'unt'])
274+
275+
(topdir / "subdir" / "new").mkdir()
276+
(topdir / "subdir" / "new" / "afile.txt").touch()
277+
check(['dir', str(Path('subdir/new')), 'unt'])
278+
279+
# Check relative paths
280+
check([str(Path('../dir')), str(Path('../subdir/new')), '.'], cwd=str(Path("unt/")))
281+
282+
# Add a file to an existing project, ignored by --untracked
283+
(topdir / "net-tools" / "test_manifest_untracked.file").touch()
284+
check(['dir', str(Path('subdir/new')), 'unt'])
285+
286+
kconfiglib = Path(topdir / "subdir" / "Kconfiglib")
287+
# Same but with an inactive project
288+
(kconfiglib / "test_manifest_untracked.file").touch()
289+
check(['dir', str(Path('subdir/new')), 'unt'])
290+
291+
# Empty a project so it's not a Git repo anymore
292+
(topdir / "net-tools" / ".git").rename(topdir / "net-tools" / "former-git")
293+
# Should make no difference
294+
check(['dir', str(Path('subdir/new')), 'unt'])
295+
296+
# Same with an inactive project
297+
(kconfiglib / ".git").rename(kconfiglib / "former-git")
298+
# Should make no difference
299+
check(['dir', str(Path('subdir/new')), 'unt'])
300+
301+
# Even if we make the whole inactive project disappear it should make no
302+
# difference at all, except that the renamed dir will show up.
303+
(kconfiglib).rename(topdir / "subdir" / "other")
304+
check(['dir', str(Path('subdir/new')), str(Path('subdir/other')), 'unt'])
305+
306+
242307
def test_manifest_freeze(west_update_tmpdir):
243308
# We should be able to freeze manifests.
244309
actual = cmd('manifest --freeze').splitlines()

0 commit comments

Comments
 (0)