Repository: datalad/datalad-next

Issue:
`next-status` claims nothing to save when all is staged and no HEAD
Clearly misleading and wrong

```
❯ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   datasalad/__init__.py
        new file:   pyproject.toml

❯ datalad next-status
nothing to save, working tree clean
```

Discussion:

Patch:
diff --git a/changelog.d/20240514_071318_michael.hanke_bf_680.md b/changelog.d/20240514_071318_michael.hanke_bf_680.md
new file mode 100644
index 0000000..c64301e
--- /dev/null
+++ b/changelog.d/20240514_071318_michael.hanke_bf_680.md
@@ -0,0 +1,6 @@
+### 🐛 Bug Fixes
+
+- `next-status` now detects staged changes in repositories with no
+  commit.
+  Fixes https://github.com/datalad/datalad-next/issues/680 via
+  https://github.com/datalad/datalad-next/pull/681 (by @mih)
diff --git a/datalad_next/iter_collections/gitstatus.py b/datalad_next/iter_collections/gitstatus.py
index 7b94d26..1088d1d 100644
--- a/datalad_next/iter_collections/gitstatus.py
+++ b/datalad_next/iter_collections/gitstatus.py
@@ -11,6 +11,7 @@ from pathlib import (
 )
 from typing import Generator
 
+from datalad_next.consts import PRE_INIT_COMMIT_SHA
 from datalad_next.runners import (
     CommandError,
     call_git_lines,
@@ -99,6 +100,10 @@ def iter_gitstatus(
     path = Path(path)
 
     head, corresponding_head = get_worktree_head(path)
+    if head is None:
+        # no commit at all -> compare to an empty repo.
+        head = PRE_INIT_COMMIT_SHA
+
     # TODO it would make sense to always (or optionally) compare against any
     # existing corresponding_head. This would make the status communicate
     # anything that has not made it into the corresponding branch yet
@@ -162,15 +167,15 @@ def _yield_dir_items(
             )
         }
     # diff constrained to direct children
-    for item in ([] if head is None else iter_gitdiff(
+    for item in iter_gitdiff(
         path,
-        from_treeish='HEAD',
+        from_treeish=head,
         # to the worktree
         to_treeish=None,
         recursive='no',
         # TODO trim scope like in repo_items
         eval_submodule_state=eval_submodule_state,
-    )):
+    ):
         if item.status != GitDiffStatus.deletion \
                 and item.gittype in container_types:
             if item.gittype == GitTreeItemType.submodule:
@@ -251,9 +256,9 @@ def _yield_repo_items(
         str(item.name): item for item in iter_submodules(path)
     }
     # start with a repository-contrained diff against the worktree
-    for item in ([] if head is None else iter_gitdiff(
+    for item in iter_gitdiff(
         path,
-        from_treeish='HEAD',
+        from_treeish=head,
         # to the worktree
         to_treeish=None,
         recursive='repository',
@@ -261,7 +266,7 @@ def _yield_repo_items(
         # We need to redo some check for adjusted mode, and other cases anyways
         eval_submodule_state='commit'
         if eval_submodule_state == 'full' else eval_submodule_state,
-    )):
+    ):
         # immediately investigate any submodules that are already
         # reported modified by Git
         if item.gittype == GitTreeItemType.submodule:
diff --git a/datalad_next/repo_utils/worktree.py b/datalad_next/repo_utils/worktree.py
index b43cfd7..e5fb742 100644
--- a/datalad_next/repo_utils/worktree.py
+++ b/datalad_next/repo_utils/worktree.py
@@ -12,6 +12,16 @@ from datalad_next.runners import (
 def get_worktree_head(
     path: Path,
 ) -> tuple[str | None, str | None]:
+    """Returns the symbolic name of the worktree `HEAD` at the given path
+
+    Returns
+    -------
+    tuple
+      The first item is the symbolic name of the worktree `HEAD`, or `None`
+      if there is no commit.
+      The second item is the symbolic name of the "corresponding branch" in
+      an adjusted-mode git-annex repository, or `None`.
+    """
     try:
         HEAD = call_git_lines(
             # we add the pathspec disambiguator to get cleaner error messages


Test patch:
diff --git a/datalad_next/iter_collections/tests/test_itergitstatus.py b/datalad_next/iter_collections/tests/test_itergitstatus.py
index 20e40ce..7d4487e 100644
--- a/datalad_next/iter_collections/tests/test_itergitstatus.py
+++ b/datalad_next/iter_collections/tests/test_itergitstatus.py
@@ -241,3 +241,15 @@ def test_status_gitinit(tmp_path):
         assert len(res) == 1
         assert res[0].name == 'untracked'
         assert res[0].status == GitDiffStatus.other
+
+
+def test_status_nohead_staged(tmp_path):
+    # initialize a fresh git repo, but make no commits
+    assert call_git_success(['init'], cwd=tmp_path)
+    # stage a file
+    (tmp_path / 'probe').write_text('tostage')
+    assert call_git_success(['add', 'probe'], cwd=tmp_path)
+    _assert_testcases(
+        {i.name: i for i in iter_gitstatus(tmp_path)},
+        [{'name': 'probe', 'status': GitDiffStatus.addition}],
+    )