Git Operations¶
git
¶
Generic git operations for CI pipelines.
Host-side git operations: clone, push, branch creation, diff inspection. All operations use subprocess calls to git.
RemoteLease
¶
Bases: Enum
Special values for push_branch(expected_remote_sha=...).
TRACKING = 'tracking'
class-attribute
instance-attribute
¶
Bare --force-with-lease: git reads the expected value from the
local remote-tracking ref (the default, and the behavior before
RHAI-3020).
ABSENT = 'absent'
class-attribute
instance-attribute
¶
The remote branch must not exist yet: --force-with-lease=refs/heads/<branch>:.
GitControlTamperError
¶
Bases: RuntimeError
The agent replaced .git itself, so the host copy cannot be restored.
GitControlSnapshot(repo_dir, dot_git, entries)
dataclass
¶
Host copy of the .git files that decide what host-side git executes.
Taken by :func:snapshot_git_control before an agent can write the
repository and put back by :func:restore_git_control afterwards.
GitDiffError
¶
Bases: Exception
Raised when git diff fails (missing ref, not a repo, etc.).
extract_repo_url(text)
¶
Extract a repo URL from text, validating against forge APIs.
Filters out subpaths, file extensions, and placeholder URLs. Returns the first URL that resolves to a real project, or the first unvalidated candidate if no API tokens are available.
Source code in src/agentic_ci/git.py
extract_all_repo_urls(text)
¶
Extract all distinct repo root URLs from text.
Scans for both GitLab and GitHub URLs, filters out subpaths, file extensions, and placeholder URLs. GitLab URLs that are strict prefixes of other GitLab URLs are collapsed (nested group dedup).
Unlike :func:extract_repo_url, this does not validate URLs
against forge APIs -- it returns all plausible candidates.
Source code in src/agentic_ci/git.py
validate_repo_url(url)
¶
Check that a repo URL points to an allowed host with no path traversal.
Source code in src/agentic_ci/git.py
validate_branch_exists(repo_url, branch)
¶
Check if a branch exists on the remote repository.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_url
|
str
|
HTTPS URL of the git repository |
required |
branch
|
str
|
Branch name to validate |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the branch exists on the remote, False otherwise |
Note
Returns False for any error condition (network issues, invalid refs, etc.) to allow graceful fallback in the resolution chain.
Source code in src/agentic_ci/git.py
clone_repo(url, dest, branch=None, depth=None)
¶
Clone a repository. Returns True on success.
Source code in src/agentic_ci/git.py
create_branch(repo_dir, branch_name)
¶
Create and checkout a new branch.
Source code in src/agentic_ci/git.py
checkout_branch(repo_dir, branch)
¶
Checkout an existing branch. Returns True on success.
Source code in src/agentic_ci/git.py
rebase_branch(repo_dir, onto)
¶
Rebase the current branch onto onto. Returns True on success.
On conflict the rebase is aborted so the worktree stays clean.
Source code in src/agentic_ci/git.py
get_default_branch(repo_dir)
¶
Detect the default branch of the remote origin.
Runs git rev-parse --abbrev-ref origin/HEAD and strips the
origin/ prefix. Falls back to "main" when the remote HEAD
cannot be determined.
Source code in src/agentic_ci/git.py
git_output(repo_dir, *args)
¶
Run a git command and return its stripped stdout, or None on error.
This is a thin wrapper around subprocess.run for cases where
the caller only needs the text output of a git command.
Source code in src/agentic_ci/git.py
push_branch(repo_dir, remote='origin', branch=None, *, max_retries=GIT_PUSH_MAX_RETRIES, retry_delay=GIT_PUSH_RETRY_DELAY, expected_remote_sha=RemoteLease.TRACKING)
¶
Push a branch to remote with a force-with-lease. Returns True on success.
Retries up to max_retries times on transient errors (server 5xx,
commit_refs failures, lock contention, network resets) with
exponential backoff starting at retry_delay seconds. A lease
rejection (git reports stale info) is never retried and is
logged as such: someone else moved the remote branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_dir
|
Path
|
Local repository to push from. |
required |
remote
|
str
|
Remote name (not a URL). |
'origin'
|
branch
|
str | None
|
Branch to push. Defaults to the checked-out branch.
Required when expected_remote_sha is given, and then it
must be a short name (not starting with |
None
|
max_retries
|
int
|
Retries after the first attempt on transient errors. |
GIT_PUSH_MAX_RETRIES
|
retry_delay
|
float
|
Initial backoff in seconds. |
GIT_PUSH_RETRY_DELAY
|
expected_remote_sha
|
str | RemoteLease
|
What the remote branch must point at for
the push to go through. The default,
|
TRACKING
|
Source code in src/agentic_ci/git.py
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 | |
setup_git_config(repo_dir, name, email)
¶
Set local git user config.
Source code in src/agentic_ci/git.py
harden_git_config(repo_dir)
¶
Apply security hardening to git config (disable hooks, fsmonitor).
Source code in src/agentic_ci/git.py
snapshot_git_control(repo_dir)
¶
Record the git control files of the repository at repo_dir.
Call this on the host before an agent can write repo_dir (for example
before a sandbox upload or a container bind mount), after any hardening
such as :func:harden_git_config. Pass the result to
:func:restore_git_control once the agent can no longer write the
repository.
Source code in src/agentic_ci/git.py
restore_git_control(snapshot)
¶
Put the host's git control files back after an agent had write access.
Everything under :data:GIT_CONTROL_PATHS is moved out of git's reach and
rewritten from snapshot, so config keys, hooks, attributes and
commondir redirects added by the agent are gone before any host git
command runs, while its commits, refs and index are kept. Nothing the
agent wrote is read or walked before that, so no tree it built can make
the restore fail early, and symlinks it planted are never written through.
A .git the agent created in a workdir that had none is removed.
Returns the control paths the agent had changed (best effort, for the
log). Raises :class:GitControlTamperError when .git was a directory
and is now missing, a symlink or a file (that entry is deleted first), or
when the host copy cannot be put back; .git is then moved aside so
host git cannot use the agent's config.
Source code in src/agentic_ci/git.py
929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 | |
discard_git_dir(repo_dir)
¶
Move repo_dir/.git aside and delete it.
For when the host copy cannot be restored safely, for example because an
agent process may still be running and able to write .git. Host git
then finds no repository in repo_dir instead of the agent's config. The
agent's commits are lost with it. Failures are logged, not raised, so the
caller's own error is what propagates.
Source code in src/agentic_ci/git.py
get_commit_info(repo_dir)
¶
Get the latest commit info (committer, email, message, sha).
Uses committer identity (not author) so that rebased or cherry-picked commits always reflect the current git config.
Source code in src/agentic_ci/git.py
get_changed_files(repo_dir, base_ref='HEAD~1', *, include_deleted=True)
¶
Return files changed between base_ref and HEAD (committed state only).
Uses the two-ref form git diff <base_ref> HEAD so that files still in
HEAD after a failed git commit --amend are detected even when
git rm --cached already removed them from the index.
Rename detection is off, so a moved file is listed by both its old and
its new path: a check on protected paths sees a file moved out of them.
Names are read NUL-separated and decoded with os.fsdecode, so they
are never C-quoted and a name that is not UTF-8 cannot raise. git
replace refs, which the agent can write, are ignored. With
include_deleted False, paths that no longer exist in HEAD (deleted
files and the old side of a move) are left out.
Raises GitDiffError if the git command fails.
Source code in src/agentic_ci/git.py
unquote_git_path(path)
¶
Undo git's C-quoting of a path ("caf\303\251" becomes café).
Git quotes names with unusual characters in its default (non -z)
output. Only git's own escapes are decoded: the letter escapes, and
octal escapes for the bytes git writes in octal (control characters,
DEL and non-ASCII). A path without the surrounding double quotes is
returned as is, and so is one with an escape git never writes, such as
\057 for /, so an escape cannot hide a / or other
printable character from a caller matching the result.
Source code in src/agentic_ci/git.py
display_path(path)
¶
Return path safe to show in a log line or gate message.
get_changed_files() returns names as git stores them, so an
agent-chosen name can hold a newline or other control character that
would forge extra log lines. Such names come back with those
characters (and any non-ASCII, undecodable bytes included) escaped; a
printable name is returned unchanged.
Source code in src/agentic_ci/git.py
strip_committed_files(repo_dir, patterns, base_ref='origin/HEAD')
¶
Remove files matching patterns from the latest commit.
Agents can bypass .git/info/exclude by explicitly naming files in
git add. This function detects any committed files that match the
given fnmatch patterns and amends the commit to remove them, keeping
the working-tree copies intact.
Returns the list of file paths actually stripped (empty if none matched or all removals failed).
Source code in src/agentic_ci/git.py
1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 | |
setup_git_credentials(repo_url, *, github_token_resolver=None)
¶
Configure git url.insteadOf for the forge hosting repo_url.
Sets up transparent credential injection so that clone_repo() and
push_branch() (which use bare HTTPS URLs) can authenticate
without modification.
For GitLab, reads BOT_PAT from the environment.
For GitHub, calls github_token_resolver(repo_url) to obtain a
short-lived token. If no resolver is provided for GitHub URLs,
returns False.
Idempotent and safe to call multiple times. Returns True on success, False if credentials are unavailable.