Repository: xonsh/xonsh

Issue:
{gitstatus: {}} is no longer autohiding gitstatus
`{gitstatus: {}}` in my right prompt no longer hides itself when I'm not in a git repository.

## xonfig

<details>

```
$ xonfig
+------------------+---------------------+
| xonsh            | 0.13.0              |
| Python           | 3.10.5              |
| PLY              | 3.11                |
| have readline    | True                |
| prompt toolkit   | 3.0.30              |
| shell type       | prompt_toolkit      |
| history backend  | sqlite              |
| pygments         | 2.12.0              |
| on posix         | True                |
| on linux         | True                |
| distro           | unknown             |
| on wsl           | False               |
| on darwin        | False               |
| on windows       | False               |
| on cygwin        | False               |
| on msys2         | False               |
| is superuser     | False               |
| default encoding | utf-8               |
| xonsh encoding   | utf-8               |
| encoding errors  | surrogateescape     |
| xontrib          | []                  |
| RC file 1        | /home/monk/.xonshrc |
+------------------+---------------------+
```
</details>

## Expected Behavior
```
$ xonsh --no-rc
monk@lychee ~ $ $RIGHT_PROMPT='<{gitstatus: {}}>'
monk@lychee ~ $ $PROMPT_FIELDS['gitstatus'].fragments = ()                    <>
monk@lychee ~ $                                                               <>
```

## Current Behavior
```
$ xonsh --no-rc
monk@lychee ~ $ $RIGHT_PROMPT='<{gitstatus: {}}>'
monk@lychee ~ $ $PROMPT_FIELDS['gitstatus'].fragments = ()                 < |✓>
monk@lychee ~ $                                                              < >
```

## Steps to Reproduce
see above

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
Related to #4697
@jnoortheen please take a look
Thanks @jnoortheen. @t184256 please update from master!
Hm, not quite:

```
$ xonsh --no-rc
$ $RIGHT_PROMPT='<{gitstatus: {}}>'
$ $PROMPT_FIELDS['gitstatus'].fragments = ()                   < >
$                                                              < >
```

Tutorial says that

> The solution is to add a nested format string (separated with a colon)
> that will be invoked only if the value is not None:

so, maybe, to have a truly disappearing prompt section, that `exp` should be `None`?
it will be empty for None and empty string. The result from an PROMPT_FIELD is expected to be string
Then why is there a space in `< >`?
Yes the space should not appear. I am reopening this

Resolving patch:
diff --git a/xonsh/prompt/gitstatus.py b/xonsh/prompt/gitstatus.py
--- a/xonsh/prompt/gitstatus.py
+++ b/xonsh/prompt/gitstatus.py
@@ -92,12 +92,48 @@ def _get_sp_output(xsh, *args: str, **kwargs) -> str:
     return out
 
 
-class _GSField(PromptField):
+class _GitDir(PromptField):
+    _cwd = ""
+
+    def update(self, ctx):
+        # call the subprocess only if cwd changed
+        # or if value is None (in case `git init` was run)
+        from xonsh.dirstack import _get_cwd
+
+        cwd = _get_cwd()
+        if cwd != self._cwd or self.value is None:
+            self._cwd = cwd
+            self.value = _get_sp_output(
+                ctx.xsh, "git", "rev-parse", "--git-dir"
+            ).strip()
+            if self.value == "":
+                self.value = None
+
+
+repo_path = _GitDir()
+
+
+def inside_repo(ctx):
+    return ctx.pick_val(repo_path) is not None
+
+
+class GitStatusPromptField(PromptField):
+    """Only calls the updator if we are inside a git repository"""
+
+    def update(self, ctx):
+        if inside_repo(ctx):
+            if self.updator:
+                self.updator(self, ctx)
+        else:
+            self.value = None
+
+
+class _GSField(GitStatusPromptField):
     """wrap output from git command to value"""
 
     _args: "tuple[str, ...]" = ()
 
-    def update(self, ctx):
+    def updator(self, ctx):
         self.value = _get_sp_output(ctx.xsh, *self._args).strip()
 
 
@@ -105,7 +141,7 @@ def update(self, ctx):
 tag = _GSField(_args=("git", "describe", "--always"))
 
 
-@PromptField.wrap()
+@GitStatusPromptField.wrap()
 def tag_or_hash(fld: PromptField, ctx):
     fld.value = ctx.pick(tag) or ctx.pick(short_head)
 
@@ -116,23 +152,6 @@ def _parse_int(val: str, default=0):
     return default
 
 
-class _GitDir(_GSField):
-    _args = ("git", "rev-parse", "--git-dir")
-    _cwd = ""
-
-    def update(self, ctx):
-        # call the subprocess only if cwd changed
-        from xonsh.dirstack import _get_cwd
-
-        cwd = _get_cwd()
-        if cwd != self._cwd:
-            self._cwd = cwd
-            super().update(ctx)
-
-
-repo_path = _GitDir()
-
-
 def get_stash_count(gitdir: str):
     """Get git-stash count"""
     with contextlib.suppress(OSError):
@@ -141,7 +160,7 @@ def get_stash_count(gitdir: str):
     return 0
 
 
-@PromptField.wrap(prefix="⚑")
+@GitStatusPromptField.wrap(prefix="⚑")
 def stash_count(fld: PromptField, ctx: PromptFields):
     fld.value = get_stash_count(ctx.pick_val(repo_path))
 
@@ -160,7 +179,7 @@ def get_operations(gitdir: str):
             yield name
 
 
-@PromptField.wrap(prefix="{CYAN}", separator="|")
+@GitStatusPromptField.wrap(prefix="{CYAN}", separator="|")
 def operations(fld, ctx: PromptFields) -> None:
     gitdir = ctx.pick_val(repo_path)
     op = fld.separator.join(get_operations(gitdir))
@@ -170,7 +189,7 @@ def operations(fld, ctx: PromptFields) -> None:
         fld.value = ""
 
 
-@PromptField.wrap()
+@GitStatusPromptField.wrap()
 def porcelain(fld, ctx: PromptFields):
     """Return parsed values from ``git status --porcelain``"""
 
@@ -228,7 +247,7 @@ def get_gitstatus_info(fld: "_GSInfo", ctx: PromptFields) -> None:
     fld.value = info[fld.info]
 
 
-class _GSInfo(PromptField):
+class _GSInfo(GitStatusPromptField):
     info: str
 
     def __init__(self, **kwargs):
@@ -246,7 +265,7 @@ def __init__(self, **kwargs):
 staged = _GSInfo(prefix="{RED}●", suffix="{RESET}", info="staged")
 
 
-@PromptField.wrap()
+@GitStatusPromptField.wrap()
 def numstat(fld, ctx):
     changed = _get_sp_output(ctx.xsh, "git", "diff", "--numstat")
 
@@ -262,17 +281,17 @@ def numstat(fld, ctx):
     fld.value = (insert, delete)
 
 
-@PromptField.wrap(prefix="{BLUE}+", suffix="{RESET}")
+@GitStatusPromptField.wrap(prefix="{BLUE}+", suffix="{RESET}")
 def lines_added(fld: PromptField, ctx: PromptFields):
     fld.value = ctx.pick_val(numstat)[0]
 
 
-@PromptField.wrap(prefix="{RED}-", suffix="{RESET}")
+@GitStatusPromptField.wrap(prefix="{RED}-", suffix="{RESET}")
 def lines_removed(fld: PromptField, ctx):
     fld.value = ctx.pick_val(numstat)[-1]
 
 
-@PromptField.wrap(prefix="{BOLD_GREEN}", suffix="{RESET}", symbol="✓")
+@GitStatusPromptField.wrap(prefix="{BOLD_GREEN}", suffix="{RESET}", symbol="✓")
 def clean(fld, ctx):
     changes = sum(
         ctx.pick_val(f)
@@ -319,11 +338,11 @@ def get_frags(self, env):
                 continue
             yield frag
 
-    def _collect(self, ctx):
-        if not ctx.pick_val(repo_path):
-            # no need to display any other fragments
-            return
-        yield from super()._collect(ctx)
+    def update(self, ctx):
+        if inside_repo(ctx):
+            super().update(ctx)
+        else:
+            self.value = None
 
 
 gitstatus = GitStatus()


Test patch:
diff --git a/tests/prompt/test_gitstatus.py b/tests/prompt/test_gitstatus.py
--- a/tests/prompt/test_gitstatus.py
+++ b/tests/prompt/test_gitstatus.py
@@ -79,19 +79,14 @@ def test_gitstatus_clean(prompts, fake_proc):
 def test_no_git(prompts, fake_process, tmp_path):
     os.chdir(tmp_path)
     err = b"fatal: not a git repository (or any of the parent directories): .git"
-    for cmd in (
-        "git status --porcelain --branch",
-        "git rev-parse --git-dir",
-        "git diff --numstat",
-    ):
-        fake_process.register_subprocess(
-            command=cmd,
-            stderr=err,
-            returncode=128,
-        )
-
-    exp = ""
-    assert prompts.pick_val("gitstatus.repo_path") == ""
-    assert format(prompts.pick("gitstatus")) == exp
-    assert _format_value(prompts.pick("gitstatus"), None, None) == exp
-    assert _format_value(prompts.pick("gitstatus"), "{}", None) == exp
+    fake_process.register_subprocess(
+        command="git rev-parse --git-dir", stderr=err, returncode=128
+    )
+
+    # test that all gitstatus fields (gitstatus, gitstatus.branch,
+    # gitstatus.porceclain, etc) are None and are formatted correctly in a
+    # format string like {gitstatus: hello {}}
+    for field in prompts.get_fields(gitstatus):
+        assert prompts.pick_val(field) is None
+        assert _format_value(prompts.pick(field), None, None) == ""
+        assert _format_value(prompts.pick(field), "hello {}", None) == ""