Skip to content

Conversation

@tnijboer
Copy link

Fixes #2244.

Problem

Running copier update fails with OSError: [Errno 7] Argument list too long on projects with extensive .gitignore files or _skip_if_exists lists.

The cause is that copier builds the git apply command by adding an individual --exclude={pattern} argument for every exclusion. This exceeds the OS command-line argument length limit.

Solution

This patch refactors the exclusion handling for git apply:

  1. All exclusion patterns (from .gitignore, all_skip_if_exists, answers_relpath) are consolidated into a single list.
  2. This list is written to a temporary file.
  3. The git apply command is modified to use a single --exclude-from=<temp_file> argument, pointing to the new file.
  4. A try...finally block is used to guarantee the temporary file is cleaned up.

This change avoids the argument length limit and allows copier update to succeed on projects with many exclusions.

Copy link
Member

@sisp sisp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing this fix, @tnijboer! 🙏

It looks like the right approach to fixing the problem. I left two inline comments.

@sisp
Copy link
Member

sisp commented Oct 29, 2025

Several tests are failing; it seems that git apply does not have an --exclude-from flag. 🤔

@tnijboer
Copy link
Author

Yes, you're totally right. My bad, was confused with rsync. I updated the code to just check the .gitignore for folders, and if there are any files listed in git status --ignored --porcelain that are in an ignored folder just add the exclude for the folder instead so the list of excludes get way shorter.

@tnijboer tnijboer changed the title fix: use exclude-from file instead of exclude argument fix: use .gitignore for exludes Oct 30, 2025
@tnijboer
Copy link
Author

tnijboer commented Nov 4, 2025

@sisp what do you think of this approach?

assert (dst / "skip-if-exists.txt").read_text() == "baz"


def test_exclude_with_gitignore(tmp_path_factory: pytest.TempPathFactory) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes even without the changes in the update algorithm, so it doesn't seem to cover the real scenario under test.

This test looks way too complex for the scenario under test. Could you please scope it down to the scenario where too many exclude patterns cause the OSError: [Errno 7] Argument list too long error?

Thanks for your attention.
"""
),
(repo / ".gitignore.jinja"): ".venv/\n**/.cache/\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this added file serves any purpose in this test case.

Suggested change
(repo / ".gitignore.jinja"): ".venv/\n**/.cache/\n",

Comment on lines +1309 to +1345
gitignore_path = Path(subproject_top, ".gitignore")
dir_patterns = set()
if gitignore_path.exists():
with gitignore_path.open() as gitignore_file:
for line in gitignore_file:
line = line.strip()
if not line or line.startswith("#"):
continue
# Matches foo/, foo/*, foo/**/, **/test/, **/test/*, etc.
if (
line.endswith("/")
or line.endswith("/*")
or line.endswith("/**/")
or re.match(r".*\*\*/.*", line)
):
cleaned = re.sub(r"(\/\*|\/\*\*\/|\/$)", "", line)
dir_patterns.add(cleaned)
# Single loop: process ignored files and build exclude lists
ignored_dirs = set()
extra_exclude = []
for filename in ignored_files.splitlines():
if filename.startswith("!! "):
fname = filename.split("!! ").pop()
matched_dir = False
for p in dir_patterns:
if fname == p or fname.startswith(p + "/"):
if fname.endswith("/"):
ignored_dirs.add(fname.rstrip("/"))
matched_dir = True
break
if not matched_dir:
extra_exclude.append(fname)
for dir_pattern in ignored_dirs:
apply_cmd = apply_cmd["--exclude", dir_pattern + "/*"]
for skip_pattern in map(self._render_string, self.all_skip_if_exists):
apply_cmd = apply_cmd["--exclude", skip_pattern]
for skip_pattern in extra_exclude:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, this looks quite complex for a problem, I'm a bit worried that we're missing some edge cases.

I haven't looked into it in detail, but I think there are two ways to exclude files in the update:

  1. Exclude files when applying the patch via git apply --exclude, which has the limitation you've encountered.
  2. Omit the files to be excluded from the patch.

Option 2 sounds potentially simpler and more robust. WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue with too many files in (ignored) .venv

2 participants