X Tutup
Skip to content

feat(extensions): support .extensionignore to exclude files during install#1781

Open
Rubiss wants to merge 2 commits intogithub:mainfrom
Rubiss:feat/extensionignore-support
Open

feat(extensions): support .extensionignore to exclude files during install#1781
Rubiss wants to merge 2 commits intogithub:mainfrom
Rubiss:feat/extensionignore-support

Conversation

@Rubiss
Copy link
Contributor

@Rubiss Rubiss commented Mar 9, 2026

Summary

Adds .extensionignore support so extension authors can exclude files and folders from being copied when users run specify extension add.

Problem

Extension authors often have development-only files (tests, CI configs, documentation source, build artifacts) that shouldn't be copied into the user's project when installing an extension. There was no mechanism to exclude these files.

Solution

Extension authors can now create a .extensionignore file in their extension root. The file uses .gitignore-compatible patterns (one per line), powered by the pathspec library:

# .extensionignore

# Development files
tests/
.github/
.gitignore

# Build artifacts
__pycache__/
*.pyc

Features

  • .gitignore-compatible pattern matching via pathspec.GitIgnoreSpec
  • * matches anything except /; ** matches zero or more directories
  • ? matches any single character except /
  • Trailing / on directory patterns restricts matching to directories only
  • Patterns containing / (other than trailing) are anchored to the extension root
  • ! negates a previously excluded pattern (re-includes a file)
  • Comment lines (#) and blank lines are ignored
  • Backslash patterns are normalised to forward slashes for cross-platform compatibility
  • The .extensionignore file itself is always excluded from the copy

Changes

  • src/specify_cli/extensions.py: Added _load_extensionignore() static method and integrated it into install_from_directory() via shutil.copytree(ignore=...)
  • extensions/EXTENSION-DEVELOPMENT-GUIDE.md: New documentation section covering .extensionignore format, example patterns, and a pattern-matching reference table
  • tests/test_extensions.py: 13 new tests in TestExtensionIgnore covering all pattern matching scenarios
  • pyproject.toml: Added pathspec>=0.12.0 dependency
  • CHANGELOG.md: Added [Unreleased] entry for this feature

Testing

All 152 passing tests continue to pass (4 pre-existing failures in test_cursor_frontmatter.py are unrelated — bash/WSL not available on Windows). New tests cover:

  • No .extensionignore → all files copied (baseline)
  • Directory exclusion (tests/, .github/)
  • Glob patterns (*.pyc) across nested directories
  • Comments and blank lines ignored
  • .extensionignore itself never copied
  • Relative path pattern matching
  • .. traversal patterns are a no-op (security)
  • Absolute path patterns are a no-op (security)
  • Empty .extensionignore file
  • Windows backslash patterns normalised correctly
  • * does not cross directory boundaries
  • ** crosses directory boundaries
  • ! negation re-includes previously excluded files

AI Disclosure

This PR was authored with GitHub Copilot assistance.

@Rubiss Rubiss requested a review from mnriem as a code owner March 9, 2026 04:45
Copilot AI review requested due to automatic review settings March 9, 2026 04:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds .extensionignore support to the extension installation pipeline, letting extension authors exclude development-only files (tests, CI configs, docs source, build artifacts) from being copied when users run specify extension add. The feature is analogous to .gitignore and is integrated into ExtensionManager.install_from_directory() via a new _load_extensionignore() static method.

Changes:

  • Added _load_extensionignore() static method to ExtensionManager and wired it into install_from_directory() via shutil.copytree(ignore=…)
  • Added 6 new test cases in TestExtensionIgnore covering all documented scenarios
  • Added documentation section in EXTENSION-DEVELOPMENT-GUIDE.md and bumped version to 0.1.14

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/specify_cli/extensions.py New _load_extensionignore() static method; integrates ignore function into install_from_directory(); adds Callable/Set and fnmatch imports
tests/test_extensions.py 6 new tests in TestExtensionIgnore class covering all pattern-matching scenarios
extensions/EXTENSION-DEVELOPMENT-GUIDE.md New "Excluding Files with .extensionignore" section with format description, example, and pattern table
CHANGELOG.md New 0.1.14 entry documenting the feature
pyproject.toml Version bump from 0.1.13 to 0.1.14

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Rubiss Rubiss force-pushed the feat/extensionignore-support branch from 0a00c97 to 5245079 Compare March 9, 2026 04:52
Copilot AI review requested due to automatic review settings March 9, 2026 13:52
@Rubiss Rubiss force-pushed the feat/extensionignore-support branch from 5245079 to 868d608 Compare March 9, 2026 13:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mnriem
mnriem previously approved these changes Mar 9, 2026
Copy link
Collaborator

@mnriem mnriem left a comment

Choose a reason for hiding this comment

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

I will approve, but consider the things below

  1. fnmatch.gitignore semantics (documentation bug)
    The docs and PR description claim "similar to .gitignore syntax," but fnmatch.fnmatch() treats * as matching any character including path separators. In .gitignore, * does not cross directory boundaries. The documentation table says docs/*.draft.md matches "Draft markdown files inside docs/", but fnmatch would also match docs/sub/api.draft.md. This needs to either:

    • Switch to pathlib.PurePath.match() or fnmatch with per-segment matching to get proper gitignore-like behavior, or
    • Remove all .gitignore comparisons from the docs and clearly document that * matches across directories.
  2. No support for negation (!) or ** patterns
    Not a blocker since these aren't claimed, but the .gitignore analogy sets expectations. At minimum, the docs should state what's not supported.

  3. Missing edge-case tests:

    • Pattern with .. (e.g., ../sibling/) — should be a no-op or explicitly rejected
    • Absolute path patterns (e.g., /etc/passwd) — verify they don't match anything
    • Empty .extensionignore file (no patterns, just the file itself)
    • Patterns with Windows-style backslashes
  4. Minor: forward-slash normalization is one-directional
    The code does rel_path_fwd = rel_path.replace("\\", "/") but doesn't normalize the patterns themselves. A user on Windows writing docs\internal\draft.md in their .extensionignore would get inconsistent behavior.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Rubiss
Copy link
Contributor Author

Rubiss commented Mar 9, 2026

I will approve, but consider the things below

  1. fnmatch.gitignore semantics (documentation bug)
    The docs and PR description claim "similar to .gitignore syntax," but fnmatch.fnmatch() treats * as matching any character including path separators. In .gitignore, * does not cross directory boundaries. The documentation table says docs/*.draft.md matches "Draft markdown files inside docs/", but fnmatch would also match docs/sub/api.draft.md. This needs to either:

    • Switch to pathlib.PurePath.match() or fnmatch with per-segment matching to get proper gitignore-like behavior, or
    • Remove all .gitignore comparisons from the docs and clearly document that * matches across directories.
  2. No support for negation (!) or ** patterns
    Not a blocker since these aren't claimed, but the .gitignore analogy sets expectations. At minimum, the docs should state what's not supported.

  3. Missing edge-case tests:

    • Pattern with .. (e.g., ../sibling/) — should be a no-op or explicitly rejected
    • Absolute path patterns (e.g., /etc/passwd) — verify they don't match anything
    • Empty .extensionignore file (no patterns, just the file itself)
    • Patterns with Windows-style backslashes
  4. Minor: forward-slash normalization is one-directional
    The code does rel_path_fwd = rel_path.replace("\\", "/") but doesn't normalize the patterns themselves. A user on Windows writing docs\internal\draft.md in their .extensionignore would get inconsistent behavior.

No need to approve yet, these seem like all valid concerns that I will address. Thanks for the feedback. Personally, I think it should work consistently like .gitignore does since generally people are very familiar with the concept.

…stall

Add .extensionignore support so extension authors can exclude files and
folders from being copied when users run 'specify extension add'.

The file uses glob-style patterns (one per line), supports comments (#),
blank lines, trailing-slash directory patterns, and relative path matching.
The .extensionignore file itself is always excluded from the copy.

- Add _load_extensionignore() to ExtensionManager
- Integrate ignore function into shutil.copytree in install_from_directory
- Document .extensionignore in EXTENSION-DEVELOPMENT-GUIDE.md
- Add 6 tests covering all pattern matching scenarios
- Bump version to 0.1.14
@Rubiss Rubiss force-pushed the feat/extensionignore-support branch from 868d608 to bcc8f30 Compare March 10, 2026 01:22
Rubiss added a commit to Rubiss/spec-kit that referenced this pull request Mar 10, 2026
…re matching

Replace fnmatch with pathspec.GitIgnoreSpec to get proper .gitignore
semantics where * does not cross directory boundaries. This addresses
review feedback on github#1781.

Changes:
- Switch from fnmatch to pathspec>=0.12.0 (GitIgnoreSpec.from_lines)
- Normalize backslashes in patterns for cross-platform compatibility
- Distinguish directories from files for trailing-slash patterns
- Update docs to accurately describe supported pattern semantics
- Add edge-case tests: .., absolute paths, empty file, backslashes,
  * vs ** boundary behavior, and ! negation
- Move changelog entry to [Unreleased] section
Copilot AI review requested due to automatic review settings March 10, 2026 02:02
@Rubiss Rubiss requested a review from mnriem March 10, 2026 02:03
@Rubiss
Copy link
Contributor Author

Rubiss commented Mar 10, 2026

I will approve, but consider the things below

1. **`fnmatch` ≠ `.gitignore` semantics (documentation bug)**
   The docs and PR description claim "similar to `.gitignore` syntax," but `fnmatch.fnmatch()` treats `*` as matching **any** character including path separators. In `.gitignore`, `*` does **not** cross directory boundaries. The documentation table says `docs/*.draft.md` matches "Draft markdown files inside `docs/`", but `fnmatch` would also match `docs/sub/api.draft.md`. This needs to either:
   
   * Switch to `pathlib.PurePath.match()` or `fnmatch` with per-segment matching to get proper gitignore-like behavior, **or**
   * Remove all `.gitignore` comparisons from the docs and clearly document that `*` matches across directories.

2. **No support for negation (`!`) or `**` patterns**
   Not a blocker since these aren't claimed, but the `.gitignore` analogy sets expectations. At minimum, the docs should state what's **not** supported.

3. **Missing edge-case tests:**
   
   * Pattern with `..` (e.g., `../sibling/`) — should be a no-op or explicitly rejected
   * Absolute path patterns (e.g., `/etc/passwd`) — verify they don't match anything
   * Empty `.extensionignore` file (no patterns, just the file itself)
   * Patterns with Windows-style backslashes

4. **Minor: forward-slash normalization is one-directional**
   The code does `rel_path_fwd = rel_path.replace("\\", "/")` but doesn't normalize the patterns themselves. A user on Windows writing `docs\internal\draft.md` in their `.extensionignore` would get inconsistent behavior.

Thanks for the feedback. I have updated .extensionignore implementation to match familiar .gitignore patterns with a callout of what is not supported. Let me know what you think.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…re matching

Replace fnmatch with pathspec.GitIgnoreSpec to get proper .gitignore
semantics where * does not cross directory boundaries. This addresses
review feedback on github#1781.

Changes:
- Switch from fnmatch to pathspec>=0.12.0 (GitIgnoreSpec.from_lines)
- Normalize backslashes in patterns for cross-platform compatibility
- Distinguish directories from files for trailing-slash patterns
- Update docs to accurately describe supported pattern semantics
- Add edge-case tests: .., absolute paths, empty file, backslashes,
  * vs ** boundary behavior, and ! negation
- Move changelog entry to [Unreleased] section
@Rubiss Rubiss force-pushed the feat/extensionignore-support branch from 13c25bc to cdb4a0c Compare March 10, 2026 02:23
@Rubiss Rubiss requested a review from Copilot March 10, 2026 02:24
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

3 participants

X Tutup