scitex_git

scitex-git — git operations and utilities (extracted from SciTeX).

scitex_git.init_git_repo(project_dir, git_strategy='child')[source]

Initialize or detect git repository based on git_strategy.

Parameters:
  • project_dir (Path) – Project directory

  • git_strategy (Optional[str]) – Git initialization strategy - None: Git disabled, returns None - ‘child’: Creates isolated git repo in project directory - ‘parent’: Tries to use parent git, degrades to ‘child’ if not found - ‘origin’: Preserves template’s original git history (handled by clone)

Return type:

Optional[Path]

Returns:

Path to git repository root, or None if disabled

scitex_git.find_parent_git(project_dir)[source]

Find parent git repository by walking up directory tree.

Parameters:

project_dir (Path) – Starting directory to search from

Return type:

Optional[Path]

Returns:

Path to parent git root, or None if not found

scitex_git.create_child_git(project_dir)[source]

Create isolated git repository in project directory.

Uses GitPython to initialize and make initial commit.

Parameters:

project_dir (Path) – Directory to initialize as git repo

Return type:

Optional[Path]

Returns:

Path to git root (project_dir), or None on failure

scitex_git.remove_child_git(project_dir)[source]

Remove project’s local .git folder.

When parent git is found, the project’s own .git/ is redundant and should be removed to avoid nested git repository issues.

Parameters:

project_dir (Path) – Project directory containing .git to remove

Return type:

bool

Returns:

True if removed successfully or doesn’t exist, False on error

scitex_git.clone_repo(url, target_path, branch=None, tag=None, verbose=True)[source]

Safely clone a git repository.

Parameters:
  • url (str) – Git repository URL

  • target_path (Path) – Destination path for cloning

  • branch (str, optional) – Specific branch to clone. If None, clones the default branch. Mutually exclusive with tag parameter.

  • tag (str, optional) – Specific tag/release to clone. If None, clones the default branch. Mutually exclusive with branch parameter.

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False otherwise

Return type:

bool

Raises:

ValueError – If both branch and tag are specified

scitex_git.git_init(repo_path, verbose=True)[source]

Initialize a new git repository.

Parameters:
  • repo_path (Path) – Path to initialize as git repository

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False otherwise

Return type:

bool

scitex_git.git_add_all(repo_path, verbose=True)[source]

Add all files to git staging.

Parameters:
  • repo_path (Path) – Git repository path

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False if: - repo_path is not a git repository - git add command fails - path is invalid

Return type:

bool

Examples

>>> git_add_all(Path("/my/repo"))
True
scitex_git.git_commit(repo_path, message, verbose=True)[source]

Create a git commit.

Parameters:
  • repo_path (Path) – Git repository path

  • message (str) – Commit message

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False if: - repo_path is not a git repository - message is empty - git commit command fails - no changes to commit

Return type:

bool

Examples

>>> git_commit(Path("/my/repo"), "Fix bug in parser")
True
scitex_git.git_branch_rename(repo_path, new_name, verbose=True)[source]

Rename current branch.

Parameters:
  • repo_path (Path) – Git repository path

  • new_name (str) – New branch name

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False if: - repo_path is not a git repository - new_name is invalid - git branch command fails

Return type:

bool

Examples

>>> git_branch_rename(Path("/my/repo"), "main")
True
scitex_git.git_checkout_new_branch(repo_path, branch_name, verbose=True)[source]

Create and checkout a new branch.

Parameters:
  • repo_path (Path) – Git repository path

  • branch_name (str) – New branch name

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False if: - repo_path is not a git repository - branch_name is invalid - git checkout command fails

Return type:

bool

Examples

>>> git_checkout_new_branch(Path("/my/repo"), "feature/auth")
True
scitex_git.get_remote_url(repo_path, remote_name='origin', verbose=False)[source]

Get remote URL for a git repository.

Parameters:
  • repo_path (Path) – Git repository path

  • remote_name (str) – Remote name (default: origin)

  • verbose (bool) – Enable verbose output

Returns:

Remote URL if found, None otherwise

Return type:

Optional[str]

Notes

Returns None if remote doesn’t exist or repo is not a git repository. Check stderr via sh() directly if detailed error info is needed.

scitex_git.is_cloned_from(repo_path, expected_url, remote_name='origin')[source]

Check if directory is a git repository cloned from specific URL. Handles both HTTPS and SSH URL formats.

Parameters:
  • repo_path (Path) – Directory to check

  • expected_url (str) – Expected remote URL

  • remote_name (str) – Remote name to check (default: origin)

Returns:

True if directory is cloned from expected URL, False otherwise

Return type:

bool

scitex_git.ls_remote(url, ref=None, verbose=False)[source]

Get commit hash of a remote ref via git ls-remote.

Parameters:
  • url (str) – Git repository URL.

  • ref (str, optional) – Branch name, tag, or ref pattern. If None, returns HEAD.

  • verbose (bool) – Enable verbose output.

Returns:

Commit SHA-1 hash (40 hex chars), or None on failure.

Return type:

Optional[str]

Examples

>>> ls_remote("https://github.com/user/repo.git")
'abc123...'
>>> ls_remote("https://github.com/user/repo.git", ref="main")
'abc123...'
>>> ls_remote("https://github.com/user/repo.git", ref="v1.0.0")
'def456...'
scitex_git.get_head_hash(repo_path, verbose=False)[source]

Get HEAD commit hash of a local git repository.

Parameters:
  • repo_path (Path) – Git repository path (must contain .git/).

  • verbose (bool) – Enable verbose output.

Returns:

Commit SHA-1 hash, or None if not a git repo.

Return type:

Optional[str]

scitex_git.setup_branches(repo_path, template_name, verbose=True)[source]

Setup standard git branches (main and develop).

Parameters:
  • repo_path (Path) – Git repository path

  • template_name (str) – Template name for initial commit message

  • verbose (bool) – Enable verbose output

Returns:

True if successful, False otherwise

Return type:

bool

Notes

This function attempts to rollback changes if branch operations fail. If add or commit fail, no rollback is needed as the repo state is unchanged.

scitex_git.git_retry(operation, max_retries=5, initial_delay=0.1, max_delay=2.0, backoff_factor=2.0)[source]

Retry git operations with exponential backoff.

Handles git index.lock conflicts when multiple processes access git.

Parameters:
  • operation (Callable) – Function to retry

  • max_retries (int) – Maximum number of retry attempts

  • initial_delay (float) – Initial delay in seconds

  • max_delay (float) – Maximum delay between retries

  • backoff_factor (float) – Exponential backoff multiplier

Returns:

Result of operation

Return type:

T

Raises:
  • TimeoutError – If all retries exhausted due to lock

  • Exception – Original exception if not a lock error

Examples

>>> def commit_file():
...     subprocess.run(["git", "commit", "-m", "msg"], check=True)
>>> git_retry(commit_file)
exception scitex_git.GhSecretError[source]

Raised when a gh subprocess returns non-zero.

scitex_git.set_secret(repo, name, value)[source]

Push value into the Actions secret name for repo.

repo follows the owner/repo form. Idempotent — creates if missing, updates otherwise.

Return type:

None

scitex_git.list_secrets(repo)[source]

Return {secret_name: updated_at_iso} for the repo.

Pagination is honoured (--paginate) so this scales beyond GitHub’s 100-secret default page size.

Return type:

dict[str, str]

scitex_git.get_secret_metadata(repo, name)[source]

Return the secret’s metadata dict (name / created_at / updated_at), or None.

GitHub never exposes the value itself — that’s the whole point of Actions secrets. Use get_variable() against a SHA sidecar (see set_secret_with_sha_sidecar()) if you need to detect value drift.

Return type:

dict | None

scitex_git.set_variable(repo, name, value)[source]

Create-or-update an Actions variable. Idempotent.

Variables are visible to anyone with repo read access. Use this for non-sensitive metadata (e.g. SHA256 fingerprints of secrets, feature flags). NEVER store token material here.

Return type:

None

scitex_git.get_variable(repo, name)[source]

Return the variable’s value, or None if absent.

Return type:

str | None

scitex_git.set_secret_with_sha_sidecar(repo, name, value, *, sidecar_suffix='_SHA256')[source]

Push the secret AND its SHA256 fingerprint as a public variable.

Returns the SHA256 hex digest so the caller can show it in CLI output. The sidecar variable is named {name}{sidecar_suffix} and holds the irreversible hash; later get_variable(repo, f"{name}{sidecar_suffix}") lets you tell whether the GitHub-side secret matches a local value (compare with sha256_hex(local_value)).

The hash itself is not sensitive — it cannot be reversed to the original token without a brute-force search of the OAuth/api-key namespace, which is computationally infeasible. It IS visible to repo readers, so don’t use the sidecar for tokens whose mere existence is sensitive (the hash leaks “this token was rotated at {time} to a value with hash {h}” but never the value).

Return type:

str

scitex_git.sha256_hex(value)[source]

Return the SHA256 hex digest of value (UTF-8).

Return type:

str

scitex_git.format_age(updated_iso)[source]

Render an ISO-8601 timestamp as a single-unit human age.

Picks the largest unit that gives a readable number: seconds for very fresh rotations, then minutes, hours, days. Mirrors the git relative_date style — a single value + unit, no compound “1d 4h” form. Returns "?" for None input.

Return type:

str

>>> format_age(None)
'?'