gitingest-tddschn 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitingest/__init__.py +5 -0
- gitingest/__main__.py +204 -0
- gitingest/clone.py +176 -0
- gitingest/config.py +14 -0
- gitingest/entrypoint.py +352 -0
- gitingest/ingestion.py +325 -0
- gitingest/output_formatter.py +243 -0
- gitingest/query_parser.py +190 -0
- gitingest/schemas/__init__.py +7 -0
- gitingest/schemas/cloning.py +42 -0
- gitingest/schemas/filesystem.py +161 -0
- gitingest/schemas/ingestion.py +101 -0
- gitingest/utils/__init__.py +1 -0
- gitingest/utils/auth.py +27 -0
- gitingest/utils/compat_func.py +44 -0
- gitingest/utils/compat_typing.py +18 -0
- gitingest/utils/exceptions.py +27 -0
- gitingest/utils/file_utils.py +77 -0
- gitingest/utils/git_utils.py +531 -0
- gitingest/utils/ignore_patterns.py +240 -0
- gitingest/utils/ingestion_utils.py +88 -0
- gitingest/utils/logging_config.py +200 -0
- gitingest/utils/notebook.py +159 -0
- gitingest/utils/os_utils.py +24 -0
- gitingest/utils/pattern_utils.py +73 -0
- gitingest/utils/query_parser_utils.py +243 -0
- gitingest/utils/timeout_wrapper.py +46 -0
- gitingest_tddschn-0.3.1.dist-info/METADATA +422 -0
- gitingest_tddschn-0.3.1.dist-info/RECORD +48 -0
- gitingest_tddschn-0.3.1.dist-info/WHEEL +5 -0
- gitingest_tddschn-0.3.1.dist-info/entry_points.txt +2 -0
- gitingest_tddschn-0.3.1.dist-info/licenses/LICENSE +21 -0
- gitingest_tddschn-0.3.1.dist-info/top_level.txt +2 -0
- server/__init__.py +1 -0
- server/__main__.py +32 -0
- server/form_types.py +16 -0
- server/main.py +213 -0
- server/metrics_server.py +68 -0
- server/models.py +198 -0
- server/query_processor.py +429 -0
- server/routers/__init__.py +7 -0
- server/routers/dynamic.py +39 -0
- server/routers/index.py +37 -0
- server/routers/ingest.py +153 -0
- server/routers_utils.py +57 -0
- server/s3_utils.py +561 -0
- server/server_config.py +56 -0
- server/server_utils.py +73 -0
gitingest/__init__.py
ADDED
gitingest/__main__.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Command-line interface (CLI) for Gitingest."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=no-value-for-parameter
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import TypedDict
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from typing_extensions import Unpack
|
|
11
|
+
|
|
12
|
+
from gitingest.config import MAX_FILE_SIZE, OUTPUT_FILE_NAME
|
|
13
|
+
from gitingest.entrypoint import ingest_async
|
|
14
|
+
|
|
15
|
+
# Import logging configuration first to intercept all logging
|
|
16
|
+
from gitingest.utils.logging_config import get_logger
|
|
17
|
+
|
|
18
|
+
# Initialize logger for this module
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CLIArgs(TypedDict):
|
|
23
|
+
source: str
|
|
24
|
+
max_size: int
|
|
25
|
+
exclude_pattern: tuple[str, ...]
|
|
26
|
+
include_pattern: tuple[str, ...]
|
|
27
|
+
branch: str | None
|
|
28
|
+
include_gitignored: bool
|
|
29
|
+
include_submodules: bool
|
|
30
|
+
token: str | None
|
|
31
|
+
output: str | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.argument("source", type=str, default=".")
|
|
36
|
+
@click.option(
|
|
37
|
+
"--max-size",
|
|
38
|
+
"-s",
|
|
39
|
+
default=MAX_FILE_SIZE,
|
|
40
|
+
show_default=True,
|
|
41
|
+
help="Maximum file size to process in bytes",
|
|
42
|
+
)
|
|
43
|
+
@click.option("--exclude-pattern", "-e", multiple=True, help="Shell-style patterns to exclude.")
|
|
44
|
+
@click.option(
|
|
45
|
+
"--include-pattern",
|
|
46
|
+
"-i",
|
|
47
|
+
multiple=True,
|
|
48
|
+
help="Shell-style patterns to include.",
|
|
49
|
+
)
|
|
50
|
+
@click.option("--branch", "-b", default=None, help="Branch to clone and ingest")
|
|
51
|
+
@click.option(
|
|
52
|
+
"--include-gitignored",
|
|
53
|
+
is_flag=True,
|
|
54
|
+
default=False,
|
|
55
|
+
help="Include files matched by .gitignore and .gitingestignore",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--include-submodules",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
help="Include repository's submodules in the analysis",
|
|
61
|
+
default=False,
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--token",
|
|
65
|
+
"-t",
|
|
66
|
+
envvar="GITHUB_TOKEN",
|
|
67
|
+
default=None,
|
|
68
|
+
help=(
|
|
69
|
+
"GitHub personal access token (PAT) for accessing private repositories. "
|
|
70
|
+
"If omitted, the CLI will look for the GITHUB_TOKEN environment variable."
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--output",
|
|
75
|
+
"-o",
|
|
76
|
+
default=None,
|
|
77
|
+
help="Output file path (default: digest.txt in current directory). Use '-' for stdout.",
|
|
78
|
+
)
|
|
79
|
+
def main(**cli_kwargs: Unpack[_CLIArgs]) -> None:
|
|
80
|
+
"""Run the CLI entry point to analyze a repo / directory and dump its contents.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
**cli_kwargs : Unpack[_CLIArgs]
|
|
85
|
+
A dictionary of keyword arguments forwarded to ``ingest_async``.
|
|
86
|
+
|
|
87
|
+
Notes
|
|
88
|
+
-----
|
|
89
|
+
See ``ingest_async`` for a detailed description of each argument.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
Basic usage:
|
|
94
|
+
$ gitingest
|
|
95
|
+
$ gitingest /path/to/repo
|
|
96
|
+
$ gitingest https://github.com/user/repo
|
|
97
|
+
|
|
98
|
+
Output to stdout:
|
|
99
|
+
$ gitingest -o -
|
|
100
|
+
$ gitingest https://github.com/user/repo --output -
|
|
101
|
+
|
|
102
|
+
With filtering:
|
|
103
|
+
$ gitingest -i "*.py" -e "*.log"
|
|
104
|
+
$ gitingest --include-pattern "*.js" --exclude-pattern "node_modules/*"
|
|
105
|
+
|
|
106
|
+
Private repositories:
|
|
107
|
+
$ gitingest https://github.com/user/private-repo -t ghp_token
|
|
108
|
+
$ GITHUB_TOKEN=ghp_token gitingest https://github.com/user/private-repo
|
|
109
|
+
|
|
110
|
+
Include submodules:
|
|
111
|
+
$ gitingest https://github.com/user/repo --include-submodules
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
asyncio.run(_async_main(**cli_kwargs))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def _async_main(
|
|
118
|
+
source: str,
|
|
119
|
+
*,
|
|
120
|
+
max_size: int = MAX_FILE_SIZE,
|
|
121
|
+
exclude_pattern: tuple[str, ...] | None = None,
|
|
122
|
+
include_pattern: tuple[str, ...] | None = None,
|
|
123
|
+
branch: str | None = None,
|
|
124
|
+
include_gitignored: bool = False,
|
|
125
|
+
include_submodules: bool = False,
|
|
126
|
+
token: str | None = None,
|
|
127
|
+
output: str | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Analyze a directory or repository and create a text dump of its contents.
|
|
130
|
+
|
|
131
|
+
This command scans the specified ``source`` (a local directory or Git repo),
|
|
132
|
+
applies custom include and exclude patterns, and generates a text summary of
|
|
133
|
+
the analysis. The summary is written to an output file or printed to ``stdout``.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
source : str
|
|
138
|
+
A directory path or a Git repository URL.
|
|
139
|
+
max_size : int
|
|
140
|
+
Maximum file size in bytes to ingest (default: 10 MB).
|
|
141
|
+
exclude_pattern : tuple[str, ...] | None
|
|
142
|
+
Glob patterns for pruning the file set.
|
|
143
|
+
include_pattern : tuple[str, ...] | None
|
|
144
|
+
Glob patterns for including files in the output.
|
|
145
|
+
branch : str | None
|
|
146
|
+
Git branch to ingest. If ``None``, the repository's default branch is used.
|
|
147
|
+
include_gitignored : bool
|
|
148
|
+
If ``True``, also ingest files matched by ``.gitignore`` or ``.gitingestignore`` (default: ``False``).
|
|
149
|
+
include_submodules : bool
|
|
150
|
+
If ``True``, recursively include all Git submodules within the repository (default: ``False``).
|
|
151
|
+
token : str | None
|
|
152
|
+
GitHub personal access token (PAT) for accessing private repositories.
|
|
153
|
+
Can also be set via the ``GITHUB_TOKEN`` environment variable.
|
|
154
|
+
output : str | None
|
|
155
|
+
The path where the output file will be written (default: ``digest.txt`` in current directory).
|
|
156
|
+
Use ``"-"`` to write to ``stdout``.
|
|
157
|
+
|
|
158
|
+
Raises
|
|
159
|
+
------
|
|
160
|
+
click.Abort
|
|
161
|
+
Raised if an error occurs during execution and the command must be aborted.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
# Normalise pattern containers (the ingest layer expects sets)
|
|
166
|
+
exclude_patterns = set(exclude_pattern) if exclude_pattern else set()
|
|
167
|
+
include_patterns = set(include_pattern) if include_pattern else set()
|
|
168
|
+
|
|
169
|
+
output_target = output if output is not None else OUTPUT_FILE_NAME
|
|
170
|
+
|
|
171
|
+
if output_target == "-":
|
|
172
|
+
click.echo("Analyzing source, preparing output for stdout...", err=True)
|
|
173
|
+
else:
|
|
174
|
+
click.echo(f"Analyzing source, output will be written to '{output_target}'...", err=True)
|
|
175
|
+
|
|
176
|
+
summary, _, _ = await ingest_async(
|
|
177
|
+
source,
|
|
178
|
+
max_file_size=max_size,
|
|
179
|
+
include_patterns=include_patterns,
|
|
180
|
+
exclude_patterns=exclude_patterns,
|
|
181
|
+
branch=branch,
|
|
182
|
+
include_gitignored=include_gitignored,
|
|
183
|
+
include_submodules=include_submodules,
|
|
184
|
+
token=token,
|
|
185
|
+
output=output_target,
|
|
186
|
+
)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
# Convert any exception into Click.Abort so that exit status is non-zero
|
|
189
|
+
click.echo(f"Error: {exc}", err=True)
|
|
190
|
+
raise click.Abort from exc
|
|
191
|
+
|
|
192
|
+
if output_target == "-": # stdout
|
|
193
|
+
click.echo("\n--- Summary ---", err=True)
|
|
194
|
+
click.echo(summary, err=True)
|
|
195
|
+
click.echo("--- End Summary ---", err=True)
|
|
196
|
+
click.echo("Analysis complete! Output sent to stdout.", err=True)
|
|
197
|
+
else: # file
|
|
198
|
+
click.echo(f"Analysis complete! Output written to: {output_target}")
|
|
199
|
+
click.echo("\nSummary:")
|
|
200
|
+
click.echo(summary)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
gitingest/clone.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Module containing functions for cloning a Git repository to a local path."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import git
|
|
9
|
+
|
|
10
|
+
from gitingest.config import DEFAULT_TIMEOUT
|
|
11
|
+
from gitingest.utils.git_utils import (
|
|
12
|
+
check_repo_exists,
|
|
13
|
+
checkout_partial_clone,
|
|
14
|
+
create_git_repo,
|
|
15
|
+
ensure_git_installed,
|
|
16
|
+
git_auth_context,
|
|
17
|
+
is_github_host,
|
|
18
|
+
resolve_commit,
|
|
19
|
+
)
|
|
20
|
+
from gitingest.utils.logging_config import get_logger
|
|
21
|
+
from gitingest.utils.os_utils import ensure_directory_exists_or_create
|
|
22
|
+
from gitingest.utils.timeout_wrapper import async_timeout
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from gitingest.schemas import CloneConfig
|
|
26
|
+
|
|
27
|
+
# Initialize logger for this module
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@async_timeout(DEFAULT_TIMEOUT)
|
|
32
|
+
async def clone_repo(config: CloneConfig, *, token: str | None = None) -> None:
|
|
33
|
+
"""Clone a repository to a local path based on the provided configuration.
|
|
34
|
+
|
|
35
|
+
This function handles the process of cloning a Git repository to the local file system.
|
|
36
|
+
It can clone a specific branch, tag, or commit if provided, and it raises exceptions if
|
|
37
|
+
any errors occur during the cloning process.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
config : CloneConfig
|
|
42
|
+
The configuration for cloning the repository.
|
|
43
|
+
token : str | None
|
|
44
|
+
GitHub personal access token (PAT) for accessing private repositories.
|
|
45
|
+
|
|
46
|
+
Raises
|
|
47
|
+
------
|
|
48
|
+
ValueError
|
|
49
|
+
If the repository is not found, if the provided URL is invalid, or if the token format is invalid.
|
|
50
|
+
RuntimeError
|
|
51
|
+
If Git operations fail during the cloning process.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
# Extract and validate query parameters
|
|
55
|
+
url: str = config.url
|
|
56
|
+
local_path: str = config.local_path
|
|
57
|
+
partial_clone: bool = config.subpath != "/"
|
|
58
|
+
|
|
59
|
+
logger.info(
|
|
60
|
+
"Starting git clone operation",
|
|
61
|
+
extra={
|
|
62
|
+
"url": url,
|
|
63
|
+
"local_path": local_path,
|
|
64
|
+
"partial_clone": partial_clone,
|
|
65
|
+
"subpath": config.subpath,
|
|
66
|
+
"branch": config.branch,
|
|
67
|
+
"tag": config.tag,
|
|
68
|
+
"commit": config.commit,
|
|
69
|
+
"include_submodules": config.include_submodules,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
logger.debug("Ensuring git is installed")
|
|
74
|
+
await ensure_git_installed()
|
|
75
|
+
|
|
76
|
+
logger.debug("Creating local directory", extra={"parent_path": str(Path(local_path).parent)})
|
|
77
|
+
await ensure_directory_exists_or_create(Path(local_path).parent)
|
|
78
|
+
|
|
79
|
+
logger.debug("Checking if repository exists", extra={"url": url})
|
|
80
|
+
if not await check_repo_exists(url, token=token):
|
|
81
|
+
logger.error("Repository not found", extra={"url": url})
|
|
82
|
+
msg = "Repository not found. Make sure it is public or that you have provided a valid token."
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
|
|
85
|
+
logger.debug("Resolving commit reference")
|
|
86
|
+
commit = await resolve_commit(config, token=token)
|
|
87
|
+
logger.debug("Resolved commit", extra={"commit": commit})
|
|
88
|
+
|
|
89
|
+
# Clone the repository using GitPython with proper authentication
|
|
90
|
+
logger.info("Executing git clone operation", extra={"url": "<redacted>", "local_path": local_path})
|
|
91
|
+
try:
|
|
92
|
+
clone_kwargs = {
|
|
93
|
+
"single_branch": True,
|
|
94
|
+
"no_checkout": True,
|
|
95
|
+
"depth": 1,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
with git_auth_context(url, token) as (git_cmd, auth_url):
|
|
99
|
+
if partial_clone:
|
|
100
|
+
# For partial clones, use git.Git() with filter and sparse options
|
|
101
|
+
cmd_args = ["--single-branch", "--no-checkout", "--depth=1"]
|
|
102
|
+
cmd_args.extend(["--filter=blob:none", "--sparse"])
|
|
103
|
+
cmd_args.extend([auth_url, local_path])
|
|
104
|
+
git_cmd.clone(*cmd_args)
|
|
105
|
+
elif token and is_github_host(url):
|
|
106
|
+
# For authenticated GitHub repos, use git_cmd with auth URL
|
|
107
|
+
cmd_args = ["--single-branch", "--no-checkout", "--depth=1", auth_url, local_path]
|
|
108
|
+
git_cmd.clone(*cmd_args)
|
|
109
|
+
else:
|
|
110
|
+
# For non-authenticated repos, use the standard GitPython method
|
|
111
|
+
git.Repo.clone_from(url, local_path, **clone_kwargs)
|
|
112
|
+
|
|
113
|
+
logger.info("Git clone completed successfully")
|
|
114
|
+
except git.GitCommandError as exc:
|
|
115
|
+
msg = f"Git clone failed: {exc}"
|
|
116
|
+
raise RuntimeError(msg) from exc
|
|
117
|
+
|
|
118
|
+
# Checkout the subpath if it is a partial clone
|
|
119
|
+
if partial_clone:
|
|
120
|
+
logger.info("Setting up partial clone for subpath", extra={"subpath": config.subpath})
|
|
121
|
+
await checkout_partial_clone(config, token=token)
|
|
122
|
+
logger.debug("Partial clone setup completed")
|
|
123
|
+
|
|
124
|
+
# Perform post-clone operations
|
|
125
|
+
await _perform_post_clone_operations(config, local_path, url, token, commit)
|
|
126
|
+
|
|
127
|
+
logger.info("Git clone operation completed successfully", extra={"local_path": local_path})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def _perform_post_clone_operations(
|
|
131
|
+
config: CloneConfig,
|
|
132
|
+
local_path: str,
|
|
133
|
+
url: str,
|
|
134
|
+
token: str | None,
|
|
135
|
+
commit: str,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Perform post-clone operations like fetching, checkout, and submodule updates.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
config : CloneConfig
|
|
142
|
+
The configuration for cloning the repository.
|
|
143
|
+
local_path : str
|
|
144
|
+
The local path where the repository was cloned.
|
|
145
|
+
url : str
|
|
146
|
+
The repository URL.
|
|
147
|
+
token : str | None
|
|
148
|
+
GitHub personal access token (PAT) for accessing private repositories.
|
|
149
|
+
commit : str
|
|
150
|
+
The commit SHA to checkout.
|
|
151
|
+
|
|
152
|
+
Raises
|
|
153
|
+
------
|
|
154
|
+
RuntimeError
|
|
155
|
+
If any Git operation fails.
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
repo = create_git_repo(local_path, url, token)
|
|
160
|
+
|
|
161
|
+
# Ensure the commit is locally available
|
|
162
|
+
logger.debug("Fetching specific commit", extra={"commit": commit})
|
|
163
|
+
repo.git.fetch("--depth=1", "origin", commit)
|
|
164
|
+
|
|
165
|
+
# Write the work-tree at that commit
|
|
166
|
+
logger.info("Checking out commit", extra={"commit": commit})
|
|
167
|
+
repo.git.checkout(commit)
|
|
168
|
+
|
|
169
|
+
# Update submodules
|
|
170
|
+
if config.include_submodules:
|
|
171
|
+
logger.info("Updating submodules")
|
|
172
|
+
repo.git.submodule("update", "--init", "--recursive", "--depth=1")
|
|
173
|
+
logger.debug("Submodules updated successfully")
|
|
174
|
+
except git.GitCommandError as exc:
|
|
175
|
+
msg = f"Git operation failed: {exc}"
|
|
176
|
+
raise RuntimeError(msg) from exc
|
gitingest/config.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Configuration file for the project."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # Maximum size of a single file to process (10 MB)
|
|
7
|
+
MAX_DIRECTORY_DEPTH = 20 # Maximum depth of directory traversal
|
|
8
|
+
MAX_FILES = 10_000 # Maximum number of files to process
|
|
9
|
+
MAX_TOTAL_SIZE_BYTES = 500 * 1024 * 1024 # Maximum size of output file (500 MB)
|
|
10
|
+
DEFAULT_TIMEOUT = 60 # seconds
|
|
11
|
+
|
|
12
|
+
OUTPUT_FILE_NAME = "digest.txt"
|
|
13
|
+
|
|
14
|
+
TMP_BASE_PATH = Path(tempfile.gettempdir()) / "gitingest"
|