-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpackfiles.py
150 lines (116 loc) · 3.62 KB
/
packfiles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "ignorelib",
# "python-magic",
# ]
# ///
"""
Packfiles allows merging multiple files into a single Markdown file.
"""
import argparse
import io
import re
import sys
from pathlib import Path
from shutil import copyfileobj
from subprocess import CalledProcessError, check_output
from typing import TextIO
from ignorelib import IgnoreFilterManager
from magic import Magic
__author__ = "EcmaXp"
__version__ = "0.1.0"
__license__ = "MIT"
__url__ = "https://pypi.org/project/packfiles/"
__all__ = []
CODE_MIME_TYPES_PATTERN = re.compile(r"^(text/.*|application/json|.*\+xml)$")
GIT_IGNORE_FILEPATHS: list[Path] = [
# get_git_global_ignore_file_paths():
# - `.git/info/exclude` (git repo)
# - `~/.gitignore` (git user)
]
GIT_IGNORE_PATTERNS = [
".git",
"*.lock",
]
GIT_IGNORE_FILENAME = ".gitignore"
magic = Magic(mime=True)
def packfiles(
path: Path,
*,
ignore_filter: IgnoreFilterManager | None = None,
) -> None:
if path.is_dir():
files = sorted(path.rglob("*"))
folder = path
else:
files = [path]
folder = path.parent
folder = get_git_root(path) or path
if ignore_filter is None:
ignore_filter = get_ignore_filter(folder)
for file in files:
if not file.is_file():
continue
if ignore_filter.is_ignored(str(file)):
continue
if not is_source_code_file(file):
continue
packfile(file, folder)
def packfile(file: Path, folder: Path):
print(f"```{file.relative_to(folder)}")
with open(file, "r", encoding="utf-8") as f:
copyfileobj(f, sys.stdout)
if not is_ends_with_newline(f):
print()
print("```")
print()
def is_ends_with_newline(fp: TextIO) -> bool:
pos = fp.tell()
try:
fp.seek(fp.seek(0, io.SEEK_END) - 1)
return fp.read(1) == "\n"
finally:
fp.seek(pos)
def get_git_root(path: Path) -> Path | None:
if (path / ".git").is_dir():
return path
if path == path.parent:
return None
return get_git_root(path.parent)
def get_git_core_excludesfile() -> Path | None:
try:
global_ignore_file = check_output(
["git", "config", "core.excludesfile"],
encoding="utf-8",
)
except CalledProcessError:
return None
return Path(global_ignore_file).expanduser()
def get_ignore_filter(path: Path) -> IgnoreFilterManager:
return IgnoreFilterManager.build(
str(path),
global_ignore_file_paths=list(map(str, get_git_global_ignore_file_paths(path))),
global_patterns=GIT_IGNORE_PATTERNS,
ignore_file_name=GIT_IGNORE_FILENAME,
)
def get_git_global_ignore_file_paths(path: Path) -> list[Path]:
global_ignore_file_paths = GIT_IGNORE_FILEPATHS.copy()
if git_root := get_git_root(path):
global_ignore_file_paths.append(git_root / ".git" / "info" / "exclude")
if git_core_excludesfile := get_git_core_excludesfile():
global_ignore_file_paths.append(git_core_excludesfile)
return global_ignore_file_paths
def is_source_code_file(path: Path) -> bool:
mime_type = magic.from_file(path)
return CODE_MIME_TYPES_PATTERN.match(mime_type) is not None
parser = argparse.ArgumentParser(description=(__doc__ or "").strip())
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument("path", type=Path, nargs="*", default=[Path(".")])
def main() -> None:
args = parser.parse_args()
for path in args.path:
packfiles(path)
if __name__ == "__main__":
main()