Skip to content

Commit c057f6e

Browse files
NicolasIRAGNEiburelCopilot
authored
feat: use gitpython for git stuff (#504)
Co-authored-by: Iwan Burel <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent c9fff75 commit c057f6e

File tree

9 files changed

+455
-394
lines changed

9 files changed

+455
-394
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ repos:
117117
boto3>=1.28.0,
118118
click>=8.0.0,
119119
'fastapi[standard]>=0.109.1',
120+
gitpython>=3.1.0,
120121
httpx,
121122
loguru>=0.7.0,
122123
pathspec>=0.12.1,
@@ -144,6 +145,7 @@ repos:
144145
boto3>=1.28.0,
145146
click>=8.0.0,
146147
'fastapi[standard]>=0.109.1',
148+
gitpython>=3.1.0,
147149
httpx,
148150
loguru>=0.7.0,
149151
pathspec>=0.12.1,

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ COPY src/ ./src/
1313

1414
RUN set -eux; \
1515
pip install --no-cache-dir --upgrade pip; \
16-
pip install --no-cache-dir --timeout 1000 .[server]
16+
pip install --no-cache-dir --timeout 1000 .[server,mcp]
1717

1818
# Stage 2: Runtime image
1919
FROM python:3.13.5-slim@sha256:4c2cf9917bd1cbacc5e9b07320025bdb7cdf2df7b0ceaccb55e9dd7e30987419

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ readme = {file = "README.md", content-type = "text/markdown" }
66
requires-python = ">= 3.8"
77
dependencies = [
88
"click>=8.0.0",
9+
"gitpython>=3.1.0",
910
"httpx",
1011
"loguru>=0.7.0",
1112
"pathspec>=0.12.1",

src/gitingest/clone.py

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
from pathlib import Path
66
from typing import TYPE_CHECKING
77

8+
import git
9+
810
from gitingest.config import DEFAULT_TIMEOUT
911
from gitingest.utils.git_utils import (
1012
check_repo_exists,
1113
checkout_partial_clone,
12-
create_git_auth_header,
13-
create_git_command,
14+
create_git_repo,
1415
ensure_git_installed,
16+
git_auth_context,
1517
is_github_host,
1618
resolve_commit,
17-
run_command,
1819
)
1920
from gitingest.utils.logging_config import get_logger
2021
from gitingest.utils.os_utils import ensure_directory_exists_or_create
@@ -46,6 +47,8 @@ async def clone_repo(config: CloneConfig, *, token: str | None = None) -> None:
4647
------
4748
ValueError
4849
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.
4952
5053
"""
5154
# Extract and validate query parameters
@@ -83,41 +86,91 @@ async def clone_repo(config: CloneConfig, *, token: str | None = None) -> None:
8386
commit = await resolve_commit(config, token=token)
8487
logger.debug("Resolved commit", extra={"commit": commit})
8588

86-
clone_cmd = ["git"]
87-
if token and is_github_host(url):
88-
clone_cmd += ["-c", create_git_auth_header(token, url=url)]
89-
90-
clone_cmd += ["clone", "--single-branch", "--no-checkout", "--depth=1"]
91-
if partial_clone:
92-
clone_cmd += ["--filter=blob:none", "--sparse"]
93-
94-
clone_cmd += [url, local_path]
95-
96-
# Clone the repository
97-
logger.info("Executing git clone command", extra={"command": " ".join([*clone_cmd[:-1], "<url>", local_path])})
98-
await run_command(*clone_cmd)
99-
logger.info("Git clone completed successfully")
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
100117

101118
# Checkout the subpath if it is a partial clone
102119
if partial_clone:
103120
logger.info("Setting up partial clone for subpath", extra={"subpath": config.subpath})
104121
await checkout_partial_clone(config, token=token)
105122
logger.debug("Partial clone setup completed")
106123

107-
git = create_git_command(["git"], local_path, url, token)
124+
# Perform post-clone operations
125+
await _perform_post_clone_operations(config, local_path, url, token, commit)
108126

109-
# Ensure the commit is locally available
110-
logger.debug("Fetching specific commit", extra={"commit": commit})
111-
await run_command(*git, "fetch", "--depth=1", "origin", commit)
127+
logger.info("Git clone operation completed successfully", extra={"local_path": local_path})
112128

113-
# Write the work-tree at that commit
114-
logger.info("Checking out commit", extra={"commit": commit})
115-
await run_command(*git, "checkout", commit)
116129

117-
# Update submodules
118-
if config.include_submodules:
119-
logger.info("Updating submodules")
120-
await run_command(*git, "submodule", "update", "--init", "--recursive", "--depth=1")
121-
logger.debug("Submodules updated successfully")
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.
122138
123-
logger.info("Git clone operation completed successfully", extra={"local_path": local_path})
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

0 commit comments

Comments
 (0)