tools: make dependency retries safe
diff --git a/test/hil/test/test_change_impact.py b/test/hil/test/test_change_impact.py
index 4a3631f..9cda65d 100644
--- a/test/hil/test/test_change_impact.py
+++ b/test/hil/test/test_change_impact.py
@@ -3563,24 +3563,40 @@
         r = subprocess.run([sys.executable, '-c', code], cwd=REPO, capture_output=True, text=True, timeout=60)
         self.assertEqual(r.returncode, 0, r.stderr)
         self.assertEqual(r.stdout.strip(), '[]')
+
+
 class TestGetDepsExistingDirectory(unittest.TestCase):
-    def run_get_dep(self, fail=None, create_git=True):
+    def run_get_dep(self, fail=None, create_git=True, initialized=False, valid=True, unborn=False,
+                    origin=True, pinned=False):
         from tempfile import TemporaryDirectory
         from unittest import mock
         import get_deps
 
-        with TemporaryDirectory() as top:
+        with TemporaryDirectory(prefix='tinyusb deps ') as top:
             dep = pathlib.Path(top, 'lib', 'dep')
             dep.mkdir(parents=True)
+            if initialized:
+                (dep / '.git').mkdir()
 
             commands = []
 
             def run(command):
                 commands.append(command)
-                if command.endswith(' init') and fail != 'init' and create_git:
+                if command[-1] == 'init' and fail != 'init' and create_git:
                     (dep / '.git').mkdir()
-                return subprocess.CompletedProcess([], 1 if fail and command.endswith(fail) else 0,
-                                                   stdout=b'parent-head\n')
+                returncode, output = 0, b'parent-head\n'
+                if command[-1] == '--is-inside-work-tree':
+                    returncode, output = (0, b'true\n') if valid else (1, b'not a repository\n')
+                elif command[-3:] == ['remote', 'get-url', 'origin']:
+                    returncode, output = ((0, b'https://example.invalid/dep.git\n') if origin else (1, b''))
+                elif command[-1] == 'HEAD':
+                    if unborn:
+                        returncode, output = 1, b''
+                    elif pinned:
+                        output = b'0123456789abcdef\n'
+                if fail and command[-1].endswith(fail):
+                    returncode = 1
+                return subprocess.CompletedProcess([], returncode, stdout=output)
 
             with mock.patch.object(get_deps, 'TOP', pathlib.Path(top)), \
                     mock.patch.dict(get_deps.deps_all,
@@ -3594,14 +3610,15 @@
 
     def test_directory_without_git_metadata_is_initialized(self):
         dep, commands, status = self.run_get_dep()
-        git = f'git -C {dep}'
+        git = ['git', '-C', str(dep)]
         self.assertEqual(status, 0)
         self.assertEqual(commands, [
-            f'{git} init',
-            f'{git} remote add origin https://example.invalid/dep.git',
-            f'{git} fetch --depth 1 origin 0123456789abcdef',
-            f'{git} checkout FETCH_HEAD',
+            [*git, 'init'],
+            [*git, 'remote', 'add', 'origin', 'https://example.invalid/dep.git'],
+            [*git, 'fetch', '--depth', '1', 'origin', '0123456789abcdef'],
+            [*git, 'checkout', 'FETCH_HEAD'],
         ])
+        self.assertIn(' ', str(dep))
 
     def test_git_failures_stop_dependency_setup(self):
         for name, suffix, count in [('init', 'init', 1), ('remote', 'dep.git', 2),
@@ -3616,6 +3633,32 @@
         self.assertEqual(status, 1)
         self.assertEqual(len(commands), 1)
 
+    def test_unborn_repository_retries_fetch(self):
+        dep, commands, status = self.run_get_dep(initialized=True, unborn=True, origin=False)
+        git = ['git', '-C', str(dep)]
+        self.assertEqual(status, 0)
+        self.assertEqual(commands, [
+            [*git, 'rev-parse', '--is-inside-work-tree'],
+            [*git, 'remote', 'get-url', 'origin'],
+            [*git, 'remote', 'add', 'origin', 'https://example.invalid/dep.git'],
+            [*git, 'rev-parse', '--verify', 'HEAD'],
+            [*git, 'fetch', '--depth', '1', 'origin', '0123456789abcdef'],
+            [*git, 'checkout', 'FETCH_HEAD'],
+        ])
+
+    def test_pinned_branch_is_detached_and_failure_propagates(self):
+        for fail, expected in [(None, 0), ('--detach', 1)]:
+            with self.subTest(fail=fail):
+                dep, commands, status = self.run_get_dep(fail, initialized=True, pinned=True)
+                self.assertEqual(status, expected)
+                self.assertEqual(commands[-1], ['git', '-C', str(dep), 'checkout', '--detach'])
+                self.assertFalse(any('fetch' in command for command in commands))
+
+    def test_invalid_git_metadata_stops_setup(self):
+        dep, commands, status = self.run_get_dep(initialized=True, valid=False)
+        self.assertEqual(status, 1)
+        self.assertEqual(commands, [['git', '-C', str(dep), 'rev-parse', '--is-inside-work-tree']])
+
 
 if __name__ == '__main__':
     unittest.main(verbosity=1)
diff --git a/tools/get_deps.py b/tools/get_deps.py
index 5160690..0bec83b 100755
--- a/tools/get_deps.py
+++ b/tools/get_deps.py
@@ -362,8 +362,8 @@
 
 
 def run_cmd(cmd):
-    r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-    title = f'Command Error: {cmd}'
+    r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    title = f'Command Error: {subprocess.list2cmdline([str(arg) for arg in cmd])}'
     if r.returncode != 0:
         print(title)
         print(r.stdout.decode("utf-8"))
@@ -381,35 +381,38 @@
     print(f'cloning {d} with {url}')
 
     p = Path(TOP / d)
-    git_cmd = f"git -C {p}"
+    git_cmd = ["git", "-C", str(p)]
 
     # Init git deps if not initialized. Checking only p.exists() lets git -C walk
     # up to TinyUSB's repository when an empty dependency directory is present.
     if not (p / '.git').exists():
         p.mkdir(parents=True, exist_ok=True)
-        if run_cmd(f"{git_cmd} init").returncode != 0 or not (p / '.git').exists():
+        if run_cmd([*git_cmd, "init"]).returncode != 0 or not (p / '.git').exists():
             return 1
-        if run_cmd(f"{git_cmd} remote add origin {url}").returncode != 0:
+        if run_cmd([*git_cmd, "remote", "add", "origin", url]).returncode != 0:
             return 1
         head = None
     else:
-        # Check if commit is already fetched
-        result = run_cmd(f"{git_cmd} rev-parse HEAD")
-        heads = result.stdout.decode("utf-8").splitlines()
-        if result.returncode != 0 or not heads:
+        result = run_cmd([*git_cmd, "rev-parse", "--is-inside-work-tree"])
+        if result.returncode != 0 or result.stdout.strip() != b"true":
             return 1
-        head = heads[0]
-        if run_cmd(f"{git_cmd} reset --hard").returncode != 0:
+        if run_cmd([*git_cmd, "remote", "get-url", "origin"]).returncode != 0 and \
+                run_cmd([*git_cmd, "remote", "add", "origin", url]).returncode != 0:
+            return 1
+        result = run_cmd([*git_cmd, "rev-parse", "--verify", "HEAD"])
+        heads = result.stdout.decode("utf-8").splitlines()
+        head = heads[0] if result.returncode == 0 and heads else None
+        if head is not None and run_cmd([*git_cmd, "reset", "--hard"]).returncode != 0:
             return 1
 
     if commit != head:
-        if run_cmd(f"{git_cmd} fetch --depth 1 origin {commit}").returncode != 0:
+        if run_cmd([*git_cmd, "fetch", "--depth", "1", "origin", commit]).returncode != 0:
             return 1
-        if run_cmd(f"{git_cmd} checkout FETCH_HEAD").returncode != 0:
+        if run_cmd([*git_cmd, "checkout", "FETCH_HEAD"]).returncode != 0:
             return 1
     elif dep_head(d) is None:
         # a branch at the pinned commit: detach it, as dep_head() readers expect
-        if run_cmd(f"{git_cmd} checkout --detach").returncode != 0:
+        if run_cmd([*git_cmd, "checkout", "--detach"]).returncode != 0:
             return 1
 
     # Remove files that conflict with TinyUSB's custom versions