Repository: cookiecutter/cookiecutter

Issue:
Add checkout details to context
* Cookiecutter version: 2.1.1
* Template project url: N/A
* Python version: 3.9.13
* Operating System: macOS

### Description:

I would like the context output available in `{{cookiecutter | pprint }}` to include the `--checkout` argument passed to the CLI, under a '_checkout' field. This will allow me to perform different actions in my post generation hooks if a non-default branch, tag, or commit was passed.

### What I've run:

```
from collections import OrderedDict
cookiecutter_context = {{cookiecutter | pprint }}
print(cookiecutter_context)
```

Related to https://github.com/cookiecutter/cookiecutter/issues/1422

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 44c7128..3ea51b4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,8 +10,8 @@ repos:
         language: python
         files: \.rst$
         require_serial: true
-  - repo: https://github.com/psf/black.git
-    rev: 23.7.0
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.9.1
     hooks:
       - id: black
         language_version: python3
diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py
index 69764db..064423d 100644
--- a/cookiecutter/__init__.py
+++ b/cookiecutter/__init__.py
@@ -6,7 +6,7 @@ def _get_version() -> str:
     """Read VERSION.txt and return its contents."""
     path = Path(__file__).parent.resolve()
     version_file = path / "VERSION.txt"
-    return version_file.read_text().strip()
+    return version_file.read_text(encoding="utf-8").strip()
 
 
 __version__ = _get_version()
diff --git a/cookiecutter/main.py b/cookiecutter/main.py
index cf9d083..9516f73 100644
--- a/cookiecutter/main.py
+++ b/cookiecutter/main.py
@@ -154,16 +154,20 @@ def cookiecutter(
                 keep_project_on_failure=keep_project_on_failure,
             )
 
-    logger.debug('contex is %s', context)
+    logger.debug('context is %s', context)
 
     # include template dir or url in the context dict
     context['cookiecutter']['_template'] = template
 
     # include output+dir in the context dict
     context['cookiecutter']['_output_dir'] = os.path.abspath(output_dir)
+
     # include repo dir or url in the context dict
     context['cookiecutter']['_repo_dir'] = repo_dir
 
+    # include checkout details in the context dict
+    context['cookiecutter']['_checkout'] = checkout
+
     dump(config_dict['replay_dir'], template_name, context)
 
     # Create project from local context and project template.
diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py
index f5c2824..16830d4 100644
--- a/cookiecutter/prompt.py
+++ b/cookiecutter/prompt.py
@@ -20,7 +20,13 @@ def read_user_variable(var_name, default_value, prompts=None, prefix=""):
         if prompts and var_name in prompts.keys() and prompts[var_name]
         else var_name
     )
-    return Prompt.ask(f"{prefix}{question}", default=default_value)
+
+    while True:
+        variable = Prompt.ask(f"{prefix}{question}", default=default_value)
+        if variable is not None:
+            break
+
+    return variable
 
 
 class YesNoPrompt(Confirm):
@@ -243,8 +249,10 @@ def prompt_for_config(context, no_input=False):
     # values might refer to them.
 
     count = 0
-    size = len(context['cookiecutter'].items())
-    for key, raw in context['cookiecutter'].items():
+    all_prompts = context['cookiecutter'].items()
+    visible_prompts = [k for k, _ in all_prompts if not k.startswith("_")]
+    size = len(visible_prompts)
+    for key, raw in all_prompts:
         if key.startswith('_') and not key.startswith('__'):
             cookiecutter_dict[key] = raw
             continue
diff --git a/cookiecutter/replay.py b/cookiecutter/replay.py
index d3c9898..5558584 100644
--- a/cookiecutter/replay.py
+++ b/cookiecutter/replay.py
@@ -31,7 +31,7 @@ def dump(replay_dir: "os.PathLike[str]", template_name: str, context: dict):
 
     replay_file = get_file_name(replay_dir, template_name)
 
-    with open(replay_file, 'w') as outfile:
+    with open(replay_file, 'w', encoding="utf-8") as outfile:
         json.dump(context, outfile, indent=2)
 
 
@@ -42,7 +42,7 @@ def load(replay_dir, template_name):
 
     replay_file = get_file_name(replay_dir, template_name)
 
-    with open(replay_file) as infile:
+    with open(replay_file, encoding="utf-8") as infile:
         context = json.load(infile)
 
     if 'cookiecutter' not in context:
diff --git a/docs/advanced/human_readable_prompts.rst b/docs/advanced/human_readable_prompts.rst
index ee30e12..51ebc92 100644
--- a/docs/advanced/human_readable_prompts.rst
+++ b/docs/advanced/human_readable_prompts.rst
@@ -22,15 +22,15 @@ See the following cookiecutter config as example:
         "init_git": true,
         "linting": ["ruff", "flake8", "none"],
         "__prompts__": {
-            "package_name": "Select your package name:",
-            "module_name": "Select your module name:",
-            "package_name_stylized": "Stylized package name:",
-            "short_description": "Short description:",
-            "github_username": "GitHub username or organization:",
-            "full_name": "Author full name:",
-            "email": "Author email:",
-            "command_line_interface": "Add CLI:",
-            "init_git": "Initialize a git repository:",
+            "package_name": "Select your package name",
+            "module_name": "Select your module name",
+            "package_name_stylized": "Stylized package name",
+            "short_description": "Short description",
+            "github_username": "GitHub username or organization",
+            "full_name": "Author full name",
+            "email": "Author email",
+            "command_line_interface": "Add CLI",
+            "init_git": "Initialize a git repository",
             "linting": {
                 "__prompt__": "Which linting tool do you want to use?",
                 "ruff": "Ruff",
diff --git a/docs/installation.rst b/docs/installation.rst
index 995fd07..00e8d5f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -104,6 +104,12 @@ Alternate installations
 
     brew install cookiecutter
 
+**Void Linux:**
+
+.. code-block:: bash
+
+    xbps-install cookiecutter
+
 **Pipx (Linux, OSX and Windows):**
 
 .. code-block:: bash
diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst
index d16f6b7..0a93191 100644
--- a/docs/tutorials/tutorial2.rst
+++ b/docs/tutorials/tutorial2.rst
@@ -66,7 +66,7 @@ By running following command `cookiecutter.zip` will get generated which can be
 .. code-block:: bash
 
    $ (SOURCE_DIR=$(basename $PWD) ZIP=cookiecutter.zip && # Set variables
-   pushd && # Set parent directory as working directory
+   pushd .. && # Set parent directory as working directory
    zip -r $ZIP $SOURCE_DIR --exclude $SOURCE_DIR/$ZIP --quiet && # ZIP cookiecutter
    mv $ZIP $SOURCE_DIR/$ZIP && # Move ZIP to original directory
    popd && # Restore original work directory


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 93f0537..ce9d9b9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -6,7 +6,6 @@ from pathlib import Path
 
 import pytest
 from click.testing import CliRunner
-
 from cookiecutter import utils
 from cookiecutter.__main__ import main
 from cookiecutter.environment import StrictEnvironment
@@ -432,6 +431,7 @@ def test_echo_undefined_variable_error(output_dir, cli_runner):
             '_template': template_path,
             '_repo_dir': template_path,
             '_output_dir': output_dir,
+            '_checkout': None,
         },
     }
     context_str = json.dumps(context, indent=4, sort_keys=True)
diff --git a/tests/test_read_user_variable.py b/tests/test_read_user_variable.py
index 02890a1..6fac4d3 100644
--- a/tests/test_read_user_variable.py
+++ b/tests/test_read_user_variable.py
@@ -1,18 +1,36 @@
 """test_read_user_variable."""
+import pytest
 from cookiecutter.prompt import read_user_variable
 
 VARIABLE = 'project_name'
 DEFAULT = 'Kivy Project'
 
 
-def test_click_invocation(mocker):
+@pytest.fixture
+def mock_prompt(mocker):
+    """Return a mocked version of the 'Prompt.ask' function."""
+    return mocker.patch('rich.prompt.Prompt.ask')
+
+
+def test_click_invocation(mock_prompt):
     """Test click function called correctly by cookiecutter.
 
     Test for string type invocation.
     """
-    prompt = mocker.patch('rich.prompt.Prompt.ask')
-    prompt.return_value = DEFAULT
+    mock_prompt.return_value = DEFAULT
 
     assert read_user_variable(VARIABLE, DEFAULT) == DEFAULT
 
-    prompt.assert_called_once_with(VARIABLE, default=DEFAULT)
+    mock_prompt.assert_called_once_with(VARIABLE, default=DEFAULT)
+
+
+def test_input_loop_with_null_default_value(mock_prompt):
+    """Test `Prompt.ask` is run repeatedly until a valid answer is provided.
+
+    Test for `default_value` parameter equal to None.
+    """
+    # Simulate user providing None input initially and then a valid input
+    mock_prompt.side_effect = [None, DEFAULT]
+
+    assert read_user_variable(VARIABLE, None) == DEFAULT
+    assert mock_prompt.call_count == 2