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.
Files changed (48) hide show
  1. gitingest/__init__.py +5 -0
  2. gitingest/__main__.py +204 -0
  3. gitingest/clone.py +176 -0
  4. gitingest/config.py +14 -0
  5. gitingest/entrypoint.py +352 -0
  6. gitingest/ingestion.py +325 -0
  7. gitingest/output_formatter.py +243 -0
  8. gitingest/query_parser.py +190 -0
  9. gitingest/schemas/__init__.py +7 -0
  10. gitingest/schemas/cloning.py +42 -0
  11. gitingest/schemas/filesystem.py +161 -0
  12. gitingest/schemas/ingestion.py +101 -0
  13. gitingest/utils/__init__.py +1 -0
  14. gitingest/utils/auth.py +27 -0
  15. gitingest/utils/compat_func.py +44 -0
  16. gitingest/utils/compat_typing.py +18 -0
  17. gitingest/utils/exceptions.py +27 -0
  18. gitingest/utils/file_utils.py +77 -0
  19. gitingest/utils/git_utils.py +531 -0
  20. gitingest/utils/ignore_patterns.py +240 -0
  21. gitingest/utils/ingestion_utils.py +88 -0
  22. gitingest/utils/logging_config.py +200 -0
  23. gitingest/utils/notebook.py +159 -0
  24. gitingest/utils/os_utils.py +24 -0
  25. gitingest/utils/pattern_utils.py +73 -0
  26. gitingest/utils/query_parser_utils.py +243 -0
  27. gitingest/utils/timeout_wrapper.py +46 -0
  28. gitingest_tddschn-0.3.1.dist-info/METADATA +422 -0
  29. gitingest_tddschn-0.3.1.dist-info/RECORD +48 -0
  30. gitingest_tddschn-0.3.1.dist-info/WHEEL +5 -0
  31. gitingest_tddschn-0.3.1.dist-info/entry_points.txt +2 -0
  32. gitingest_tddschn-0.3.1.dist-info/licenses/LICENSE +21 -0
  33. gitingest_tddschn-0.3.1.dist-info/top_level.txt +2 -0
  34. server/__init__.py +1 -0
  35. server/__main__.py +32 -0
  36. server/form_types.py +16 -0
  37. server/main.py +213 -0
  38. server/metrics_server.py +68 -0
  39. server/models.py +198 -0
  40. server/query_processor.py +429 -0
  41. server/routers/__init__.py +7 -0
  42. server/routers/dynamic.py +39 -0
  43. server/routers/index.py +37 -0
  44. server/routers/ingest.py +153 -0
  45. server/routers_utils.py +57 -0
  46. server/s3_utils.py +561 -0
  47. server/server_config.py +56 -0
  48. server/server_utils.py +73 -0
gitingest/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Gitingest: A package for ingesting data from Git repositories."""
2
+
3
+ from gitingest.entrypoint import ingest, ingest_async
4
+
5
+ __all__ = ["ingest", "ingest_async"]
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"