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
922 923 924 925 926 927 928 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 | |
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')
¶
Return files changed between base_ref and HEAD (committed state only).
Uses the two-ref form git diff --name-only <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.
Raises GitDiffError if the git command fails.
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
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 | |
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.