Repository: copier-org/copier (python)

Issue:
missing config when using git clone
### Describe the problem

When copier sees a template dir that's a dirty git repo, it will run git clone to a temp dir, and then try to commit dirty changes.
However, since no author is passed by copier, git will fail if no global user.name/user.email is set.

### Template

- init a git repo
- commit some files
- add untracked files

### To Reproduce

`copier /path/to/template .`

### Logs

```console
Command line: | /usr/bin/git --git-dir=.git --work-tree=/home/x/templates/pyproject commit -m 'Copier automated commit for draft changes' --no-verify --no-gpg-sign
Stderr:       | Author identity unknown
              | 
              | *** Please tell me who you are.
              | 
              | Run
              | 
              |   git config --global user.email "you@example.com"
              |   git config --global user.name "Your Name"
              | 
              | to set your account's default identity.
              | Omit --global to set the identity only in this repository.
              | 
```


### Expected behavior

Since the commit is temporary, the author information is not important.
I would expect copier to pass something like `--author='copier <copier@example.com>'` to `git-commit`

### Screenshots/screencasts/logs

_No response_

### Operating system

Linux

### Operating system distribution and version

debian

### Copier version

9.3.1

### Python version

3.12

### Installation method

pipx+pypi

### Additional context

_No response_

Pull request:
fix: support Git config without user identity
I've fixed a problem that caused Copier to fail when Git didn't know the user identity (e.g. via system/global `user.{name,email}` settings) while generating a project based on a dirty template.

The problem was caused by a `git commit` call happening during the `copier.vcs.clone()` call on a dirty template:

https://github.com/copier-org/copier/blob/78c484379804bfdd20a652e2ebb5e5c940b9b828/copier/vcs.py#L192-L200

When the user identity wasn't known to Git, it failed with the following error:

```
Author identity unknown

*** Please tell me who you are.
 
Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.
```

Instead of setting `user.{name,email}` before our internal commits and unsetting them afterwards, I extended the `git` command created by `copier.vcs.get_git()` to include the [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing) `GIT_{AUTHOR,COMMITTER}_{NAME,EMAIL}`, which take precedence over config files, such that all our internal `git commit` calls are made with user identity information. At the same time, I've unset the user identity information in our test suite to make sure we don't implicitly depend on this information being available. To make `git commit` calls in our tests, I've created a new `git` helper which includes user identity information via the same environment variables as before but with a different name and email to distinguish our internal Copier Git user from the test user.

Fixes #1714.

Patch:
diff --git a/copier/main.py b/copier/main.py
index 960a41b..9991628 100644
--- a/copier/main.py
+++ b/copier/main.py
@@ -1104,8 +1104,6 @@ class Worker:
 
     def _git_commit(self, message: str = "dumb commit") -> None:
         git = get_git()
-        git("config", "user.name", "Copier")
-        git("config", "user.email", "copier@copier")
         # 1st commit could fail if any pre-commit hook reformats code
         # 2nd commit uses --no-verify to disable pre-commit-like checks
         git(
@@ -1124,8 +1122,6 @@ class Worker:
             "--no-gpg-sign",
             "--no-verify",
         )
-        git("config", "--unset", "user.name")
-        git("config", "--unset", "user.email")
 
 
 def run_copy(
diff --git a/copier/vcs.py b/copier/vcs.py
index c0dcf26..40f8e91 100644
--- a/copier/vcs.py
+++ b/copier/vcs.py
@@ -17,10 +17,18 @@ from plumbum.machines import LocalCommand
 from .errors import DirtyLocalWarning, ShallowCloneWarning
 from .types import OptBool, OptStrOrPath, StrOrPath
 
+GIT_USER_NAME = "Copier"
+GIT_USER_EMAIL = "copier@copier"
+
 
 def get_git(context_dir: OptStrOrPath = None) -> LocalCommand:
     """Gets `git` command, or fails if it's not available."""
-    command = local["git"]
+    command = local["git"].with_env(
+        GIT_AUTHOR_NAME=GIT_USER_NAME,
+        GIT_AUTHOR_EMAIL=GIT_USER_EMAIL,
+        GIT_COMMITTER_NAME=GIT_USER_NAME,
+        GIT_COMMITTER_EMAIL=GIT_USER_EMAIL,
+    )
     if context_dir:
         command = command["-C", context_dir]
     return command


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index bfdb982..c5d5ba8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,13 +2,13 @@ from __future__ import annotations
 
 import platform
 import sys
-from typing import Iterator
+from typing import Any, Iterator
 
 import pytest
 from coverage.tracer import CTracer
 from pexpect.popen_spawn import PopenSpawn
 from plumbum import local
-from pytest_gitconfig.plugin import GitConfig
+from pytest_gitconfig.plugin import DELETE, GitConfig
 
 from .helpers import Spawn
 
@@ -38,6 +38,18 @@ def spawn() -> Spawn:
     return _spawn
 
 
+@pytest.fixture(scope="session")
+def default_git_user_name() -> Any:
+    """Unset the default Git user name."""
+    return DELETE
+
+
+@pytest.fixture(scope="session")
+def default_git_user_email() -> Any:
+    """Unset the default Git user email."""
+    return DELETE
+
+
 @pytest.fixture(scope="session", autouse=True)
 def default_gitconfig(default_gitconfig: GitConfig) -> GitConfig:
     """
diff --git a/tests/helpers.py b/tests/helpers.py
index 9eb1840..bee3287 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -12,9 +12,11 @@ from typing import Any, Mapping, Protocol
 
 from pexpect.popen_spawn import PopenSpawn
 from plumbum import local
-from plumbum.cmd import git
+from plumbum.cmd import git as _git
+from plumbum.machines import LocalCommand
 from prompt_toolkit.input.ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
 from prompt_toolkit.keys import Keys
+from pytest_gitconfig.plugin import DEFAULT_GIT_USER_EMAIL, DEFAULT_GIT_USER_NAME
 
 import copier
 from copier.types import StrOrPath
@@ -139,6 +141,14 @@ def expect_prompt(
             tui.expect_exact(f"({expected_type})")
 
 
+git: LocalCommand = _git.with_env(
+    GIT_AUTHOR_NAME=DEFAULT_GIT_USER_NAME,
+    GIT_AUTHOR_EMAIL=DEFAULT_GIT_USER_EMAIL,
+    GIT_COMMITTER_NAME=DEFAULT_GIT_USER_NAME,
+    GIT_COMMITTER_EMAIL=DEFAULT_GIT_USER_EMAIL,
+)
+
+
 def git_save(
     dst: StrOrPath = ".",
     message: str = "Test commit",
@@ -168,7 +178,5 @@ def git_init(message: str = "hello world") -> None:
         message: The first commit message.
     """
     git("init")
-    git("config", "user.name", "Copier Test")
-    git("config", "user.email", "test@copier")
     git("add", ".")
     git("commit", "-m", message)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index eb9c741..8c6331a 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -6,11 +6,10 @@ from typing import Callable, Generator
 import pytest
 import yaml
 from plumbum import local
-from plumbum.cmd import git
 
 from copier.cli import CopierApp
 
-from .helpers import COPIER_CMD, build_file_tree
+from .helpers import COPIER_CMD, build_file_tree, git
 
 
 @pytest.fixture(scope="module")
diff --git a/tests/test_complex_questions.py b/tests/test_complex_questions.py
index 8e61cbf..15f3ac7 100644
--- a/tests/test_complex_questions.py
+++ b/tests/test_complex_questions.py
@@ -10,7 +10,6 @@ import pytest
 import yaml
 from pexpect.popen_spawn import PopenSpawn
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import run_copy, run_update
 
@@ -23,6 +22,7 @@ from .helpers import (
     Spawn,
     build_file_tree,
     expect_prompt,
+    git,
 )
 
 BLK_START = BRACKET_ENVOPS["block_start_string"]
diff --git a/tests/test_conditional_file_name.py b/tests/test_conditional_file_name.py
index 4c19941..599dc6e 100644
--- a/tests/test_conditional_file_name.py
+++ b/tests/test_conditional_file_name.py
@@ -1,12 +1,11 @@
 import pexpect
 import pytest
 from plumbum import local
-from plumbum.cmd import git
 from pytest import TempPathFactory
 
 import copier
 
-from .helpers import COPIER_PATH, Spawn, build_file_tree, expect_prompt
+from .helpers import COPIER_PATH, Spawn, build_file_tree, expect_prompt, git
 
 
 def test_render_conditional(tmp_path_factory: TempPathFactory) -> None:
diff --git a/tests/test_copy.py b/tests/test_copy.py
index 513391b..edaca49 100644
--- a/tests/test_copy.py
+++ b/tests/test_copy.py
@@ -15,7 +15,6 @@ from typing import Any, ContextManager
 import pytest
 import yaml
 from plumbum import local
-from plumbum.cmd import git
 
 import copier
 from copier import run_copy
@@ -27,6 +26,7 @@ from .helpers import (
     PROJECT_TEMPLATE,
     assert_file,
     build_file_tree,
+    git,
     git_save,
     render,
 )
diff --git a/tests/test_demo_update_tasks.py b/tests/test_demo_update_tasks.py
index b437331..7dd9648 100644
--- a/tests/test_demo_update_tasks.py
+++ b/tests/test_demo_update_tasks.py
@@ -1,10 +1,9 @@
 import pytest
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import run_copy, run_update
 
-from .helpers import build_file_tree
+from .helpers import build_file_tree, git
 
 
 def test_update_tasks(tmp_path_factory: pytest.TempPathFactory) -> None:
@@ -58,9 +57,6 @@ def test_update_tasks(tmp_path_factory: pytest.TempPathFactory) -> None:
     # Init destination as a new independent git repo
     with local.cwd(dst):
         git("init")
-        # Configure git in case you're running in CI
-        git("config", "user.name", "Copier Test")
-        git("config", "user.email", "test@copier")
         # Commit changes
         git("add", ".")
         git("commit", "-m", "hello world")
diff --git a/tests/test_dirty_local.py b/tests/test_dirty_local.py
index 2d5957f..9b210bc 100644
--- a/tests/test_dirty_local.py
+++ b/tests/test_dirty_local.py
@@ -3,14 +3,13 @@ from shutil import copy2, copytree
 
 import pytest
 from plumbum import local
-from plumbum.cmd import git
 from pytest_gitconfig.plugin import GitConfig
 
 import copier
 from copier.errors import DirtyLocalWarning
 from copier.main import run_copy, run_update
 
-from .helpers import DATA, PROJECT_TEMPLATE, build_file_tree
+from .helpers import DATA, PROJECT_TEMPLATE, build_file_tree, git
 
 
 def test_copy(tmp_path_factory: pytest.TempPathFactory) -> None:
diff --git a/tests/test_legacy_migration.py b/tests/test_legacy_migration.py
index c560127..853eef1 100644
--- a/tests/test_legacy_migration.py
+++ b/tests/test_legacy_migration.py
@@ -6,12 +6,11 @@ from shutil import copytree
 import pytest
 import yaml
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import run_copy, run_update
 from copier.errors import UserMessageError
 
-from .helpers import BRACKET_ENVOPS_JSON, PROJECT_TEMPLATE, build_file_tree
+from .helpers import BRACKET_ENVOPS_JSON, PROJECT_TEMPLATE, build_file_tree, git
 
 SRC = Path(f"{PROJECT_TEMPLATE}_legacy_migrations").absolute()
 
@@ -33,8 +32,6 @@ def test_migrations_and_tasks(tmp_path: Path, skip_tasks: bool) -> None:
     copytree(SRC, src)
     with local.cwd(src):
         git("init")
-        git("config", "user.name", "Copier Test")
-        git("config", "user.email", "test@copier")
         git("add", ".")
         git("commit", "-m1")
         git("tag", "v1.0.0")
@@ -66,8 +63,6 @@ def test_migrations_and_tasks(tmp_path: Path, skip_tasks: bool) -> None:
     with local.cwd(dst):
         git("init")
         git("add", ".")
-        git("config", "user.name", "Copier Test")
-        git("config", "user.email", "test@copier")
         git("commit", "-m1")
     # Update it to v2
     run_update(
diff --git a/tests/test_migrations.py b/tests/test_migrations.py
index b0ed397..b77f50d 100644
--- a/tests/test_migrations.py
+++ b/tests/test_migrations.py
@@ -3,7 +3,6 @@ from pathlib import Path
 import pytest
 import yaml
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import run_copy, run_update
 from copier.errors import UnsafeTemplateError, UserMessageError
@@ -12,6 +11,7 @@ from .helpers import (
     COPIER_ANSWERS_FILE,
     PROJECT_TEMPLATE,
     build_file_tree,
+    git,
     git_save,
 )
 
diff --git a/tests/test_minimum_version.py b/tests/test_minimum_version.py
index c01aa6e..7f71168 100644
--- a/tests/test_minimum_version.py
+++ b/tests/test_minimum_version.py
@@ -4,7 +4,6 @@ from pathlib import Path
 import pytest
 from packaging.version import Version
 from plumbum import local
-from plumbum.cmd import git
 
 import copier
 from copier.errors import (
@@ -13,7 +12,7 @@ from copier.errors import (
     UnsupportedVersionError,
 )
 
-from .helpers import build_file_tree
+from .helpers import build_file_tree, git
 
 
 @pytest.fixture(scope="module")
@@ -64,8 +63,6 @@ def test_minimum_version_update(
 
     with local.cwd(tmp_path):
         git("init")
-        git("config", "user.name", "Copier Test")
-        git("config", "user.email", "test@copier")
         git("add", ".")
         git("commit", "-m", "hello world")
 
diff --git a/tests/test_output.py b/tests/test_output.py
index f33abf6..6f075d1 100644
--- a/tests/test_output.py
+++ b/tests/test_output.py
@@ -4,12 +4,11 @@ from pathlib import Path
 import pexpect
 import pytest
 from plumbum import local
-from plumbum.cmd import git
 
 from copier.errors import InvalidTypeError
 from copier.main import run_copy, run_recopy, run_update
 
-from .helpers import COPIER_PATH, Spawn, build_file_tree, expect_prompt, render
+from .helpers import COPIER_PATH, Spawn, build_file_tree, expect_prompt, git, render
 
 
 def test_output(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
diff --git a/tests/test_prompt.py b/tests/test_prompt.py
index 7346cdb..43a85f3 100644
--- a/tests/test_prompt.py
+++ b/tests/test_prompt.py
@@ -9,7 +9,6 @@ import pytest
 import yaml
 from pexpect.popen_spawn import PopenSpawn
 from plumbum import local
-from plumbum.cmd import git
 
 from copier.types import StrOrPath
 
@@ -22,6 +21,7 @@ from .helpers import (
     Spawn,
     build_file_tree,
     expect_prompt,
+    git,
     git_save,
 )
 
diff --git a/tests/test_subdirectory.py b/tests/test_subdirectory.py
index 3f24646..0a1ea60 100644
--- a/tests/test_subdirectory.py
+++ b/tests/test_subdirectory.py
@@ -5,11 +5,10 @@ from typing import Literal
 import pytest
 import yaml
 from plumbum import local
-from plumbum.cmd import git
 
 import copier
 
-from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree, git_init
+from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree, git, git_init
 
 
 @pytest.fixture(scope="module")
diff --git a/tests/test_symlinks.py b/tests/test_symlinks.py
index c787c9e..579def1 100644
--- a/tests/test_symlinks.py
+++ b/tests/test_symlinks.py
@@ -3,13 +3,12 @@ from pathlib import Path
 
 import pytest
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import run_copy, run_update
 from copier.errors import DirtyLocalWarning
 from copier.tools import readlink
 
-from .helpers import build_file_tree
+from .helpers import build_file_tree, git
 
 
 def test_copy_symlink(tmp_path_factory: pytest.TempPathFactory) -> None:
diff --git a/tests/test_templated_prompt.py b/tests/test_templated_prompt.py
index bccf0d2..c69c49c 100644
--- a/tests/test_templated_prompt.py
+++ b/tests/test_templated_prompt.py
@@ -9,7 +9,6 @@ import pytest
 import yaml
 from pexpect.popen_spawn import PopenSpawn
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import Worker
 from copier.errors import InvalidTypeError
@@ -24,6 +23,7 @@ from .helpers import (
     Spawn,
     build_file_tree,
     expect_prompt,
+    git,
     git_init,
 )
 
diff --git a/tests/test_tmpdir.py b/tests/test_tmpdir.py
index 3dac756..07fe621 100644
--- a/tests/test_tmpdir.py
+++ b/tests/test_tmpdir.py
@@ -1,12 +1,11 @@
 from pathlib import Path
 
 import pytest
-from plumbum.cmd import git
 
 from copier.cli import CopierApp
 from copier.main import run_copy, run_recopy, run_update
 
-from .helpers import build_file_tree
+from .helpers import build_file_tree, git
 
 
 @pytest.fixture(scope="module")
diff --git a/tests/test_tools.py b/tests/test_tools.py
index 5f601aa..27dadb2 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -3,11 +3,12 @@ from stat import S_IREAD
 from tempfile import TemporaryDirectory
 
 import pytest
-from plumbum.cmd import git
 from poethepoet.app import PoeThePoet
 
 from copier.tools import normalize_git_path
 
+from .helpers import git
+
 
 def test_types() -> None:
     """Ensure source code static typing."""
diff --git a/tests/test_unsafe.py b/tests/test_unsafe.py
index e501edb..1ee8d16 100644
--- a/tests/test_unsafe.py
+++ b/tests/test_unsafe.py
@@ -7,14 +7,13 @@ import pytest
 import yaml
 from jinja2.ext import Extension
 from plumbum import local
-from plumbum.cmd import git
 
 from copier.cli import CopierApp
 from copier.errors import UnsafeTemplateError
 from copier.main import run_copy, run_update
 from copier.types import AnyByStrDict
 
-from .helpers import build_file_tree
+from .helpers import build_file_tree, git
 
 
 class JinjaExtension(Extension):
diff --git a/tests/test_updatediff.py b/tests/test_updatediff.py
index 81a2019..9c8db36 100644
--- a/tests/test_updatediff.py
+++ b/tests/test_updatediff.py
@@ -10,7 +10,6 @@ import pexpect
 import pytest
 import yaml
 from plumbum import local
-from plumbum.cmd import git
 
 from copier.cli import CopierApp
 from copier.errors import UserMessageError
@@ -24,6 +23,7 @@ from .helpers import (
     SUFFIX_TMPL,
     Spawn,
     build_file_tree,
+    git,
     git_init,
 )
 
@@ -185,9 +185,6 @@ def test_updatediff(tmp_path_factory: pytest.TempPathFactory) -> None:
     # Init destination as a new independent git repo
     with local.cwd(target):
         git("init")
-        # Configure git in case you're running in CI
-        git("config", "user.name", "Copier Test")
-        git("config", "user.email", "test@copier")
         # Commit changes
         git("add", ".")
         commit("-m", "hello world")
diff --git a/tests/test_vcs.py b/tests/test_vcs.py
index 2e8f256..5eba8d8 100644
--- a/tests/test_vcs.py
+++ b/tests/test_vcs.py
@@ -7,12 +7,13 @@ import pytest
 import yaml
 from packaging.version import Version
 from plumbum import local
-from plumbum.cmd import git
 
 from copier import Worker, run_copy, run_update
 from copier.errors import ShallowCloneWarning
 from copier.vcs import checkout_latest_tag, clone, get_git_version, get_repo
 
+from .helpers import git
+
 
 def test_get_repo() -> None:
     get = get_repo