Metadata-Version: 2.1
Name: wexample-filestate-git
Version: 8.1.0
Summary: Declares desired Git state for wexample-filestate targets: repo init, branches, remotes, and CI variables.
Author-Email: weeger <contact@wexample.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Project-URL: homepage, https://github.com/wexample/python-filestate-git
Requires-Python: >=3.10
Requires-Dist: attrs>=23.1.0
Requires-Dist: cattrs>=23.1.0
Requires-Dist: requests
Requires-Dist: wexample-api>=6.8.0
Requires-Dist: wexample-filestate>=17.0.0
Requires-Dist: wexample-helpers-git>=7.1.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Description-Content-Type: text/markdown

# filestate_git

Version: 8.1.0

`wexample-filestate-git` extends `wexample-filestate` with a `git` option that lets any filesystem target declare its desired Git state: repository initialisation, branch aliases and renames, remote registration (including creating the repository on GitHub or GitLab through their APIs), and CI variable synchronisation. It also ships a `gitignore` option that reorganises `.gitignore` files by deduplicating and alphabetically sorting entries within each blank-line section. It is for developers who manage repositories as code and want a declarative spec — rather than manual shell commands — to bring each target's Git configuration into the desired state.

## Table of Contents

- [Installation](#installation)
- [Quickstart](#quickstart)
- [Tests](#tests)
- [Architecture](#architecture)
- [Integration in the Suite](#integration-in-the-suite)
- [Dependencies](#dependencies)
- [Versioning & Compatibility Policy](#versioning--compatibility-policy)
- [License](#license)
- [About us](#about-us)
- [Known Limitations & Roadmap](#known-limitations--roadmap)
- [Status & Compatibility](#status--compatibility)
- [Useful Links](#useful-links)
- [Migration Notes](#migration-notes)

## Installation

```bash
pip install wexample-filestate-git
```

Requires Python >=3.10.

## Quickstart

`wexample-filestate-git` extends the `wexample-filestate` state manager with Git-aware options. Register `GitOptionsProvider` alongside the default provider when you build the manager, then set `"git"` on any directory target.

```python
from pathlib import Path

from wexample_filestate.const.disk import DiskItemType
from wexample_filestate.options_provider.default_options_provider import DefaultOptionsProvider
from wexample_filestate.utils.file_state_manager import FileStateManager
from wexample_filestate_git.options_provider.git_options_provider import GitOptionsProvider
from wexample_prompt.common.io_manager import IoManager

manager = FileStateManager.create_from_path(
    io=IoManager(),
    path=Path("/path/to/workspace"),
    options_providers=[DefaultOptionsProvider, GitOptionsProvider],
)

manager.configure(config={
    "children": [
        {
            "name": "my-repo",
            "type": DiskItemType.DIRECTORY,
            "git": True,
        }
    ]
})

manager.apply()
```

After `apply()`, `/path/to/workspace/my-repo/.git` exists. If the directory was already a Git repository the call is a no-op.

Pass a dict instead of `True` to declare more of the desired state:

```python
manager.configure(config={
    "children": [
        {
            "name": "my-repo",
            "type": DiskItemType.DIRECTORY,
            "git": {
                "main_branch": "main",
                "remote": [
                    {"name": "origin", "url": "https://github.com/org/my-repo.git"},
                ],
            },
        }
    ]
})

manager.apply()
```

This initialises the repository, creates the `main` branch if it does not exist, and registers `origin` if it is absent or points at a different URL.

## Tests

This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.

### Installation

First, install the required testing dependencies:
```bash
.venv/bin/python -m pip install pytest pytest-cov
```

### Basic Usage

Run all tests with coverage:
```bash
.venv/bin/python -m pytest --cov --cov-report=html
```

### Common Commands
```bash
# Run tests with coverage for a specific module
.venv/bin/python -m pytest --cov=your_module

# Show which lines are not covered
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing

# Generate an HTML coverage report
.venv/bin/python -m pytest --cov=your_module --cov-report=html

# Combine terminal and HTML reports
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html

# Run specific test file with coverage
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
```

### Viewing HTML Reports

After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.

### Coverage Threshold

To enforce a minimum coverage percentage:
```bash
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
```

This will cause the test suite to fail if coverage drops below 80%.

## Architecture

`wexample-filestate-git` extends `wexample-filestate` with Git awareness: it adds options that declare what Git state a target directory should be in and generates the operations needed to reach it.

### Layers

The package is divided into four layers that a call passes through in order: **options provider → options → operations → remote gateways**.

### Options provider

src/wexample_filestate_git/options_provider/git_options_provider.py is the package's single registration point. It extends `AbstractOptionsProvider` and returns two top-level options:

- `GitOption` — the `git` key on any filestate target
- `GitignoreOption` — the `gitignore` key on a file target

The filestate framework discovers this provider and makes both options available on any target without any per-target wiring.

### Options

**Top-level options** live in src/wexample_filestate_git/option/:

- src/wexample_filestate_git/option/git_option.py — `AbstractNestedConfigOption` subclass. Accepts `True` (bare init, no sub-config) or a dict. When its `create_required_operation()` is called, it first checks whether `.git/` exists; if not, it returns `GitInitOperation` immediately. If the repo is already initialized, it delegates to its children via `_create_child_required_operation()`. Its `get_allowed_options()` declares four children.
- src/wexample_filestate_git/option/gitignore_option.py — `AbstractFileContentOption` subclass. Rewrites a `.gitignore` in-place: deduplicates entries and sorts alphabetically within blank-line sections. Bails without change on any file containing globs (`*`, `?`, `[`) or negations (`!`).

**Child options** under src/wexample_filestate_git/option/_git/ are the four children `GitOption` allows:

- src/wexample_filestate_git/option/_git/main_branch_option.py — accepts a string or list. `_get_desired_branch_name()` resolves the first item of a list, or the bare string, defaulting to `"main"`. `create_required_operation()` checks `repo.heads`; if the branch is absent, it returns `GitCreateBranchOperation`.
- src/wexample_filestate_git/option/_git/branches_option.py — accepts a dict of `{canonical: {aliases: [...], on_alias_conflict: "merge"|"skip"|"error", sync_remote: bool}}`. Iterates local branches first, then each remote's refs. Returns one of three operations per pass: `GitRenameBranchOperation` (alias exists, canonical does not), `GitMergeBranchOperation` (both exist, conflict is `"merge"`), or `GitDeleteRemoteBranchOperation` (alias still on a remote after local cleanup). One operation per rectify pass; the framework re-evaluates until `None` is returned.
- src/wexample_filestate_git/option/_git/remote_option.py — `AbstractListConfigOption`; each item is a `RemoteItemOption`. Maintains a process-level `_REMOTE_EXISTS_CACHE: set[str]` to avoid repeating API existence checks within a single run. Two priorities: (1) if `create_remote` is set and the remote repository does not exist, return `GitRemoteCreateOperation`; (2) if any configured remote is absent or has the wrong URL locally, return `GitRemoteAddOperation`.
- src/wexample_filestate_git/option/_git/ci_variables_option.py — accepts a list of environment-variable names. For each name, reads the local value via `target.get_env_parameter_or_suite_fallback()`, compares it to the current value on the remote API, and queues names that differ in a `vars_to_sync` dict. Returns `GitSyncCiVariablesOperation` if anything is out of sync. Uses `_CI_VARIABLES_SYNCED_CACHE: set[tuple[str, str]]` to skip already-confirmed pairs.

Each remote item is parsed by src/wexample_filestate_git/option/_git/remote_item_option.py, which is an `AbstractNestedConfigOption` that allows `NameOption`, `CreateRemoteOption`, `TypeOption`, and `UrlOption` as children. src/wexample_filestate_git/option/_git/url_option.py is notable: it accepts either a plain string or a callable `(target) -> str`, resolved at rectify time.

### Config values

src/wexample_filestate_git/config_value/git_config_value.py, src/wexample_filestate_git/config_value/remote_config_value.py, and src/wexample_filestate_git/config_value/branch_design_config_value.py are typed `ConfigValue` subclasses (attrs-backed). They are alternative inputs — a caller may pass a `GitConfigValue` instance instead of a raw dict; `to_option_raw_value()` converts it back to the dict form the options expect. They do not participate in the operation pipeline themselves.

### Operations

All operations extend src/wexample_filestate_git/operation/abstract_git_operation.py, which extends `AbstractOperation` from `wexample-filestate`. `AbstractGitOperation` adds:

- `_get_target_git_repo()` — returns a GitPython `Repo` for the target path.
- `_is_active_flag()` / `_is_active_git_option()` — evaluate the `active` sub-key on the git option.
- All operations carry `Scope.REMOTE`, meaning they only run in remote-scope rectification passes.

The concrete operations:

| File | What `apply_operation()` does |
|---|---|
| src/wexample_filestate_git/operation/git_init_operation.py | `Repo.init(path)` |
| src/wexample_filestate_git/operation/git_create_branch_operation.py | Creates an empty initial commit if the repo has none, then `repo.create_head(branch_name)` |
| src/wexample_filestate_git/operation/git_rename_branch_operation.py | `branch.rename(to_branch)`, pushes the new name, calls the API to unprotect the old name and set the new default, then deletes the old remote branch |
| src/wexample_filestate_git/operation/git_merge_branch_operation.py | Checks out the canonical branch, `git merge alias --no-edit`, deletes the alias head, pushes the canonical branch, and deletes the alias from every remote |
| src/wexample_filestate_git/operation/git_delete_remote_branch_operation.py | Unprotects the alias and sets the canonical as default via API, then `remote.push(refspec=f":{branch}")` |
| src/wexample_filestate_git/operation/git_remote_add_operation.py | `repo.create_remote(name, url)` for new remotes; `existing.set_url(url)` for URL mismatches |
| src/wexample_filestate_git/operation/git_remote_create_operation.py | Calls `remote.connect()` then `remote.create_repository_if_not_exists(url)`; adds the URL to `_REMOTE_EXISTS_CACHE` |
| src/wexample_filestate_git/operation/git_sync_ci_variables_operation.py | Calls `api_remote.set_ci_variable()` for each variable in `self.variables`; adds synced pairs to `_CI_VARIABLES_SYNCED_CACHE` |

### Remote gateways

src/wexample_filestate_git/remote/abstract_remote.py extends `AbstractGateway` from `wexample-api`. It declares the interface: `detect_remote_type()`, `build_remote_api_url_from_repo()`, `check_repository_exists()`, `create_repository()`, `parse_repository_url()`, `set_ci_variable()`, `set_default_branch()`, `unprotect_branch()`, `create_merge_proposal()`, `merge_merge_proposal()`, `get_pipeline()`, and `poll_pipeline()`. The last method implements a polling loop with a configurable timeout and tick callback.

Two concrete implementations ship:

- src/wexample_filestate_git/remote/github_remote.py — targets `https://api.github.com` (or `https://{host}/api/v3` for GHE). Detects URLs matching `github.com[:/]`. Authentication header: `Authorization: token {GITHUB_API_TOKEN}`.
- src/wexample_filestate_git/remote/gitlab_remote.py — targets `https://{host}/api/v4`. Detects URLs matching `gitlab.[domain][:/]`. Supports SSH, git@, and HTTPS URL forms. Authentication header: `PRIVATE-TOKEN: {GITLAB_API_TOKEN}`. `merge_merge_proposal()` polls `detailed_merge_status` before calling the merge endpoint to avoid 405 errors on unchecked MRs.

src/wexample_filestate_git/remote/mixin/with_git_remote_mixin.py is mixed into options and operations that need to reach a remote API. It provides three static methods:

- `_detect_remote_type(url)` — tries `GithubRemote.detect_remote_type()` then `GitlabRemote.detect_remote_type()`, returns the class or `None`.
- `_get_api_token(remote_type, target)` — derives the env key as `{SNAKE_SHORT_CLASS_NAME}_API_TOKEN` (e.g., `GITHUB_API_TOKEN`) and reads it via `target.get_env_parameter_or_suite_fallback()`; raises `MissingEnvVariableException` if absent.
- `_build_remote_instance(remote_type, remote_url, target)` — instantiates the gateway class with `io`, `api_token`, and `base_url` derived from `build_remote_api_url_from_repo(url)`.

### Call path

A typical rectification of a directory target that declares `git: {main_branch: main, remote: [{url: "git@github.com:org/repo.git", create_remote: true}]}`:

1. `GitOptionsProvider` makes `GitOption` available on the target.
2. The framework calls `GitOption.create_required_operation(target, scopes)`.
3. If `.git/` is absent, `GitInitOperation` is returned and applied (`Repo.init()`). The framework re-runs the same option next pass.
4. Git is now initialized. `GitOption` calls `_create_child_required_operation()`, which iterates its children.
5. `MainBranchOption.create_required_operation()` finds no `main` branch → returns `GitCreateBranchOperation` → `repo.create_head("main")`. Applied; next pass.
6. `RemoteOption.create_required_operation()` iterates `RemoteItemOption` children. `create_remote` is `True`; `_detect_remote_type()` returns `GithubRemote`; `_build_remote_instance()` constructs the gateway. `remote.check_repository_exists()` returns `False` → returns `GitRemoteCreateOperation`. Applied; URL added to cache.
7. Next pass: repository exists (cache hit). `_collect_remotes_to_add()` finds no local remote named `origin` → returns `GitRemoteAddOperation` → `repo.create_remote("origin", url)`. Applied.
8. Next pass: all conditions satisfied. Every `create_required_operation()` returns `None`. Rectification is complete.

## Integration in the Suite

This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.

### Related Packages

The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.

Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.

## Dependencies

- attrs: >=23.1.0
- cattrs: >=23.1.0
- requests: 
- wexample-api: >=6.8.0
- wexample-filestate: >=17.0.0
- wexample-helpers-git: >=7.1.0

## Versioning & Compatibility Policy

Wexample packages follow **Semantic Versioning** (SemVer):

- **MAJOR**: Breaking changes
- **MINOR**: New features, backward compatible
- **PATCH**: Bug fixes, backward compatible

We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

Free to use in both personal and commercial projects.

## About us

[Wexample](https://wexample.com) stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.

This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.

Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.

## Known Limitations & Roadmap

Current limitations and planned features are tracked in the GitHub issues.

See the [project roadmap](https://github.com/wexample/python-filestate_git/issues) for upcoming features and improvements.

## Status & Compatibility

**Maturity**: Production-ready

**Python Support**: >=3.10

**OS Support**: Linux, macOS, Windows

**Status**: Actively maintained

## Useful Links

- **Homepage**: https://github.com/wexample/python-filestate-git
- **Documentation**: [docs.wexample.com](https://docs.wexample.com)
- **Issue Tracker**: https://github.com/wexample/python-filestate-git/issues
- **Discussions**: https://github.com/wexample/python-filestate-git/discussions
- **PyPI**: [pypi.org/project/wexample-filestate-git](https://pypi.org/project/wexample-filestate-git/)

## Migration Notes

When upgrading between major versions, refer to the migration guides in the documentation.

Breaking changes are clearly documented with upgrade paths and examples.
