| --- |
| name: core-internals |
| description: Provides guidance and best practices on concurrent synchronization, IPC, filesystem atomicity, git subprocess wrapping, XML manifests, and CLI argument parsing for git-repo. |
| --- |
| |
| # Git Repo Core Internals Engineering Guide |
| |
| ## Executive Summary |
| |
| Welcome to the authoritative engineering guide for Git Repository Management |
| within the git-repo codebase. This repository of folk knowledge exists to |
| safeguard the intricate orchestration of our multi-repository synchronization |
| toolchain. Historically, complex operations such as parallelized network |
| fetches, layered manifest overrides, and interactive terminal rendering have |
| been vulnerable to subtle concurrency races, deadlocks, and fragile system |
| states. This guide captures these critical failure modes—ranging from IPC |
| serialization bottlenecks and filesystem lock contentions to non-hermetic test |
| pollution—and establishes rigid engineering constraints to prevent their |
| regression. |
| |
| To maintain system stability, this guide enforces strict architectural |
| boundaries across the tooling ecosystem. It mandates stateless execution for |
| multiprocessing pools, guaranteed atomicity for worktree layout modifications, |
| and deterministic error translation for all standard Git subprocesses. |
| Furthermore, it defines the standard operating procedures for canonical manifest |
| object deduplication, the modernization of our hermetic testing frameworks, and |
| the delivery of consistent, machine-readable CLI interfaces. By adhering to |
| these paradigms, incoming engineers will ensure the reliability, performance, |
| and extensibility of git-repo's core repository management infrastructure. |
| |
| ## Summary |
| |
| | Chapter Theme / Title | Scope & Objective | |
| | :----------------------------------- | :------------------------------------ | |
| | **Concurrent Synchronization & IPC** | This domain governs the stable | |
| : : orchestration of parallel network : |
| : : fetches, local checkouts, and : |
| : : interleaved subprocess routines. It : |
| : : strictly enforces safe : |
| : : multiprocessing IPC, deterministic : |
| : : Git locking via exponential backoff, : |
| : : and state synchronization across : |
| : : concurrent pool workers. : |
| | **Filesystem Atomicity & Worktree | This domain governs the deterministic | |
| : Layout** : creation, migration, and cleanup of : |
| : : internal repository structures and : |
| : : Git worktrees. It relies on ephemeral : |
| : : temporary directories, atomic rename : |
| : : operations, and robust error recovery : |
| : : to prevent corrupted states during : |
| : : unexpected interruptions. : |
| | **Subprocess Git Integration & Error | This chapter defines the constraints | |
| : Translation** : for wrapping, executing, and : |
| : : translating standard Git subprocesses : |
| : : within the Repo tooling ecosystem. It : |
| : : mandates the use of centralized : |
| : : command abstractions, strict version : |
| : : gating, and deterministic stream : |
| : : handling to guarantee reliable : |
| : : repository state management and : |
| : : actionable error reporting. : |
| | **Manifest Object Model & | This chapter governs the parsing, | |
| : Deduplication** : validation, and canonicalization of : |
| : : XML manifest components. It strictly : |
| : : enforces semantic immutability via : |
| : : NamedTuple implementations, defensive : |
| : : copying for hierarchical override : |
| : : scoping, and deterministic : |
| : : JSON-backed file tracking across the : |
| : : subsystem. : |
| | **Hermetic Testing & Test | This domain governs the migration of | |
| : Modernization** : legacy unittest suites to modern : |
| : : pytest functional paradigms and the : |
| : : establishment of hermetic session : |
| : : fixtures. It enforces strict : |
| : : environment isolation to prevent : |
| : : global state pollution (e.g., : |
| : : developer .gitconfig bleeding) while : |
| : : safely intercepting standard streams : |
| : : and filesystem paths. : |
| | **CLI Argument Parsing & UX | This chapter governs the lifecycle, | |
| : Consistency** : validation, and execution of : |
| : : command-line arguments, enforcing : |
| : : strict standardization for : |
| : : machine-readable serialization, : |
| : : unified logging, and deterministic, : |
| : : thread-safe terminal interactions. : |
| | **Repo Hooks Framework** | The Repo Hooks Framework governs the | |
| : : execution, parameter validation, and : |
| : : lifecycle management of user-defined : |
| : : scripts within the repository : |
| : : ecosystem. It ensures seamless : |
| : : integration of extensions like : |
| : : post-sync or pre-upload while : |
| : : strictly isolating their execution : |
| : : failures from core operational : |
| : : workflows. : |
| |
| -------------------------------------------------------------------------------- |
| -------------------------------------------------------------------------------- |
| |
| ## Chapter: Concurrent Synchronization & IPC |
| |
| **Context:** This domain governs the stable orchestration of parallel network |
| fetches, local checkouts, and interleaved subprocess routines. It strictly |
| enforces safe multiprocessing IPC, deterministic Git locking via exponential |
| backoff, and state synchronization across concurrent pool workers. |
| |
| ### Summary |
| |
| | Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap | |
| | :-------- | :------------------------ | :------- | :------------------------ | |
| | **T1-01** | Stateless Class Methods | High | Passing bound instance | |
| : : for Parallel Pool Workers : : methods to a parallel : |
| : : : : executor, dragging : |
| : : : : unnecessary state into : |
| : : : : the multiprocessing : |
| : : : : serialization pipeline. : |
| | **T1-02** | Buffered Serialization of | High | Executing terminal print | |
| : : Concurrent Standard : : calls directly from : |
| : : Output : : within a parallelized : |
| : : : : worker routine. : |
| | **T1-03** | Interleaved Sync Path | High | Dispatching parallel | |
| : : Validation : : checkout jobs across all : |
| : : : : project variants without : |
| : : : : verifying layout types. : |
| | **T1-04** | Guaranteed | Critical | Placing save operations | |
| : : Synchronization State : : at the very end of an : |
| : : Persistence : : execution path without : |
| : : : : exception guards. : |
| | **T1-05** | Jittered Exponential | Critical | Firing `git submodule | |
| : : Backoff for Git : : init` concurrently : |
| : : Configuration Locks : : without retry logic for : |
| : : : : `config.lock` failures. : |
| | **T1-06** | Exponential Backoff for | High | Failing a git command | |
| : : Concurrent Git Config : : immediately without : |
| : : Locks : : evaluating standard : |
| : : : : streams for transient : |
| : : : : lock errors. : |
| | **T1-07** | Hard Termination on | Critical | Logging an error state | |
| : : Interleaved Sync Stalls : : inside a `while` loop but : |
| : : : : allowing the next loop : |
| : : : : iteration to execute. : |
| | **T1-08** | Resource Management via | High | Initializing proxy | |
| : : Context Handlers for IPC : : connections or : |
| : : : : multiprocessing managers : |
| : : : : as raw variable : |
| : : : : assignments without a : |
| : : : : guaranteed teardown : |
| : : : : phase. : |
| | **T1-09** | Dual-Channel Error | High | Iterating over aggregated | |
| : : Handling in Parallel : : data lists to decide if a : |
| : : Processing : : parallel orchestration : |
| : : : : should abort. : |
| | **T1-10** | Minimum Process Pool Job | Critical | Directly using | |
| : : Count Safeguard : : `min(target, len(items))` : |
| : : : : to determine pool size, : |
| : : : : which breaks if the item : |
| : : : : list is unexpectedly : |
| : : : : empty. : |
| | **T1-11** | Safeguarding Object State | Critical | Truncating object | |
| : : Across Parallel Execution : : payloads to scalar : |
| : : Boundaries : : indices during IPC : |
| : : : : context setup without : |
| : : : : updating the receiver : |
| : : : : logic to rehydrate the : |
| : : : : objects. : |
| | **T1-12** | Explicit Context | High | Assuming child workers | |
| : : Initialization for : : inherit updated class : |
| : : Multiprocessing Pools : : variables inherently : |
| : : : : without explicit : |
| : : : : initialization. : |
| | **T1-13** | Dynamic Task Chunk Sizing | Medium | Passing a hardcoded batch | |
| : : in Parallel Execution : : integer to the : |
| : : : : `chunksize` parameter. : |
| | **T1-14** | Deferred Worktree | High | Returning early from the | |
| : : Operations in Sync Local : : sync phase without : |
| : : Half : : applying required file : |
| : : : : operations. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T1-01: Stateless Class Methods for Parallel Pool Workers |
| |
| > **Rule:** Always implement multiprocessing pool targets as stateless class |
| > methods or standalone functions to bypass process serialization constraints. |
| > |
| > **What:** Worker execution targets within multiprocessing pools must be |
| > constructed as fully decoupled class methods or standalone functions to bypass |
| > process serialization constraints. |
| > |
| > **Applies To:** Multiprocessing process pools, concurrent task scheduling, and |
| > `ExecuteInParallel` implementations. |
| > |
| > **Why:** Command execution objects were heavily bound to system state. |
| > Attempting to pass instance methods (`self.method`) to a multiprocessing pool |
| > frequently caused `PicklingError` crashes, as the underlying Python |
| > serialization mechanism cannot cleanly isolate bound object graphs. Failing to |
| > adhere to this typically results in **IPC Serialization Error**. |
| |
| **Trap 1: Passing bound instance methods to a parallel executor, dragging |
| unnecessary state into the multiprocessing serialization pipeline.** |
| |
| **Don't:** |
| |
| ```python |
| class InfoCommand: |
| def _worker_logic(self, project): |
| pass |
| |
| def run(self): |
| self.ExecuteInParallel(jobs, self._worker_logic, projects) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| class InfoCommand: |
| @classmethod |
| def _worker_logic(cls, project_idx): |
| project = cls.get_parallel_context()["projects"][project_idx] |
| pass |
| |
| def run(self): |
| self.ExecuteInParallel(jobs, self._worker_logic, range(len(projects))) |
| ``` |
| |
| **Exceptions:** Threading-based execution models where memory is shared and |
| pickling is not strictly enforced. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-02: Buffered Serialization of Concurrent Standard Output |
| |
| > **Rule:** Must capture standard output generated by concurrently executing |
| > tasks into isolated memory buffers for sequential display in the parent |
| > process. |
| > |
| > **What:** Data emitted by concurrently executing tasks must be captured into |
| > isolated memory buffers and returned to the parent process for sequential |
| > display. |
| > |
| > **Applies To:** Parallel process execution layers generating human-readable |
| > CLI output. |
| > |
| > **Why:** When command execution was parallelized, worker processes wrote |
| > directly to standard output. This created severe race conditions resulting in |
| > garbled, interleaved text output on the user's terminal. Failing to adhere to |
| > this typically results in **Interleaved Terminal Output**. |
| |
| **Trap 1: Executing terminal print calls directly from within a parallelized |
| worker routine.** |
| |
| **Don't:** |
| |
| ```python |
| @classmethod |
| def _DiffHelper(cls, project): |
| print(f"Project: {project.name}") |
| print(f"Revision: {project.rev}") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| @classmethod |
| def _DiffHelper(cls, project): |
| buf = io.StringIO() |
| buf.write(f"Project: {project.name}\n") |
| buf.write(f"Revision: {project.rev}\n") |
| return buf.getvalue() |
| |
| # In main process: |
| for output in results: |
| print(output, end="") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-03: Interleaved Sync Path Validation |
| |
| > **Rule:** Always validate the presence of a worktree when executing |
| > interleaved sync processes to prevent layout evaluation crashes. |
| > |
| > **What:** When executing interleaved sync processes (parallelized network |
| > fetches and checkouts), the operation must explicitly validate the presence of |
| > a worktree, as not all Git repository types (e.g., mirrors) maintain local |
| > file checkouts. |
| > |
| > **Applies To:** Concurrency logic within `sync.py`. |
| > |
| > **Why:** Changing the default mode of `repo sync` to interleaved parallelized |
| > checkout tasks indiscriminately. This immediately broke AOSP mirror syncing |
| > because mirrors lack local checkout paths, resulting in a `TypeError` when |
| > evaluating `NoneType` paths. Failing to adhere to this typically results in |
| > **TypeError / Sync Failure**. |
| |
| **Trap 1: Dispatching parallel checkout jobs across all project variants without |
| verifying layout types.** |
| |
| **Don't:** |
| |
| * Assuming `project.worktree` always contains an `os.PathLike` object during |
| interleaved syncing. |
| |
| **Do:** |
| |
| * Adding guard clauses to verify `project.worktree is not None` and handling |
| `--mirror` modes explicitly before adding checkout tasks to the thread pool. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-04: Guaranteed Synchronization State Persistence |
| |
| > **Rule:** Must execute synchronization telemetry saving operations within |
| > `try...finally` blocks to guarantee data retention across execution |
| > interrupts. |
| > |
| > **What:** Core sync operations must wrap network and filesystem operations in |
| > `try...finally` blocks, ensuring that synchronization metadata (`_fetch_times` |
| > and `_local_sync_state`) is persisted even if the sync operation fails or is |
| > interrupted. |
| > |
| > **Applies To:** `subcmds/sync.py`, particularly the `_Fetch` logic and |
| > multiprocessing worker loops. |
| > |
| > **Why:** If a synchronization process encountered an error (like a fetch |
| > failure), the operation exited early, dropping valuable telemetry and |
| > optimization data (fetch times) for projects that successfully synced prior to |
| > the crash. Failing to adhere to this typically results in **Telemetry Loss / |
| > Sync State Inconsistency**. |
| |
| **Trap 1: Placing save operations at the very end of an execution path without |
| exception guards.** |
| |
| **Don't:** |
| |
| ```python |
| result = self._Fetch(to_fetch, opt, err_event) |
| if not result.success: |
| raise SyncError("failed") |
| |
| # BAD: Never reached if an error is raised |
| self._fetch_times.Save() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| try: |
| result = self._Fetch(to_fetch, opt, err_event) |
| if not result.success: |
| raise SyncError("failed") |
| finally: |
| # GOOD: Always saves telemetry state |
| self._fetch_times.Save() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-05: Jittered Exponential Backoff for Git Configuration Locks |
| |
| > **Rule:** Always wrap concurrent Git mutations to shared configurations in an |
| > exponential backoff routine with jitter to mitigate transient filesystem |
| > locks. |
| > |
| > **What:** When initializing submodules concurrently (e.g., `git submodule |
| > init`), the system must employ an exponential backoff mechanism with |
| > randomized jitter to handle transient filesystem lock errors on `.git/config`. |
| > |
| > **Applies To:** Submodule initialization and any highly parallel Git |
| > subprocesses mutating shared configuration state. |
| > |
| > **Why:** Running `repo sync -j<N>` caused multiple child processes to |
| > simultaneously attempt `git submodule init`, which led to lock contention on |
| > the parent project's config file and caused the entire sync operation to fail. |
| > Failing to adhere to this typically results in **File Lock Contention / Sync |
| > Failure**. |
| |
| **Trap 1: Firing `git submodule init` concurrently without retry logic for |
| `config.lock` failures.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Fails immediately if .git/config.lock exists |
| subprocess.run(["git", "submodule", "init", "--", path], check=True) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Catch lock errors and retry with jitter |
| for attempt in range(MAX_RETRIES): |
| p = subprocess.run(["git", "submodule", "init", "--", path], stderr=subprocess.PIPE) |
| if p.returncode == 0: |
| break |
| if "could not lock config file" in p.stderr: |
| time.sleep(base_delay * (2 ** attempt) + random.uniform(0, jitter)) |
| ``` |
| |
| **Exceptions:** Non-lock related Git errors should still fail immediately |
| without retrying. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-06: Exponential Backoff for Concurrent Git Config Locks |
| |
| > **Rule:** Must analyze captured Git error streams to detect config lock |
| > contention and orchestrate retries via structured logging instead of immediate |
| > process failure. |
| > |
| > **What:** Concurrent git operations modifying `.git/config` (such as submodule |
| > initialization) must utilize an exponential retry mechanism with jitter to |
| > handle transient filesystem lock contention. Standard output and error streams |
| > must be captured and parsed to detect these locks. |
| > |
| > **Applies To:** project.py, specifically concurrent `git submodule init` |
| > operations or any parallel processes altering local repository configuration. |
| > |
| > **Why:** When synchronizing with high job values (-j), multiple parallel |
| > processes modifying `.git/config` triggered race conditions, throwing 'could |
| > not lock config file' errors and aborting the sync. Failing to adhere to this |
| > typically results in **Transient Sync Failure / Lock Contention**. |
| |
| **Trap 1: Failing a git command immediately without evaluating standard streams |
| for transient lock errors.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Fails immediately on lock contention |
| if GitCommand(self, cmd).Wait() != 0: |
| raise GitError(f"{self.name} submodule init failed") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Captures stdout/stderr and retries on 'lock' matches |
| git_cmd = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True) |
| if git_cmd.Wait() != 0: |
| error = git_cmd.stderr or git_cmd.stdout |
| if "lock" in error: |
| # apply exponential backoff + jitter logic |
| else: |
| git_cmd.VerifyCommand() # Propagate real error |
| ``` |
| |
| **Trap 2: Using raw `print` statements to log retry attempts, which breaks |
| structured output formats.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Unstructured print to stdout |
| print(f"Attempt {attempt+1}: git {' '.join(cmd)} failed. Sleeping...") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Forwarded to centralized logger |
| logger.warning("Attempt %d/%d: git %s failed. Error: %s. Sleeping %.2fs before retrying.", attempt+1, max_retries, cmd, error, delay) |
| ``` |
| |
| **Exceptions:** Non-lock related Git errors should break the retry loop and be |
| propagated immediately via `git_cmd.VerifyCommand()`. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-07: Hard Termination on Interleaved Sync Stalls |
| |
| > **Rule:** Must trigger a definitive execution break and flag error signals |
| > whenever an unresolvable stall is detected in an interleaved control loop. |
| > |
| > **What:** When an unresolvable stall is detected within an interleaved or |
| > parallel execution loop, the system must trigger a definitive state break |
| > (e.g., setting global events and breaking the loop) to prevent infinite |
| > deadlocks. |
| > |
| > **Applies To:** subcmds/sync.py, specifically `_SyncInterleaved` or any |
| > looping logic monitoring pending sets. |
| > |
| > **Why:** A bug existed where a stall was logged but the loop terminator |
| > (`break`) and error trigger (`err_event.set()`) were accidentally omitted. |
| > This allowed the sync routine to lock up infinitely when interdependent |
| > projects failed to checkout. Failing to adhere to this typically results in |
| > **Infinite Loop / Deadlock**. |
| |
| **Trap 1: Logging an error state inside a `while` loop but allowing the next |
| loop iteration to execute.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Omitted termination control flow |
| if prev_pending == pending_relpaths: |
| logger.error("Stall detected") |
| prev_pending = pending_relpaths |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Set event signals and immediately break |
| if prev_pending == pending_relpaths: |
| logger.error("Stall detected") |
| err_event.set() |
| break |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-08: Resource Management via Context Handlers for IPC |
| |
| > **Rule:** Always initialize multiprocessing components and background proxies |
| > within `with` statement context blocks to assure teardown semantics. |
| > |
| > **What:** Multiprocessing managers and SSH proxies initialized for parallel |
| > workflows must be wrapped in `with` block context managers to guarantee |
| > cleanup of background processes and multiplexing sockets. |
| > |
| > **Applies To:** Concurrent network operations and parallelized repository |
| > synchronization (`subcmds/sync.py`). |
| > |
| > **Why:** When adding an interleaved fetch/checkout feature, parallel |
| > operations required shared synchronization dictionaries and SSH connection |
| > multiplexing. Without context managers, exceptions or interrupts (e.g., |
| > KeyboardInterrupt) would leave orphaned background processes and abandoned |
| > sockets. Failing to adhere to this typically results in **Resource Leaks / |
| > Orphaned Sockets**. |
| |
| **Trap 1: Initializing proxy connections or multiprocessing managers as raw |
| variable assignments without a guaranteed teardown phase.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: No guaranteed cleanup on exception |
| manager = multiprocessing.Manager() |
| ssh_proxy = ssh.ProxyManager(manager) |
| process_tasks(ssh_proxy) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Context managers assure RAII semantics |
| with multiprocessing.Manager() as manager: |
| with ssh.ProxyManager(manager) as ssh_proxy: |
| process_tasks(ssh_proxy) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-09: Dual-Channel Error Handling in Parallel Processing |
| |
| > **Rule:** Must decouple fast-fail signaling events from aggregated batch data |
| > processing logic to ensure immediate worker termination across processes. |
| > |
| > **What:** In concurrent execution environments, inter-process signaling (for |
| > immediate control flow) must be strictly separated from data aggregation (for |
| > end-of-batch error reporting). |
| > |
| > **Applies To:** Multi-process synchronization pools (`_SyncInterleaved`, |
| > `_SyncPhased`) and worker callback processors. |
| > |
| > **Why:** Reviewers questioned why a separate event flag was used when an array |
| > of errors was already being populated. Using only an aggregated list prevented |
| > workers from rapidly terminating each other via fail-fast mechanisms. Failing |
| > to adhere to this typically results in **Delayed Worker Termination**. |
| |
| **Trap 1: Iterating over aggregated data lists to decide if a parallel |
| orchestration should abort.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Waiting to process the whole batch before aborting |
| errors.append(result.error) |
| if errors and opt.fail_fast: |
| pool.close() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Using an IPC Event for immediate cross-process signaling |
| err_event.set() |
| errors.append(result.error) |
| if not ret and opt.fail_fast: |
| pool.close() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-10: Minimum Process Pool Job Count Safeguard |
| |
| > **Rule:** Must bind dynamically computed multiprocessing job allocations to a |
| > minimum floor of 1 to prevent initialization crashes. |
| > |
| > **What:** When determining the number of worker processes for parallel |
| > execution based on a variable target length (e.g., an array of projects), the |
| > resulting worker count must be explicitly bound to a minimum of 1. |
| > |
| > **Applies To:** Network fetch logic and multiprocessing pool initializations |
| > (e.g., `_Fetch` in `sync.py`). |
| > |
| > **Why:** A regression occurred where passing an empty project list evaluated |
| > the worker count to 0. This crashed the multiprocessing pool initialization |
| > with a ValueError. Returning early was rejected as a fix because secondary |
| > side-effects (like state saves) within the function still needed to execute. |
| > Failing to adhere to this typically results in **ValueError / Process Pool |
| > Crash**. |
| |
| **Trap 1: Directly using `min(target, len(items))` to determine pool size, which |
| breaks if the item list is unexpectedly empty.** |
| |
| **Don't:** |
| |
| ```python |
| jobs = min(opt.jobs_network, len(projects_list)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| jobs = max(1, min(opt.jobs_network, len(projects_list))) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-11: Safeguarding Object State Across Parallel Execution Boundaries |
| |
| > **Rule:** Always transmit deeply serialized objects directly across parallel |
| > boundaries, or strictly reconstruct object relationships within the downstream |
| > worker. |
| > |
| > **What:** When offloading command execution to parallel processes, workers |
| > must receive properly serialized complex object data, not dissociated |
| > identifiers, unless the downstream API explicitly reconstructs the object |
| > state. |
| > |
| > **Applies To:** Parallel execution contexts (`ParallelContext`, |
| > `ExecuteInParallel`) within multi-project commands like `repo forall` and |
| > `repo upload`. |
| > |
| > **Why:** A refactoring attempt to optimize IPC by passing integer project |
| > indices instead of full project objects to parallel workers caused critical |
| > breakage. Worker processes subsequently attempted to access attributes (like |
| > `.manifest`) on integer types, leading to unhandled exceptions. Failing to |
| > adhere to this typically results in **AttributeError / Early Exit |
| > Regression**. |
| |
| **Trap 1: Truncating object payloads to scalar indices during IPC context setup |
| without updating the receiver logic to rehydrate the objects.** |
| |
| **Don't:** |
| |
| ```python |
| # Worker receives an integer index instead of a Project |
| manifests = { |
| project.manifest.topdir: project.manifest |
| } |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Worker receives fully serialized Project objects (or properly reconstructs them) |
| manifests = { |
| project.manifest.topdir: project.manifest |
| } |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-12: Explicit Context Initialization for Multiprocessing Pools |
| |
| > **Rule:** Must dictate shared states through an explicit `initializer` |
| > argument injected into the multiprocessing pool rather than implicitly relying |
| > on execution fork behavior. |
| > |
| > **What:** Shared parallel context must be managed using an explicit context |
| > manager and passed via `initializer` and `initargs` in `multiprocessing.Pool`, |
| > rather than relying on unvalidated class attributes or fork memory semantics. |
| > |
| > **Applies To:** Any command or module utilizing `multiprocessing.Pool` (e.g., |
| > `ExecuteInParallel`). |
| > |
| > **Why:** Relying on `fork` memory semantics for sharing context works on some |
| > Linux systems but fails deterministically on environments (like macOS/Windows) |
| > where memory is not automatically shared or when the multiprocessing start |
| > method defaults to `spawn`. Failing to adhere to this typically results in |
| > **State Leakage / Uninitialized Context**. |
| |
| **Trap 1: Assuming child workers inherit updated class variables inherently |
| without explicit initialization.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Implicit state sharing |
| cls.parallel_context = data |
| with multiprocessing.Pool(jobs) as pool: |
| pool.imap_unordered(func, inputs) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Explicit initializer passed to workers |
| with multiprocessing.Pool( |
| jobs, |
| initializer=cls._SetParallelContext, |
| initargs=(cls._parallel_context,) |
| ) as pool: |
| pool.imap_unordered(func, inputs) |
| ``` |
| |
| **Trap 2: A context manager directly yielding an internal dictionary, allowing |
| unvalidated and lingering usage.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Direct yield of state without lifecycle validation |
| @contextlib.contextmanager |
| def ParallelContext(cls): |
| yield cls._parallel_context |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Lifecycle management and defensive assertions |
| @contextlib.contextmanager |
| def ParallelContext(cls): |
| assert cls._parallel_context is None |
| cls._parallel_context = {} |
| try: |
| yield |
| finally: |
| cls._parallel_context = None |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-13: Dynamic Task Chunk Sizing in Parallel Execution |
| |
| > **Rule:** Always formulate the `chunksize` of mapping workers dynamically |
| > based on the dataset to job ratio to avoid workload serialization blocks. |
| > |
| > **What:** When utilizing `multiprocessing.Pool.imap_unordered`, the |
| > `chunksize` must be dynamically calculated based on the ratio of inputs to |
| > worker jobs, rather than statically hardcoded. |
| > |
| > **Applies To:** Parallel task dispatch mechanisms (`ExecuteInParallel`, |
| > synchronization loops). |
| > |
| > **Why:** Using a statically hardcoded chunk size (e.g., `WORKER_BATCH_SIZE = |
| > 32`) forced all tasks onto a single worker thread when the total number of |
| > projects in the manifest was fewer than 32, completely negating the benefit of |
| > parallelism for smaller workloads. Failing to adhere to this typically results |
| > in **Thread Serialization / Under-utilization**. |
| |
| **Trap 1: Passing a hardcoded batch integer to the `chunksize` parameter.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Hardcoded chunksize leads to serialization for small input lists |
| submit(func, inputs, chunksize=WORKER_BATCH_SIZE) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Dynamic chunking balances load across workers |
| calc_chunk = min(max(1, len(inputs) // jobs), WORKER_BATCH_SIZE) |
| submit(func, inputs, chunksize=calc_chunk) |
| ``` |
| |
| **Exceptions:** When evaluating specifically within tests that forcefully |
| isolate to a single job execution (`jobs=1`). |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T1-14: Deferred Worktree Operations in Sync Local Half |
| |
| > **Rule:** Must enqueue manifest copy and link actions into the synchronization |
| > buffer explicitly prior to issuing early return signals for up-to-date |
| > repositories. |
| > |
| > **What:** File link (`linkfile`) and copy (`copyfile`) directives must be |
| > scheduled using the synchronization buffer (`syncbuf.later1`) within the |
| > `Sync_LocalHalf` execution phase. They must not be bypassed if the repository |
| > exits its sync early due to being up-to-date. |
| > |
| > **Applies To:** Local synchronization logic of projects (`project.py` -> |
| > `Sync_LocalHalf`). |
| > |
| > **Why:** A bug existed where if a project had published commits in Gerrit |
| > (meaning its local state was merged), the sync process would return early and |
| > entirely skip applying the manifest's `linkfile` and `copyfile` operations, |
| > leading to missing or stale files in the developer's worktree. Failing to |
| > adhere to this typically results in **Worktree Desync / Missing Files**. |
| |
| **Trap 1: Returning early from the sync phase without applying required file |
| operations.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Skipped file modifications when commits are published |
| if pub == head: |
| return |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Schedule file operations via syncbuf before returning |
| if pub == head: |
| syncbuf.later1(self, _doff, not verbose) |
| return |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T3 | Subprocess Git Integration & Error Translation - |
| *Standardizes Git command stream capturing and exception typing, enabling |
| reliable lock contention detection.* |
| * **Downstream:** T2 | Filesystem Atomicity & Worktree Layout - *Relies on |
| parallel execution guard clauses to ensure worktrees exist before checkout |
| operations are dispatched.* |
| * **Downstream:** T6 | CLI Argument Parsing & UX Consistency - *Requires |
| buffered standard outputs and structured logger directives from parallel |
| tasks to maintain coherent terminal formatting.* |
| |
| ## Chapter: Filesystem Atomicity & Worktree Layout |
| |
| **Context:** This domain governs the deterministic creation, migration, and |
| cleanup of internal repository structures and Git worktrees. It relies on |
| ephemeral temporary directories, atomic rename operations, and robust error |
| recovery to prevent corrupted states during unexpected interruptions. |
| |
| ### Summary |
| |
| | Rule ID | Principle / Constraint | Priority | Primary Symptom / | |
| : : : : Trap : |
| | :-------- | :---------------------------- | :------- | :-------------------- | |
| | **T2-01** | Unique Subproject Keys in | High | Using the git | |
| : : Worktree Environments : : directory or : |
| : : : : repository name as a : |
| : : : : unique hash key for : |
| : : : : deduplicating project : |
| : : : : objects. : |
| | **T2-02** | Non-Destructive Bottom-Up | High | Using aggressive | |
| : : Directory Pruning : : recursive delete : |
| : : : : calls to wipe an old : |
| : : : : link destination : |
| : : : : without verifying its : |
| : : : : internal contents. : |
| | **T2-03** | Guaranteed Cleanup of | Critical | Running destructive | |
| : : Temporary Git Artifacts : : or state-modifying : |
| : : : : operations without : |
| : : : : wrapping the cleanup : |
| : : : : routine in a : |
| : : : : `finally` block. : |
| | **T2-04** | Atomic Directory | High | Initializing a | |
| : : Initialization via Temporary : : complex directory : |
| : : Worktrees : : structure directly at : |
| : : : : its final target path : |
| : : : : without atomicity : |
| : : : : guarantees. : |
| | **T2-05** | Pathlib Adoption over os.path | Medium | Relying on heavily | |
| : : : : nested string-based : |
| : : : : os.path : |
| : : : : constructions. : |
| | **T2-06** | Atomic Directory | Critical | Creating and mutating | |
| : : Initialization via Temporary : : a system directory at : |
| : : Renames : : its final, visible : |
| : : : : destination. : |
| | **T2-07** | Encapsulation of Transient | High | Swapping out instance | |
| : : Configuration State : : configuration : |
| : : : : variables back and : |
| : : : : forth during setup. : |
| | **T2-08** | Explicit Temporary Resource | Critical | Keeping a stale | |
| : : Ownership and Release : : variable reference to : |
| : : : : a temporary directory : |
| : : : : after it has been : |
| : : : : renamed, allowing a : |
| : : : : deferred cleanup : |
| : : : : routine to operate on : |
| : : : : an unowned path. : |
| | **T2-09** | Configuration Object | High | Using a temporary | |
| : : Invalidation Post-Rename : : config object to set : |
| : : : : flags after the : |
| : : : : underlying directory : |
| : : : : has been renamed. : |
| | **T2-10** | Granular Safety Checks for | Critical | Deleting a shared | |
| : : Destructive Worktree : : object directory : |
| : : Operations : : immediately upon : |
| : : : : deleting a single : |
| : : : : project that uses it. : |
| | **T2-11** | Explicit Warnings for Legacy | Critical | Silently executing a | |
| : : Garbage Collection Fallbacks : : legacy fallback for : |
| : : : : repository safety : |
| : : : : configurations : |
| : : : : without informing the : |
| : : : : user of the potential : |
| : : : : unreliability. : |
| | **T2-12** | Atomic Commits for Filesystem | High | Bundling filesystem | |
| : : Restructuring : : path migrations, : |
| : : : : structural : |
| : : : : re-designs, and : |
| : : : : initialization logic : |
| : : : : changes into a single : |
| : : : : Pull : |
| : : : : Request/Patchset. : |
| | **T2-13** | Precise Symlink Target | High | Using a generic | |
| : : Replacement During Submodule : : string `.replace()` : |
| : : Migration : : to update a portion : |
| : : : : of a file path. : |
| | **T2-14** | Isolating Filesystem | High | Executing internal | |
| : : Migrations from Network-Only : : directory migrations : |
| : : Operations : : globally prior to : |
| : : : : downloading network : |
| : : : : changes. : |
| | **T2-15** | Idempotent Cross-Platform | High | Relying on standard | |
| : : File Cleanup : : library file removal : |
| : : : : and failing to handle : |
| : : : : the scenario where : |
| : : : : the file no longer : |
| : : : : exists. : |
| | **T2-16** | Self-Healing Binary JSON | High | Opening JSON state | |
| : : Deserialization : : files in text mode : |
| : : : : without validating : |
| : : : : structure, allowing : |
| : : : : exceptions to bubble : |
| : : : : up and break the CLI. : |
| | **T2-17** | Atomic File Writes for | Critical | Using the standard | |
| : : Repository Configurations : : python `open()` : |
| : : : : context manager and : |
| : : : : `print()` to mutate : |
| : : : : core git directories. : |
| | **T2-18** | File Descriptor Lock Scope | Medium | Processing strings | |
| : : Minimization : : and building : |
| : : : : directory paths : |
| : : : : inside the file : |
| : : : : reading scope. : |
| | **T2-19** | Two-Phase Atomic Deletion for | High | Directly triggering | |
| : : Worktrees : : `rmtree` on a live : |
| : : : : repository path. : |
| | **T2-20** | Conditional Absolute Path | High | Blindly deleting and | |
| : : Resolution for Git Worktrees : : rewriting the : |
| : : : : `gitdir` file using : |
| : : : : `os.path.relpath` : |
| : : : : without checking if : |
| : : : : the source path is : |
| : : : : absolute first. : |
| | **T2-21** | Independent Manifest | High | Defaulting to a | |
| : : Repository Shallow Cloning : : shallow clone for : |
| : : : : configuration : |
| : : : : repositories, causing : |
| : : : : subsequent : |
| : : : : initialization/branch : |
| : : : : switching commands to : |
| : : : : fail due to missing : |
| : : : : objects. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T2-01: Unique Subproject Keys in Worktree Environments |
| |
| > **Rule:** Always use repository-relative paths instead of git directory names |
| > as unique deduplication keys to prevent collisions in worktree environments. |
| > |
| > **What:** Repository-relative paths must be used instead of git directory |
| > names as unique deduplication keys to prevent collisions in environments |
| > utilizing git worktrees. |
| > |
| > **Applies To:** Subproject iteration and deduplication logic, primarily within |
| > `command.py` or project discovery utilities. |
| > |
| > **Why:** When tracking derived subprojects, the system originally used |
| > `gitdir` as a unique identifier. In git worktree setups, multiple distinct |
| > project instances can share the same underlying `gitdir`, leading to silent |
| > data collisions and missed projects during sync operations. Failing to adhere |
| > to this typically results in **Data Collision / Omission**. |
| |
| **Trap 1: Using the git directory or repository name as a unique hash key for |
| deduplicating project objects.** |
| |
| **Don't:** |
| |
| ```python |
| derived_projects.update( |
| (p.gitdir, p) for p in project.GetDerivedSubprojects() |
| ) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| derived_projects.update( |
| (p.RelPath(local=False), p) for p in project.GetDerivedSubprojects() |
| ) |
| ``` |
| |
| #### T2-02: Non-Destructive Bottom-Up Directory Pruning |
| |
| > **Rule:** Always utilize non-destructive, bottom-up directory iteration that |
| > exclusively targets empty directories and symlinks during legacy path cleanup. |
| > |
| > **What:** Legacy path cleanup must utilize non-destructive, bottom-up |
| > directory iteration that specifically targets empty directories and symlinks, |
| > avoiding recursive tree destruction. |
| > |
| > **Applies To:** Workspace migration, obsolete symlink cleanup, and |
| > `platform_utils` file deletion wrappers. |
| > |
| > **Why:** When a subproject's destination directory changed, the |
| > synchronization tool utilized aggressive recursive deletion (`rmtree`) to |
| > remove the previous location. This routinely endangered untracked developer |
| > code or extraneous files housed inside the obsolete tree. Failing to adhere to |
| > this typically results in **Accidental Data Deletion**. |
| |
| **Trap 1: Using aggressive recursive delete calls to wipe an old link |
| destination without verifying its internal contents.** |
| |
| **Don't:** |
| |
| ```python |
| if platform_utils.isdir(absDest): |
| platform_utils.rmtree(absDest) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Use a safe helper that only removes empty dirs/symlinks bottom-up |
| platform_utils.removedirs(absDest) |
| ``` |
| |
| **Trap 2: Attempting directory deletion without explicitly pruning stale |
| manifest files first.** |
| |
| **Don't:** |
| |
| ```python |
| platform_utils.removedirs(need_remove_path) |
| # Misses files entirely as removedirs stops at regular files |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if os.path.isfile(need_remove_path) or os.path.islink(need_remove_path): |
| platform_utils.remove(need_remove_path) |
| platform_utils.removedirs(os.path.dirname(need_remove_path)) |
| ``` |
| |
| **Exceptions:** Explicitly defined cases where a directory is formally tracked |
| and deleted from the manifest metadata, explicitly commanding removal. |
| |
| #### T2-03: Guaranteed Cleanup of Temporary Git Artifacts |
| |
| > **Rule:** Must wrap any operation mutating the local `.git` repository with |
| > temporary states inside a `try/finally` block to guarantee restoration. |
| > |
| > **What:** Operations mutating the local `.git` repository with temporary |
| > states (e.g., tracking alt refs during fetch) must wrap the operation in a |
| > `try/finally` block to guarantee state restoration. |
| > |
| > **Applies To:** Subprocess network operations and filesystem interactions |
| > affecting the `.git` database. |
| > |
| > **Why:** If a network operation threw an exception (like a `GitAuthError` |
| > prompting for credentials), the cleanup logic was bypassed. This left orphaned |
| > references in the object database and corrupted future repository syncs. |
| > Failing to adhere to this typically results in **Stale Repository State**. |
| |
| **Trap 1: Running destructive or state-modifying operations without wrapping the |
| cleanup routine in a `finally` block.** |
| |
| **Don't:** |
| |
| ```python |
| setup_temporary_refs() |
| run_git_fetch() # Might throw GitAuthError |
| cleanup_temporary_refs() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| setup_temporary_refs() |
| try: |
| run_git_fetch() |
| finally: |
| cleanup_temporary_refs() |
| ``` |
| |
| #### T2-04: Atomic Directory Initialization via Temporary Worktrees |
| |
| > **Rule:** Always perform directory initialization via temporary directories |
| > and an atomic rename to prevent fragmented repository states. |
| > |
| > **What:** Directory initialization processes must use temporary directories |
| > and atomic renames to prevent partial or corrupted states upon unexpected |
| > interruption. |
| > |
| > **Applies To:** Filesystem operations involving git repository setup, |
| > specifically superproject initialization and dot-git dir creation. |
| > |
| > **Why:** Historically, initializing git directories directly in their final |
| > path left behind broken, partially-initialized structures if the user or |
| > system interrupted the process. This caused subsequent workflow executions to |
| > fail permanently. Failing to adhere to this typically results in **Corrupt |
| > State / Initialization Failure**. |
| |
| **Trap 1: Initializing a complex directory structure directly at its final |
| target path without atomicity guarantees.** |
| |
| **Don't:** |
| |
| ```python |
| os.makedirs(final_git_dir, exist_ok=True) |
| subprocess.run(['git', 'init', final_git_dir]) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| temp_dir = tempfile.mkdtemp(dir=target_base) |
| try: |
| subprocess.run(['git', 'init', temp_dir]) |
| platform_utils.rename(temp_dir, final_git_dir) |
| finally: |
| platform_utils.rmtree(temp_dir, ignore_errors=True) |
| ``` |
| |
| #### T2-05: Pathlib Adoption over os.path |
| |
| > **Rule:** Avoid legacy `os.path` APIs; prefer modern `pathlib.Path` structures |
| > for traversing, joining, and creating files. |
| > |
| > **What:** Use modern Python `pathlib.Path` paradigms for traversing, joining, |
| > and creating files instead of the legacy `os.path` APIs. |
| > |
| > **Applies To:** Filesystem and worktree directory manipulation. |
| > |
| > **Why:** Chaining `os.path.join` and calling `os.mkdir` repeatedly bloated the |
| > codebase, was less readable, and invited platform separator path bugs. Failing |
| > to adhere to this typically results in **Pathing Errors / High Complexity**. |
| |
| **Trap 1: Relying on heavily nested string-based os.path constructions.** |
| |
| **Don't:** |
| |
| ```python |
| manifest_dir = os.path.join(self.repodir, 'manifests') |
| os.mkdir(manifest_dir) |
| with open(os.path.join(manifest_dir, 'config'), 'w') as fp: |
| fp.write(data) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| manifest_dir = self.repodir / 'manifests' |
| manifest_dir.mkdir(parents=True, exist_ok=True) |
| (manifest_dir / 'config').write_text(data) |
| ``` |
| |
| **Exceptions:** Legacy modules pending modernization where intermingling |
| `os.path` strings and `Path` objects would break typing contracts. |
| |
| #### T2-06: Atomic Directory Initialization via Temporary Renames |
| |
| > **Rule:** Never construct complex internal `.git` directories in-situ; execute |
| > initialization inside an ephemeral directory and atomically rename it upon |
| > completion. |
| > |
| > **What:** Complex directory structures (like internal `.git` directories) must |
| > be constructed inside an ephemeral temporary directory on the same filesystem |
| > volume, and only moved to their final path via an atomic rename once fully |
| > validated. |
| > |
| > **Applies To:** Filesystem creation (`os.makedirs`) and repository |
| > initialization (`_InitGitDir`). |
| > |
| > **Why:** Interrupted or failed operations left partially initialized `.git` |
| > directories in-situ. Follow-up operations encountered corrupt directory trees, |
| > which the legacy recovery logic could not reliably clean up. Failing to adhere |
| > to this typically results in **Corrupt Repository State**. |
| |
| **Trap 1: Creating and mutating a system directory at its final, visible |
| destination.** |
| |
| **Don't:** |
| |
| ```python |
| os.makedirs(self.gitdir) |
| self._ReferenceGitDir(self.objdir, self.gitdir) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| tmp_gitdir = create_tmp_dir(os.path.dirname(self.gitdir)) |
| os.makedirs(tmp_gitdir) |
| self._ReferenceGitDir(self.objdir, tmp_gitdir) |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| ``` |
| |
| #### T2-07: Encapsulation of Transient Configuration State |
| |
| > **Rule:** Never reassign class-level instance attributes to intermediate paths |
| > during object setup; use local variables for temporary state tracking. |
| > |
| > **What:** Never temporarily reassign class-level instance attributes (e.g., |
| > `self.config`) to point to intermediate or ephemeral paths. Use local |
| > variables for temporary objects during setup, assigning the final result to |
| > the class instance only upon success. |
| > |
| > **Applies To:** Object initialization workflows within `project.py`. |
| > |
| > **Why:** During atomic `.git` directory initialization, `self.config` was |
| > temporarily overwritten to point to a temporary working directory. If the |
| > operation crashed, the object was left in an invalid state, pointing to a |
| > deleted transient path. Failing to adhere to this typically results in |
| > **Corrupt Instance State**. |
| |
| **Trap 1: Swapping out instance configuration variables back and forth during |
| setup.** |
| |
| **Don't:** |
| |
| ```python |
| self.config = GitConfig.ForRepository(gitdir=tmp_gitdir) |
| # initialization steps |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| self.config = GitConfig.ForRepository(gitdir=self.gitdir) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| tmp_config = GitConfig.ForRepository(gitdir=tmp_gitdir) |
| # apply setup using tmp_config |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| self.config = GitConfig.ForRepository(gitdir=self.gitdir) |
| ``` |
| |
| #### T2-08: Explicit Temporary Resource Ownership and Release |
| |
| > **Rule:** Always nullify references to a temporary directory immediately after |
| > successfully renaming it to its permanent destination. |
| > |
| > **What:** When executing atomic directory initializations via temporary paths, |
| > the reference to the temporary directory must be explicitly nullified |
| > immediately after successfully renaming it to its final destination. |
| > |
| > **Applies To:** Filesystem operations, specifically in `project.py` atomic Git |
| > directory initializations (`_InitGitDir`). |
| > |
| > **Why:** If a temporary directory reference was maintained after being |
| > atomic-renamed, the subsequent error-handling or `finally` cleanup block could |
| > mistakenly delete the directory. In parallel environments, another concurrent |
| > job could validly create a new temporary directory at that exact freed path, |
| > causing the current process to inadvertently nuke an active resource belonging |
| > to another job. Failing to adhere to this typically results in **Data Loss / |
| > Race Condition**. |
| |
| **Trap 1: Keeping a stale variable reference to a temporary directory after it |
| has been renamed, allowing a deferred cleanup routine to operate on an unowned |
| path.** |
| |
| **Don't:** |
| |
| ```python |
| try: |
| tmp_gitdir = tempfile.mkdtemp() |
| # ... initialize ... |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| # BAD: tmp_gitdir still holds the path string |
| finally: |
| if tmp_gitdir and os.path.exists(tmp_gitdir): |
| platform_utils.rmtree(tmp_gitdir) # Risk of race condition |
| ``` |
| |
| **Do:** |
| |
| ```python |
| try: |
| tmp_gitdir = tempfile.mkdtemp() |
| # ... initialize ... |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| tmp_gitdir = None # GOOD: explicitly release ownership of the path |
| finally: |
| if tmp_gitdir and os.path.exists(tmp_gitdir): |
| platform_utils.rmtree(tmp_gitdir) |
| ``` |
| |
| #### T2-09: Configuration Object Invalidation Post-Rename |
| |
| > **Rule:** Must discard intermediate configuration instances bound to temporary |
| > paths once the underlying directory is atomic-renamed. |
| > |
| > **What:** Configuration instances (e.g., `GitConfig`) bound to a temporary |
| > filesystem path must be abandoned after the path is atomic-renamed. A new |
| > instance tracking the permanent directory path must be used for subsequent |
| > settings. |
| > |
| > **Applies To:** Git config lifecycle management across atomic filesystem |
| > boundary events. |
| > |
| > **Why:** Applying configuration changes like `gc.pruneExpire` using an object |
| > instantiated against a temporary path resulted in IO errors or lost state if |
| > the operations occurred after the directory was moved to its final, permanent |
| > location. Failing to adhere to this typically results in **Lost Configuration |
| > / IO Error**. |
| |
| **Trap 1: Using a temporary config object to set flags after the underlying |
| directory has been renamed.** |
| |
| **Don't:** |
| |
| ```python |
| curr_config = GitConfig.ForRepository(gitdir=tmp_gitdir) |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| curr_config.SetString("gc.pruneExpire", "never") # BAD: Path no longer valid |
| ``` |
| |
| **Do:** |
| |
| ```python |
| curr_config = GitConfig.ForRepository(gitdir=tmp_gitdir) |
| platform_utils.rename(tmp_gitdir, self.gitdir) |
| self.config.SetString("gc.pruneExpire", "never") # GOOD: Using permanent config tracking self.gitdir |
| ``` |
| |
| #### T2-10: Granular Safety Checks for Destructive Worktree Operations |
| |
| > **Rule:** Must evaluate explicit dependency subsets and prompt overrides |
| > before wiping shared object directories. |
| > |
| > **What:** Destructive operations (like wiping directories) must separately |
| > evaluate and prompt overrides for distinct risk categories (uncommitted local |
| > changes vs. shared object directories), and only delete a shared object |
| > directory if all dependent projects are successfully wiped. |
| > |
| > **Applies To:** Commands modifying or deleting project directories and |
| > `.repo/project-objects`. |
| > |
| > **Why:** Deleting shared `.repo` state indiscriminately could leave other |
| > referencing projects broken if they relied on the same object directory, |
| > requiring strict subset checks before `rmtree` execution. Failing to adhere to |
| > this typically results in **Repository Corruption / Data Loss**. |
| |
| **Trap 1: Deleting a shared object directory immediately upon deleting a single |
| project that uses it.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Deletes shared objdir, breaking other projects |
| project.DeleteWorktree(force=True) |
| if os.path.exists(project.objdir): |
| platform_utils.rmtree(project.objdir) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Verify all users of the objdir are deleted first |
| project.DeleteWorktree(force=True) |
| successful_wipes.add(project.relpath) |
| |
| # Later... |
| if users.issubset(successful_wipes): |
| platform_utils.rmtree(objdir) |
| ``` |
| |
| #### T2-11: Explicit Warnings for Legacy Garbage Collection Fallbacks |
| |
| > **Rule:** Must emit a visible warning to `sys.stderr` when disabling Git |
| > garbage collection via legacy fallback configurations. |
| > |
| > **What:** When disabling Git garbage collection for shared network |
| > repositories using legacy mechanisms (`gc.pruneExpire=never`), the tooling |
| > must emit a visible `sys.stderr` warning due to the unreliability of older Git |
| > clients. |
| > |
| > **Applies To:** Git configuration manipulation and local filesystem management |
| > (`_GCProjects`). |
| > |
| > **Why:** Older Git clients (pre 2.7.0) do not natively support the |
| > `extensions.preciousObjects` flag. To prevent garbage collection from |
| > corrupting shared objects, `repo` forces `gc.pruneExpire` to `never`. Because |
| > this is imperfect, it can lead to unreliable repository states, requiring |
| > explicit user notification. Failing to adhere to this typically results in |
| > **Data Loss / Corruption**. |
| |
| **Trap 1: Silently executing a legacy fallback for repository safety |
| configurations without informing the user of the potential unreliability.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Silent fallback to legacy GC directive |
| if git_require((2, 7, 0)): |
| project.config.SetString('extensions.preciousObjects', 'true') |
| else: |
| project.config.SetString('gc.pruneExpire', 'never') |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Alerting the user to the legacy system risk |
| if git_require((2, 7, 0)): |
| project.config.SetString('extensions.preciousObjects', 'true') |
| else: |
| print('WARNING: shared projects are unreliable when using old versions of git...', file=sys.stderr) |
| project.config.SetString('gc.pruneExpire', 'never') |
| ``` |
| |
| #### T2-12: Atomic Commits for Filesystem Restructuring |
| |
| > **Rule:** Always strictly isolate structural filesystem modifications and |
| > migrations into single atomic commits to maintain bisectability. |
| > |
| > **What:** Refactoring critical filesystem paths (like migrating internal |
| > `.git` submodule structures) must be strictly isolated into atomic commits to |
| > preserve bisectability. |
| > |
| > **Applies To:** Worktree layout updates, `.git` metadata migrations, and |
| > submodule structure changes. |
| > |
| > **Why:** A monolithic commit attempted to restructure how submodules were |
| > stored (from `subprojects/` to `modules/`), update initialization logic, and |
| > alter fetching. This density made code review difficult and created a high |
| > risk of regressions that would be impossible to bisect cleanly. Failing to |
| > adhere to this typically results in **Unbisectable Regressions**. |
| |
| **Trap 1: Bundling filesystem path migrations, structural re-designs, and |
| initialization logic changes into a single Pull Request/Patchset.** |
| |
| **Don't:** |
| |
| * Submit 1 patchset: [Refactor] Move submodules to modules/, update git config |
| flags, and modify sync --init behaviors. |
| |
| **Do:** |
| |
| * Submit 3 sequential patchsets: 1) Isolate init flag changes. 2) Migrate |
| filesystem directories from `subprojects` to `modules`. 3) Enable Git |
| recurse submodules integration. |
| |
| #### T2-13: Precise Symlink Target Replacement During Submodule Migration |
| |
| > **Rule:** Always use explicit path prefixes instead of generic substring |
| > replacements to update symlink targets during legacy layout migrations. |
| > |
| > **What:** Symlink updates during filesystem layout migrations must use exact |
| > path prefixes rather than global substring replacements to update relative |
| > targets safely. |
| > |
| > **Applies To:** Repository migration logic (`project.py`); specifically |
| > `_MigrateSubprojectLinks` or similar symlink target modifications. |
| > |
| > **Why:** During the migration of legacy submodule directories, using generic |
| > string replacement (`target.replace`) on symlink paths unintentionally |
| > corrupted paths if a user's directory structure legitimately contained the |
| > target string (e.g., `subproject-objects`) in a higher-level parent directory |
| > name. Failing to adhere to this typically results in **Broken Symlinks / |
| > Corruption**. |
| |
| **Trap 1: Using a generic string `.replace()` to update a portion of a file |
| path.** |
| |
| **Don't:** |
| |
| ```python |
| if "subproject-objects" in target: |
| new_target = target.replace( |
| "subproject-objects", "module-objects" |
| ) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if target.startswith("../../subproject-objects/"): |
| new_target = target.replace( |
| "../../subproject-objects/", "../../module-objects/" |
| ) |
| ``` |
| |
| #### T2-14: Isolating Filesystem Migrations from Network-Only Operations |
| |
| > **Rule:** Never execute internal repository layout migrations as part of |
| > network fetch iterations. |
| > |
| > **What:** Internal repository layout migrations must only execute during local |
| > worktree initialization phases, never during network fetch iterations. |
| > |
| > **Applies To:** Repository sync operations (`subcmds/sync.py`) and worktree |
| > checkout logic (`project.py`). |
| > |
| > **Why:** Binding irreversible filesystem layout migrations to the `sync` |
| > command's network-fetch loop risked partially migrating the repository if the |
| > process was interrupted, or if a user requested a network-only sync, leaving |
| > the local workspace out of sync with internal references. Failing to adhere to |
| > this typically results in **Repository Corruption**. |
| |
| **Trap 1: Executing internal directory migrations globally prior to downloading |
| network changes.** |
| |
| **Don't:** |
| |
| ```python |
| # In subcmds/sync.py during network fetching |
| for p in all_projects: |
| p._MigrateOldSubprojectDirs() |
| # ... fetch logic |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Inside project.py during _InitWorkTree() or checkout |
| def _InitWorkTree(self): |
| self._MigrateOldSubmoduleDirs() |
| # ... symlink creation and checkout logic |
| ``` |
| |
| #### T2-15: Idempotent Cross-Platform File Cleanup |
| |
| > **Rule:** Must employ `platform_utils.remove()` for file cleanup to safely |
| > absorb OS access violations and gracefully swallow `ENOENT`. |
| > |
| > **What:** File cleanup operations must use the internal |
| > `platform_utils.remove()` instead of the standard `os.remove()` to handle |
| > Windows-specific OS limitations. Furthermore, cleanup must be idempotent by |
| > explicitly catching and swallowing `errno.ENOENT` errors. |
| > |
| > **Applies To:** Local worktree operations, internal state file cleanup (e.g., |
| > deleting obsolete copyfiles or linkfiles). |
| > |
| > **Why:** Sync operations would abruptly crash on Windows due to unhandled |
| > `EACCES` issues on read-only files/symlinks with native `os.remove()`. |
| > Furthermore, if a tracking file was already deleted manually by the user or a |
| > prior interrupted run, unhandled `ENOENT` exceptions would needlessly fail the |
| > operation. Failing to adhere to this typically results in **Crash on |
| > Missing/Locked File**. |
| |
| **Trap 1: Relying on standard library file removal and failing to handle the |
| scenario where the file no longer exists.** |
| |
| **Don't:** |
| |
| ```python |
| if path: |
| try: |
| os.remove(path) |
| except OSError as error: |
| print(f'error: remove {path} failed.') |
| return 1 |
| ``` |
| |
| **Do:** |
| |
| ```python |
| try: |
| platform_utils.remove(path) |
| except OSError as e: |
| if e.errno == errno.ENOENT: |
| # File does not exist, safe to ignore |
| pass |
| ``` |
| |
| #### T2-16: Self-Healing Binary JSON Deserialization |
| |
| > **Rule:** Always read internal JSON state files in binary mode (`rb`) and |
| > implement explicit error catching to seamlessly discard corrupted states. |
| > |
| > **What:** Internal JSON tracking files must be read in binary mode (`rb`) to |
| > allow the JSON parser to detect text encoding safely. Additionally, parsing |
| > must be wrapped in a `try/except` block to automatically delete the file and |
| > recover if the JSON is corrupted. |
| > |
| > **Applies To:** Manifest metadata tracking files, internal cache parsing |
| > (e.g., `copy-link-files.json`). |
| > |
| > **Why:** When the operating system's default text encoding did not match the |
| > file's encoding, a `UnicodeDecodeError` could occur. Additionally, if the JSON |
| > state file was corrupted (e.g., due to an abrupt power loss or killed |
| > process), `repo sync` would permanently fail until the user manually |
| > discovered and deleted the corrupted state file. Failing to adhere to this |
| > typically results in **Corrupt State Deadlock**. |
| |
| **Trap 1: Opening JSON state files in text mode without validating structure, |
| allowing exceptions to bubble up and break the CLI.** |
| |
| **Don't:** |
| |
| ```python |
| with open(copylinkfiles_path, 'r') as fd: |
| old_copylinkfiles_path = json.load(fd) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| with open(copylinkfiles_path, 'rb') as fp: |
| try: |
| old_copylinkfiles_path = json.load(fp) |
| except: |
| print('error: %s is not a json formatted file.' % copylinkfiles_path, file=sys.stderr) |
| platform_utils.remove(copylinkfiles_path) |
| return False |
| ``` |
| |
| #### T2-17: Atomic File Writes for Repository Configurations |
| |
| > **Rule:** Must utilize atomic file writes rather than standard contexts when |
| > generating core `.git` configuration files. |
| > |
| > **What:** System state modifications, specifically writing the `.git` pointer |
| > files for submodules, must be performed using an atomic write pattern (e.g., |
| > writing to a lockfile and renaming) rather than standard file descriptors. |
| > |
| > **Applies To:** All file I/O operations directly modifying `.git` contents, |
| > worktrees, or manifest definitions. |
| > |
| > **Why:** Using standard `open()` and writing to `.git` files directly leaves |
| > the repository vulnerable to corruption if the process is interrupted (e.g., |
| > via Ctrl+C or SIGTERM) during the write loop, resulting in a partially written |
| > pointer. Failing to adhere to this typically results in **Corrupted Git |
| > Worktree**. |
| |
| **Trap 1: Using the standard python `open()` context manager and `print()` to |
| mutate core git directories.** |
| |
| **Don't:** |
| |
| ```python |
| with open(dotgit, "w") as fp: |
| print(f"gitdir: {rel_path}", file=fp) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| _lwrite(dotgit, f"gitdir: {rel_path}\n") |
| ``` |
| |
| #### T2-18: File Descriptor Lock Scope Minimization |
| |
| > **Rule:** Minimize file descriptor lock retention by executing string and |
| > state logic outside of standard reading contexts. |
| > |
| > **What:** Data processing and variable manipulation must be extracted out of |
| > file descriptor lock contexts (`with open(...)`) to minimize resource |
| > retention. |
| > |
| > **Applies To:** Any block parsing local text or configuration files. |
| > |
| > **Why:** Holding file descriptors open while executing CPU-bound string |
| > manipulations increases lock contention probability and expands the attack |
| > surface for race conditions when multiple concurrent processes read/write |
| > repository states. Failing to adhere to this typically results in **Resource |
| > Lock Contention**. |
| |
| **Trap 1: Processing strings and building directory paths inside the file |
| reading scope.** |
| |
| **Don't:** |
| |
| ```python |
| with open(dotgit) as fp: |
| setting = fp.read() |
| gitdir = setting.split(":")[1].strip() |
| dotgit_path = os.path.normpath(os.path.join(self.worktree, gitdir)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| with open(dotgit) as fp: |
| setting = fp.read() |
| |
| # Lock released, safe to process |
| gitdir = setting.split(":")[1].strip() |
| dotgit_path = os.path.normpath(os.path.join(self.worktree, gitdir)) |
| ``` |
| |
| #### T2-19: Two-Phase Atomic Deletion for Worktrees |
| |
| > **Rule:** Must enact permanent `.git` and worktree removal via a two-phase |
| > destruction pattern utilizing a temporary trash path. |
| > |
| > **What:** When permanently removing a `.git` directory or worktree, the system |
| > must first perform an atomic rename to a temporary trash path before |
| > initiating the recursive deletion. |
| > |
| > **Applies To:** Garbage collection (`gc.py`), worktree cleanup, and submodule |
| > removal logic. |
| > |
| > **Why:** Recursive deletion is not instantaneous. If a user forcefully |
| > interrupted (Ctrl+C) the `rmtree` operation, the directory was left in a |
| > 'wedged' (partially deleted but still registered) state, permanently breaking |
| > future syncs for that repository. Failing to adhere to this typically results |
| > in **Wedged / Orphaned Worktree**. |
| |
| **Trap 1: Directly triggering `rmtree` on a live repository path.** |
| |
| **Don't:** |
| |
| ```python |
| for path in to_delete: |
| platform_utils.rmtree(path) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| for path in to_delete: |
| temp_path = rename_to_trash(path) |
| platform_utils.rmtree(temp_path) |
| ``` |
| |
| #### T2-20: Conditional Absolute Path Resolution for Git Worktrees |
| |
| > **Rule:** Always independently verify if a tracked worktree path is absolute |
| > before initiating internal path recreation procedures. |
| > |
| > **What:** Updates to `.git` directory reference files (e.g., `gitdir:`) must |
| > verify whether the stored worktree path is absolute before attempting to |
| > recreate it as a relative path to avoid path corruption and permissions |
| > errors. |
| > |
| > **Applies To:** Internal Git directory layout management, specifically |
| > cross-platform Git worktree initialization (`project.py`). |
| > |
| > **Why:** On certain platforms like Windows, modifying the internal `dotgit` |
| > file in situ fails due to file permissions. Code was added to delete and |
| > recreate the file using relative paths. However, depending on the Git version, |
| > the initial path could already be relative, making conversion unsafe without |
| > an `isabs()` check. Failing to adhere to this typically results in |
| > **Permission Denied / Path Corruption**. |
| |
| **Trap 1: Blindly deleting and rewriting the `gitdir` file using |
| `os.path.relpath` without checking if the source path is absolute first.** |
| |
| **Don't:** |
| |
| ```python |
| platform_utils.remove(dotgit) |
| with open(dotgit, "w", newline="\n") as fp: |
| print("gitdir:", os.path.relpath(git_worktree_path, self.worktree), file=fp) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if os.path.isabs(git_worktree_path): |
| platform_utils.remove(dotgit) |
| with open(dotgit, "w", newline="\n") as fp: |
| print("gitdir:", os.path.relpath(git_worktree_path, self.worktree), file=fp) |
| ``` |
| |
| #### T2-21: Independent Manifest Repository Shallow Cloning |
| |
| > **Rule:** Never propagate the primary manifest `--depth` flag to govern the |
| > core manifest repository's clone boundaries. |
| > |
| > **What:** The depth configuration for cloning the central manifest repository |
| > must be explicitly decoupled from the depth configuration applied to the child |
| > projects it governs, and its default must not regress existing multi-stage |
| > init workflows. |
| > |
| > **Applies To:** Repo initialization commands (`repo init`, `subcmds/init.py`). |
| > |
| > **Why:** Using the global `--depth` flag inadvertently forced shallow clones |
| > on all child projects. Introducing a dedicated `--manifest-depth` option fixed |
| > this, but defaulting it to 1 caused a regression during 'double repo init' |
| > workflows (where subsequent checkouts failed against truncated manifest |
| > histories). Failing to adhere to this typically results in **Workflow |
| > Regression / History Truncation**. |
| |
| **Trap 1: Defaulting to a shallow clone for configuration repositories, causing |
| subsequent initialization/branch switching commands to fail due to missing |
| objects.** |
| |
| **Don't:** |
| |
| ```python |
| group.add_option('--manifest-depth', type='int', default=1, metavar='DEPTH', |
| help='create a shallow clone of the manifest repo') |
| ``` |
| |
| **Do:** |
| |
| ```python |
| group.add_option('--manifest-depth', type='int', default=0, metavar='DEPTH', |
| help='create a shallow clone of the manifest repo') |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T3 | Subprocess Git Integration & Error Translation - *Network |
| operations and git process invocations require safe staging paths and |
| catchable exceptions to trigger filesystem cleanups.* |
| * **Downstream:** T1 | Concurrent Synchronization & IPC - *Parallel checkout |
| operations depend heavily on atomic path modifications and lock-free |
| directory states to prevent race conditions.* |
| * **Downstream:** T4 | Manifest Object Model & Deduplication - *Filesystem |
| migrations and subproject tree building directly parse and reflect |
| canonicalized XML manifest declarations.* |
| |
| ## Chapter: Subprocess Git Integration & Error Translation |
| |
| **Context:** This chapter defines the constraints for wrapping, executing, and |
| translating standard Git subprocesses within the Repo tooling ecosystem. It |
| mandates the use of centralized command abstractions, strict version gating, and |
| deterministic stream handling to guarantee reliable repository state management |
| and actionable error reporting. |
| |
| ### Summary |
| |
| | Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap | |
| | :-------- | :------------------------ | :------- | :------------------------ | |
| | **T3-01** | Unborn Branch Resolution | High | Catching a `rev-parse` | |
| : : via Symbolic-Ref : : GitError and immediately : |
| : : : : falling back to manual : |
| : : : : file system reads or : |
| : : : : emitting warnings. : |
| | **T3-02** | Centralized GitCommand | Medium | Wrapping `subprocess.run` | |
| : : Abstraction Usage : : manually inside a class : |
| : : : : to invoke Git. : |
| | **T3-03** | NUL-Byte Delimiters for | High | Formatting git fields | |
| : : Git Output Parsing : : with tabs and defending : |
| : : : : against length : |
| : : : : variations. : |
| | **T3-04** | Subprocess Execution | Medium | Stringifying Path | |
| : : Context and Native Path : : objects, using : |
| : : Passing : : Git-specific directory : |
| : : : : shifts, and suppressing : |
| : : : : piped output. : |
| | **T3-05** | Runtime Git Version | High | Invoking a modern git CLI | |
| : : Constraints for Promisor : : flag without ensuring : |
| : : Packs : : compatibility first. : |
| | **T3-06** | Centralized Dependency | Medium | Catching missing binary | |
| : : Availability Caching : : exceptions to terminate : |
| : : : : the process instead of : |
| : : : : returning control to the : |
| : : : : caller. : |
| | **T3-07** | Subprocess Return Code | High | Calling a non-existent | |
| : : Validation : : subprocess execution : |
| : : : : method on an already : |
| : : : : completed process object. : |
| | **T3-08** | Strict Git Version | High | Injecting convenient, | |
| : : Compatibility : : modern CLI flags into : |
| : : : : subprocess wrappers. : |
| | **T3-09** | Rebase Override for | High | Throwing a | |
| : : Published Local Branches : : `LocalSyncFail` when an : |
| : : : : upstream gain exists, : |
| : : : : completely ignoring the : |
| : : : : `force_rebase` argument. : |
| | **T3-10** | SSO Authentication | High | Treating all exit code | |
| : : Failure Translation : : 128 errors equally : |
| : : : : without sniffing stdout : |
| : : : : for authentication prompt : |
| : : : : aborts. : |
| | **T3-11** | Direct Read Fallback for | High | Raising a fatal internal | |
| : : Unborn Git HEAD : : exception immediately : |
| : : : : upon a `GitError` when : |
| : : : : checking for the HEAD : |
| : : : : reference. : |
| | **T3-12** | Safe Git Subprocess | Critical | Assuming subprocess | |
| : : Output Handling : : success and attempting to : |
| : : : : return an output variable : |
| : : : : that hasn't been : |
| : : : : initialized. : |
| | **T3-13** | Shallow Clone SHA-1 Fetch | Critical | Assuming a fetch of an | |
| : : Fallback : : arbitrary SHA-1 will : |
| : : : : unconditionally succeed : |
| : : : : if `--depth` is used. : |
| | **T3-14** | Validating Gerrit Push | High | Constructing push options | |
| : : Option Syntax : : with incorrect, : |
| : : : : unverified key strings. : |
| | **T3-15** | Submodule Initialization | Critical | Blindly initializing | |
| : : Guard Under Git Worktrees : : submodules without : |
| : : : : verifying if the : |
| : : : : underlying Git worktree : |
| : : : : feature is active. : |
| | **T3-16** | XDG Compliant Git | High | Hardcoding the user's | |
| : : Configuration Resolution : : home directory as the : |
| : : : : sole location for : |
| : : : : `.gitconfig`. : |
| | **T3-17** | Strongly-Typed Subprocess | High | Catching a subprocess | |
| : : Error Contexting : : failure and re-raising it : |
| : : : : as a generic git error : |
| : : : : with a concatenated : |
| : : : : string message. : |
| | **T3-18** | Line-Based Log Extraction | Medium | Truncating stdout to a | |
| : : for Subprocess : : fixed number of : |
| : : Diagnostics : : characters and ignoring : |
| : : : : stderr entirely. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T3-01: Unborn Branch Resolution via Symbolic-Ref |
| |
| > **Rule:** Always attempt `git symbolic-ref` before falling back to manual file |
| > parsing to correctly resolve unborn branches. |
| > |
| > **What:** When querying local Git branch structures, `git symbolic-ref` must |
| > be attempted as a fallback before file parsing to correctly identify unborn |
| > branches where `rev-parse` fails. |
| > |
| > **Applies To:** Subprocess Git integrations resolving `HEAD` or local refs. |
| > |
| > **Why:** Freshly initialized repositories and orphan branches present 'unborn' |
| > states where standard `rev-parse` commands returned errors, triggering |
| > false-positive warnings or crashing the client. Failing to adhere to this |
| > typically results in **Incorrect Branch Detection**. |
| |
| **Trap 1: Catching a `rev-parse` GitError and immediately falling back to manual |
| file system reads or emitting warnings.** |
| |
| **Don't:** |
| |
| ```python |
| try: |
| return rev_parse(HEAD) |
| except GitError: |
| logger.warning("Unparseable HEAD") |
| return parse_file(HEAD) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| try: |
| return rev_parse(HEAD) |
| except GitError: |
| try: |
| return run_git_command("symbolic-ref", "-q", HEAD) |
| except GitError: |
| pass |
| return parse_file(HEAD) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-02: Centralized GitCommand Abstraction Usage |
| |
| > **Rule:** Strictly use the internal `GitCommand` abstraction for local Git |
| > operations instead of invoking raw `subprocess.run`. |
| > |
| > **What:** Use the internal `GitCommand` abstraction for executing Git |
| > operations rather than raw `subprocess.run` to guarantee consistent tracing, |
| > environment handling, and error translations. |
| > |
| > **Applies To:** Any module interacting with local git binaries, such as |
| > `git_refs.py`. |
| > |
| > **Why:** Developers bypassing the centralized wrapper led to duplicated |
| > subprocess execution logic, loss of global tracing capabilities (e.g., |
| > `REPO_TRACE`), and fragmented error handling across the codebase. Failing to |
| > adhere to this typically results in **Missing Tracing / Environment Bugs**. |
| |
| **Trap 1: Wrapping `subprocess.run` manually inside a class to invoke Git.** |
| |
| **Don't:** |
| |
| ```python |
| def _Run(self, *cmd): |
| return subprocess.run( |
| ['git', f'--git-dir={self._gitdir}', *cmd], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.DEVNULL |
| ) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| from git_command import GitCommand |
| |
| def _Run(self, cmd): |
| p = GitCommand( |
| None, |
| cmd, |
| capture_stdout=True, |
| capture_stderr=True, |
| bare=True, |
| gitdir=self._gitdir, |
| ) |
| p.Wait() |
| return p |
| ``` |
| |
| **Exceptions:** Low-level testing utilities isolated from the core application |
| state, where bypassing `GitCommand` is necessary to bootstrap a test repository. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-03: NUL-Byte Delimiters for Git Output Parsing |
| |
| > **Rule:** Always specify NUL-byte delimiters (`%00`) when parsing Git command |
| > output to prevent splitting errors. |
| > |
| > **What:** When parsing Git formatted output, utilize the NUL byte (`%00`) |
| > instead of tabs or spaces, and leverage precise tuple unpacking without length |
| > validation. |
| > |
| > **Applies To:** Parsing output from Git commands like `git for-each-ref` or |
| > `git status`. |
| > |
| > **Why:** Using whitespace or tabs to split field data exposed the parser to |
| > failures if reference names or other entities inherently included those |
| > characters. NUL delimiters guarantee absolute safety and allow removal of |
| > defensive parsing loops. Failing to adhere to this typically results in **Data |
| > Parsing Failure**. |
| |
| **Trap 1: Formatting git fields with tabs and defending against length |
| variations.** |
| |
| **Don't:** |
| |
| ```python |
| # Using tab delimiter and checking len |
| subprocess.run(['git', 'for-each-ref', '--format=%(objectname)\t%(refname)']) |
| for line in output.splitlines(): |
| fields = line.split('\t') |
| if len(fields) < 2: |
| continue |
| ref_id, name = fields[:2] |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Using NUL delimiter and direct unpacking |
| GitCommand(..., ['for-each-ref', '--format=%(objectname)%00%(refname)%00%(symref)']) |
| for line in output.splitlines(): |
| ref_id, name, symref = line.split('\0') |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-04: Subprocess Execution Context and Native Path Passing |
| |
| > **Rule:** Pass native `pathlib.Path` objects via the `cwd` argument rather |
| > than stringifying them or utilizing Git's `-C` flag. |
| > |
| > **What:** Execute subprocesses with correct internal API paradigms: omit Git's |
| > `-C` in favor of the `cwd=` parameter, pass `pathlib.Path` objects inherently, |
| > and drop `-q` when actively capturing output. |
| > |
| > **Applies To:** Subprocess command building and execution in test scaffolding |
| > and Git interactions. |
| > |
| > **Why:** Passing `-C` combined with forced string casting of paths created |
| > redundant logic. Additionally, suppressing underlying git output via `-q` |
| > completely masked the root cause of errors captured within the internal stream |
| > buffers. Failing to adhere to this typically results in **Obscured |
| > Diagnostics**. |
| |
| **Trap 1: Stringifying Path objects, using Git-specific directory shifts, and |
| suppressing piped output.** |
| |
| **Don't:** |
| |
| ```python |
| subprocess.check_call( |
| ['git', '-C', str(git_dir), 'init', '-q'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE |
| ) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| subprocess.check_call( |
| ['git', 'init'], |
| cwd=git_dir, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE |
| ) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-05: Runtime Git Version Constraints for Promisor Packs |
| |
| > **Rule:** Explicitly verify local Git version compatibility before injecting |
| > modern CLI flags into subprocess calls. |
| > |
| > **What:** Operations utilizing modern Git arguments like |
| > `--missing=allow-promisor` must be explicitly gated by a minimal supported |
| > version check against the local executable. |
| > |
| > **Applies To:** Subprocess handlers for `git rev-list` and repacking |
| > mechanisms on partial clones. |
| > |
| > **Why:** Git 2.17.0 introduced advanced promisor arguments. Hardcoding these |
| > arguments in the execution engine crashed older repository clients on machines |
| > where Git had been downgraded or not updated. Failing to adhere to this |
| > typically results in **Subprocess Crash**. |
| |
| **Trap 1: Invoking a modern git CLI flag without ensuring compatibility first.** |
| |
| **Don't:** |
| |
| ```python |
| if opt.repack: |
| return self.repack_projects(projects, opt) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if opt.repack: |
| git_require((2, 17, 0), fail=True, msg="--repack") |
| return self.repack_projects(projects, opt) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-06: Centralized Dependency Availability Caching |
| |
| > **Rule:** Centralize and cache external dependency checks to fail gracefully |
| > without abruptly terminating the calling process. |
| > |
| > **What:** External dependency checks (e.g., verifying SSH installation) must |
| > be centralized, cached internally, and fail gracefully without forcibly |
| > exiting the calling process. |
| > |
| > **Applies To:** System API wrappers and subprocess managers (e.g., |
| > `ProxyManager`, `ssh.py`). |
| > |
| > **Why:** Deep utility functions checking for external binaries previously |
| > called `sys.exit(1)` upon `FileNotFoundError`. This abruptly terminated the |
| > execution for users in environments missing the dependency, even if their |
| > specific workflow did not actually require it. Failing to adhere to this |
| > typically results in **Unexpected Process Termination**. |
| |
| **Trap 1: Catching missing binary exceptions to terminate the process instead of |
| returning control to the caller.** |
| |
| **Don't:** |
| |
| ```python |
| def version(): |
| try: |
| return _parse_ssh_version() |
| except FileNotFoundError: |
| print("fatal: ssh not installed", file=sys.stderr) |
| sys.exit(1) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def version(): |
| try: |
| return _parse_ssh_version() |
| except FileNotFoundError as e: |
| print("warn: ssh not installed", file=sys.stderr) |
| raise e |
| ``` |
| |
| **Trap 2: Duplicating inline `try-except` blocks for missing binaries across |
| multiple execution methods rather than centralizing state.** |
| |
| **Don't:** |
| |
| ```python |
| def run_command1(self): |
| try: |
| subprocess.Popen(['ssh']) |
| except FileNotFoundError: |
| return False |
| |
| def run_command2(self): |
| try: |
| subprocess.Popen(['ssh']) |
| except FileNotFoundError: |
| return False |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def __enter__(self): |
| try: |
| version() |
| self._ssh_installed = True |
| except FileNotFoundError: |
| self._ssh_installed = False |
| return self |
| |
| def run_command(self): |
| if not self._ssh_installed: |
| return False |
| subprocess.Popen(['ssh']) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-07: Subprocess Return Code Validation |
| |
| > **Rule:** Validate subprocess success strictly using `check_returncode()` on |
| > the completed process object. |
| > |
| > **What:** When utilizing the standard Python `subprocess` library, the correct |
| > method to validate execution success on a `CompletedProcess` object is |
| > `check_returncode()`. Do not call `check_call()` on the returned object. |
| > |
| > **Applies To:** Any Python code wrapping git commands and handling |
| > `subprocess.CompletedProcess` results. |
| > |
| > **Why:** Legacy code inadvertently called `check_call()` on the result of |
| > `run_command`, which triggered a runtime `AttributeError` instead of raising |
| > the expected `subprocess.CalledProcessError` on failure. Failing to adhere to |
| > this typically results in **AttributeError**. |
| |
| **Trap 1: Calling a non-existent subprocess execution method on an already |
| completed process object.** |
| |
| **Don't:** |
| |
| ```python |
| ret = subprocess.run(cmd, capture_output=True) |
| ret.check_call() # Raises AttributeError |
| ``` |
| |
| **Do:** |
| |
| ```python |
| ret = subprocess.run(cmd, capture_output=True) |
| # This will raise subprocess.CalledProcessError for us |
| ret.check_returncode() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-08: Strict Git Version Compatibility |
| |
| > **Rule:** Never pass Git flags that are unsupported by the minimum required |
| > Git version listed in the project's manifest. |
| > |
| > **What:** Do not pass modern CLI flags to subprocess Git operations if those |
| > flags are not supported by the minimum required Git version listed in the |
| > project's dependency manifest. |
| > |
| > **Applies To:** Git subprocess string assembly in `project.py`. |
| > |
| > **Why:** The `--no-auto-gc` flag was appended to clone commands to prevent |
| > deadlocks, but this flag did not exist in Git 1.7.9, immediately breaking |
| > checkout operations for environments restricted to the minimum supported |
| > baseline. Failing to adhere to this typically results in **Unknown Option |
| > Error**. |
| |
| **Trap 1: Injecting convenient, modern CLI flags into subprocess wrappers.** |
| |
| **Don't:** |
| |
| ```python |
| cmd.append("--no-auto-gc") |
| ``` |
| |
| **Do:** |
| |
| * Disabling auto-gc via configuration setters (`self.config.SetInt("gc.auto", |
| 0)`) that are safely interpreted across legacy Git versions. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-09: Rebase Override for Published Local Branches |
| |
| > **Rule:** Allow rewriting of published local commits by respecting the |
| > `force_rebase` argument during sync operations. |
| > |
| > **What:** When a local branch is published but not yet merged upstream, `repo |
| > sync --rebase` must respect the `force_rebase` argument to allow rewriting |
| > published commits, rather than unconditionally throwing a sync failure. |
| > |
| > **Applies To:** `project.py`, specifically `_Rebase` and `_FastForward` path |
| > execution during sync. |
| > |
| > **Why:** The tool previously threw an error instructing the user to run with |
| > `--rebase`, but didn't actually execute the rebase when the flag was provided, |
| > forcing developers to manually fix the state. Failing to adhere to this |
| > typically results in **Broken UX / Unhandled Rebase State**. |
| |
| **Trap 1: Throwing a `LocalSyncFail` when an upstream gain exists, completely |
| ignoring the `force_rebase` argument.** |
| |
| **Don't:** |
| |
| ```python |
| if upstream_gain and not force_rebase: |
| # Fails even if force_rebase is TRUE due to logic gap |
| fail(LocalSyncFail("branch is published... rerun with the --rebase option")) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if upstream_gain: |
| if force_rebase: |
| syncbuf.later1(self, _dorebase, not verbose) |
| else: |
| fail(LocalSyncFail("branch is published... rerun with the --rebase option")) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-10: SSO Authentication Failure Translation |
| |
| > **Rule:** Intercept Git exit code 128 and translate it to a typed |
| > `GitAuthError` when stdout indicates an SSO session abort. |
| > |
| > **What:** Git commands that fail with exit code 128 due to an SSO session |
| > abort must be explicitly intercepted and translated into a typed |
| > `GitAuthError`, preventing them from being masked as a missing reference. |
| > |
| > **Applies To:** project.py, Git Subprocess Wrappers evaluating `gitcmd.stdout` |
| > and return codes (specifically fetch/sync operations interacting with SSO |
| > remote helpers). |
| > |
| > **Why:** In non-interactive environments, an expired SSO token caused Git to |
| > fail with a generic exit code 128. The tool misinterpreted this as a missing |
| > upstream reference, leading to confusing error logs that obscured the |
| > underlying authentication requirement. Failing to adhere to this typically |
| > results in **Ambiguous Auth Failure**. |
| |
| **Trap 1: Treating all exit code 128 errors equally without sniffing stdout for |
| authentication prompt aborts.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Ambiguous handling of 128 |
| elif current_branch_only and is_sha1 and ret == 128: |
| # Assuming missing upstream reference |
| raise GitError("Couldn't find the ref you asked for") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Explicit trap for SSO helpers |
| elif ret == 128 and gitcmd.stdout and "remote helper 'sso' aborted session" in gitcmd.stdout: |
| raise GitAuthError(gitcmd.stdout) |
| ``` |
| |
| **Exceptions:** Repositories containing multiple remotes where an auth failure |
| on a secondary remote might be non-fatal. (Currently flagged for future review |
| but translation stands). |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-11: Direct Read Fallback for Unborn Git HEAD |
| |
| > **Rule:** Implement a direct file system read of `.git/HEAD` as a fallback |
| > when standard sub-commands fail to resolve the reference. |
| > |
| > **What:** When standard Git commands fail to resolve the current HEAD (e.g., |
| > throwing a `GitError`), the system must fall back to directly reading the |
| > internal `.git/HEAD` file from the filesystem before propagating a fatal |
| > error. |
| > |
| > **Applies To:** project.py, `GetHead()` implementations traversing repository |
| > state. |
| > |
| > **Why:** Empty repositories or states featuring unborn branches caused `git |
| > rev-parse HEAD` to fail outright. Without a fallback, this cascaded into a |
| > `NoManifestException` and aborted the entire synchronization process. Falling |
| > back to the raw `.git/HEAD` text bypassed the strict ref checks. Failing to |
| > adhere to this typically results in **Hard Crash / NoManifestException**. |
| |
| **Trap 1: Raising a fatal internal exception immediately upon a `GitError` when |
| checking for the HEAD reference.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Strict reliance on git sub-commands |
| try: |
| return self.rev_parse(HEAD) |
| except GitError as e: |
| path = self.GetDotgitPath(subpath=HEAD) |
| raise NoManifestException(path, str(e)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Fallback text parsing mechanism |
| try: |
| return self.rev_parse(HEAD) |
| except GitError as e: |
| path = self.GetDotgitPath(subpath=HEAD) |
| try: |
| with open(path) as fd: |
| # Read branch manually |
| except OSError: |
| raise NoManifestException(path, str(e)) |
| ``` |
| |
| **Exceptions:** If the physical `.git/HEAD` file raises an `OSError` (meaning it |
| truly does not exist on disk), a `NoManifestException` is necessary and valid. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-12: Safe Git Subprocess Output Handling |
| |
| > **Rule:** Always validate the return code of a Git subprocess before accessing |
| > its standard output properties. |
| > |
| > **What:** Git subprocess output wrappers must explicitly handle non-zero exit |
| > codes to prevent accessing uninitialized variables, and stream properties must |
| > be spell-checked against the underlying class definitions. |
| > |
| > **Applies To:** Git subprocess wrappers (`git_superproject.py`, `GitCommand`) |
| > and any execution context capturing stdout/stderr. |
| > |
| > **Why:** When a Git command (like `git rev-parse`) failed, the code attempted |
| > to return a variable (`data`) that was only initialized on success, causing an |
| > `UnboundLocalError`. Furthermore, error logging attempts crashed due to a typo |
| > accessing `p.stdwerr` instead of `p.stderr`. Failing to adhere to this |
| > typically results in **Runtime Crash / UnboundLocalError**. |
| |
| **Trap 1: Assuming subprocess success and attempting to return an output |
| variable that hasn't been initialized.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: data is unbound if retval != 0 |
| retval = p.Wait() |
| if retval == 0: |
| data = p.stdout |
| else: |
| log_error("Failed") |
| return data |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Explicit error handling with early return |
| retval = p.Wait() |
| if retval != 0: |
| log_error(f"Failed: {p.stderr}") |
| return None |
| return p.stdout |
| ``` |
| |
| **Trap 2: Typo in the attribute name for capturing standard error from the |
| `GitCommand` class.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Invalid attribute triggers AttributeError |
| self._LogWarning("Error: {}", p.stdwerr) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Correct attribute reference |
| self._LogWarning("Error: {}", p.stderr) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-13: Shallow Clone SHA-1 Fetch Fallback |
| |
| > **Rule:** Provide a full-sync fallback when shallow fetch requests for |
| > specific SHA-1s are rejected by server policies. |
| > |
| > **What:** Git fetch requests targeting specific SHA-1s with `--depth` enabled |
| > must handle graceful fallbacks, as server-side policies often restrict |
| > unadvertised object fetching. |
| > |
| > **Applies To:** Git fetch routines in `project.py`, specifically during |
| > incremental or optimized shallow clones. |
| > |
| > **Why:** When optimizing syncs by directly fetching specific SHA-1s with a |
| > depth limitation, operations failed silently or returned generic errors |
| > because some Git servers do not advertise all objects, explicitly rejecting |
| > such requests. Failing to adhere to this typically results in **Sync |
| > Failure**. |
| |
| **Trap 1: Assuming a fetch of an arbitrary SHA-1 will unconditionally succeed if |
| `--depth` is used.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Missing fallback for unadvertised objects |
| ret = fetch_sha1(depth=1) |
| if ret < 0: |
| break |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Explicit fallback to a full sync (depth-less) if the specific SHA-1 is rejected |
| ret = fetch_sha1(depth=1) |
| if depth and is_sha1 and ret == 1: |
| # Server may not allow fetching unadvertised refs, fallback to full sync |
| break |
| ``` |
| |
| **Exceptions:** Servers specifically configured to allow |
| `uploadpack.allowReachableSHA1InWant` or similar parameters, though the client |
| cannot assume this. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-14: Validating Gerrit Push Option Syntax |
| |
| > **Rule:** Pass custom Gerrit push options strictly using the documented |
| > `custom-keyed-value` parameter syntax. |
| > |
| > **What:** When passing custom metadata to Gerrit via Git push options, |
| > strictly adhere to the `custom-keyed-value` syntax as defined by the remote |
| > server documentation. |
| > |
| > **Applies To:** `upload.py` operations mapping superproject state to `git push |
| > -o`. |
| > |
| > **Why:** The client attempted to attach superproject metadata to push |
| > operations using `custom-key-value`, which was ignored by the Gerrit server |
| > because the documented parameter was `custom-keyed-value`. Failing to adhere |
| > to this typically results in **Metadata Loss / Server Rejection**. |
| |
| **Trap 1: Constructing push options with incorrect, unverified key strings.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Incorrect key format |
| push_options.append(f"custom-key-value=rootRepo:{host}/{sp_name}") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Matches external documentation |
| push_options.append(f"custom-keyed-value=rootRepo:{host}/{sp_name}") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-15: Submodule Initialization Guard Under Git Worktrees |
| |
| > **Rule:** Skip Git submodule initialization sequences if the repository is |
| > actively utilizing Git worktrees. |
| > |
| > **What:** Submodule initialization operations (`git submodule init`) must be |
| > explicitly skipped if the repository is configured to use Git worktrees. |
| > |
| > **Applies To:** Git command wrappers, worktree initialization sequences, and |
| > submodule lifecycle operations in `project.py`. |
| > |
| > **Why:** Upstream Git has known bugs and incomplete feature support regarding |
| > the use of submodules within worktrees. Attempting to initialize submodules |
| > under these conditions leads to undefined behavior and broken repository |
| > states. Failing to adhere to this typically results in **Upstream Git Bug / |
| > Inconsistency**. |
| |
| **Trap 1: Blindly initializing submodules without verifying if the underlying |
| Git worktree feature is active.** |
| |
| **Don't:** |
| |
| ```python |
| self._InitWorkTree(force_sync=force_sync, submodules=submodules) |
| if self.has_subprojects: |
| self._InitSubmodules() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| self._InitWorkTree(force_sync=force_sync, submodules=submodules) |
| # Avoid doing a submodule init when using worktrees as the support for |
| # submodules is incomplete. |
| if self.has_subprojects and not self.use_git_worktrees: |
| self._InitSubmodules() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-16: XDG Compliant Git Configuration Resolution |
| |
| > **Rule:** Resolve user Git configuration files natively honoring the |
| > `XDG_CONFIG_HOME` environment variable before defaulting to legacy paths. |
| > |
| > **What:** Git configuration lookups must natively respect the |
| > `XDG_CONFIG_HOME` environment variable, falling back to `~/.config/git/config` |
| > before finally querying the legacy `~/.gitconfig` location. |
| > |
| > **Applies To:** Git configuration parsing, environment setup, and hermetic |
| > tests. |
| > |
| > **Why:** Users who migrated their dotfiles to the XDG Base Directory |
| > Specification experienced issues because internal tooling rigidly expected |
| > `~/.gitconfig` to exist, bypassing user intentions and leading to |
| > unauthenticated or unconfigured repository actions. Failing to adhere to this |
| > typically results in **Orphaned Configuration State**. |
| |
| **Trap 1: Hardcoding the user's home directory as the sole location for |
| `.gitconfig`.** |
| |
| **Don't:** |
| |
| ```python |
| def _getUserConfig(): |
| return os.path.expanduser("~/.gitconfig") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def _getUserConfig(): |
| """This matches git: https://git-scm.com/docs/git-config#FILES""" |
| xdg_config_home = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) |
| xdg_config_file = os.path.join(xdg_config_home, "git", "config") |
| if os.path.exists(xdg_config_file): |
| return xdg_config_file |
| return os.path.expanduser("~/.gitconfig") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-17: Strongly-Typed Subprocess Error Contexting |
| |
| > **Rule:** Raise strongly-typed `GitCommandError` exceptions with granular |
| > state context instead of generic Git errors. |
| > |
| > **What:** Git commands that yield non-zero exit codes must raise a specialized |
| > `GitCommandError` containing granular execution context, rather than a generic |
| > `GitError`. |
| > |
| > **Applies To:** Subprocess execution wrappers within `git_command.py` and |
| > downstream consumers handling Git process states. |
| > |
| > **Why:** When commands failed silently or raised generic errors, downstream |
| > handlers could not differentiate between configuration errors and actual |
| > subprocess failures, leading to loss of arguments and return codes. Failing to |
| > adhere to this typically results in **Lost Diagnostic Context**. |
| |
| **Trap 1: Catching a subprocess failure and re-raising it as a generic git error |
| with a concatenated string message.** |
| |
| **Don't:** |
| |
| ```python |
| if self.rc != 0: |
| raise GitError("%s: %s" % (command[1], e)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if self.rc != 0: |
| raise GitCommandError( |
| message="git command failure", |
| project=project.name, |
| command_args=self.cmdv, |
| git_rc=self.rc, |
| git_stdout=stdout, |
| ) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T3-18: Line-Based Log Extraction for Subprocess Diagnostics |
| |
| > **Rule:** Extract and log the first meaningful line of both standard output |
| > and error streams rather than enforcing arbitrary truncation limits. |
| > |
| > **What:** When capturing subprocess logs for error reporting, extract the |
| > first meaningful line of `stdout` and `stderr` instead of relying on arbitrary |
| > character limits. |
| > |
| > **Applies To:** Error truncation logic for Git subprocesses in `Wait()` |
| > blocks. |
| > |
| > **Why:** Truncating output by an arbitrary number of characters (e.g., 80 |
| > chars) occasionally cut off the actual error string. Furthermore, ignoring |
| > `stderr` hid the root cause of the subprocess failure. Failing to adhere to |
| > this typically results in **Obfuscated Error Logs**. |
| |
| **Trap 1: Truncating stdout to a fixed number of characters and ignoring stderr |
| entirely.** |
| |
| **Don't:** |
| |
| ```python |
| stdout = self.stdout[:80] if self.stdout else None |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Prefer extracting the first full line and including stderr |
| stdout_line = self.stdout.splitlines()[0] if self.stdout else None |
| stderr_line = self.stderr.splitlines()[0] if self.stderr else None |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T2 | Filesystem Atomicity & Worktree Layout - *Subprocess |
| executions must coordinate with filesystem atomicity rules, particularly |
| regarding submodule paths and worktree usage to prevent upstream Git bugs.* |
| * **Downstream:** T5 | Hermetic Testing & Test Modernization - *Testing |
| frameworks depend on hermetic GitCommand wrappers and mocked stream capture |
| to validate subprocess behaviors safely.* |
| |
| ## Chapter: Manifest Object Model & Deduplication |
| |
| **Context:** This chapter governs the parsing, validation, and canonicalization |
| of XML manifest components. It strictly enforces semantic immutability via |
| `NamedTuple` implementations, defensive copying for hierarchical override |
| scoping, and deterministic JSON-backed file tracking across the subsystem. |
| |
| ### Summary |
| |
| | Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap | |
| | :-------- | :------------------------ | :------- | :------------------------ | |
| | **T4-01** | Strict Traversal Prefix | High | Using broad substring | |
| : : Matching for Relative : : matching to detect : |
| : : URLs : : relative traversal : |
| : : : : sequences. : |
| | **T4-02** | Extend-Project | High | Omitting group | |
| : : Inheritance Scoping : : inheritance constraints : |
| : : : : for dynamically : |
| : : : : overridden node elements. : |
| | **T4-03** | Tuple Immutability in | Medium | Instantiating transient | |
| : : Membership Checks : : list objects inside loop : |
| : : : : conditions. : |
| | **T4-04** | Defensive Copying of | High | Assigning a shared | |
| : : Shared Collections Prior : : collection reference and : |
| : : to In-Place Mutation : : immediately using an : |
| : : : : in-place operator, : |
| : : : : unknowingly mutating the : |
| : : : : original source. : |
| | **T4-05** | Immutable NamedTuples for | High | Overriding `__hash__` and | |
| : : Hashable Object : : `__eq__` on a standard : |
| : : Deduplication : : mutable Python class to : |
| : : : : allow set insertion. : |
| | **T4-06** | Strict Empty Set | Critical | Falling back to the empty | |
| : : Initialization Over : : bracket literal when a : |
| : : Dictionary Literals : : null set is encountered : |
| : : : : prior to a union : |
| : : : : operation. : |
| | **T4-07** | Immutable Manifest | High | Implementing custom | |
| : : Elements for Reliable : : `__eq__` and `__hash__` : |
| : : Deduplication : : on mutable classes and : |
| : : : : using `list.append()` : |
| : : : : which allows duplicates : |
| : : : : to accrue. : |
| | **T4-08** | Mutually Exclusive XML | Medium | Independent `if` | |
| : : Node Parsing : : conditions checking : |
| : : : : `nodeName` sequentially : |
| : : : : in a loop. : |
| | **T4-09** | JSON Serialization for | High | Dumping file paths | |
| : : File Path State Tracking : : sequentially into a flat : |
| : : : : text file using newlines : |
| : : : : as the only delimiter. : |
| | **T4-10** | Set Operations for File | Medium | Using manual for-loops | |
| : : Deltas : : and list appends to : |
| : : : : calculate the difference : |
| : : : : between two collections. : |
| | **T4-11** | Upstream Base Revision | Medium | Overriding a project | |
| : : Validation in Layered : : revision without : |
| : : Manifests : : asserting the previous : |
| : : : : expected state. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T4-01: Strict Traversal Prefix Matching for Relative URLs |
| |
| > **Rule:** Always explicitly evaluate exact path-traversal prefixes (`./` or |
| > `../`) when resolving relative URLs. Never rely on generic substring matching. |
| > |
| > **What:** Relative URL path detection must explicitly evaluate exact |
| > path-traversal prefixes to avoid inadvertently capturing hidden files or |
| > custom naming structures. |
| > |
| > **Applies To:** URL string manipulation and Git submodule path resolution. |
| > |
| > **Why:** The path parser attempted to identify relative submodule paths by |
| > checking if the URL started with a dot (`.`). This excessively broad |
| > evaluation failed on identically named hidden repositories or custom |
| > sub-namespaces (e.g., `.foo`), improperly joining them to the base URL. |
| > Failing to adhere to this typically results in **Invalid Path Resolution**. |
| |
| **Trap 1: Using broad substring matching to detect relative traversal |
| sequences.** |
| |
| **Don't:** |
| |
| ```python |
| if url.startswith("."): |
| url = urllib.parse.urljoin("%s/" % self.remote.url, url) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if url.startswith("./") or url.startswith("../"): |
| url = urllib.parse.urljoin("%s/" % self.remote.url, url) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-02: Extend-Project Inheritance Scoping |
| |
| > **Rule:** Must explicitly restrict `extend-project` XML nodes from |
| > automatically inheriting contextually local execution scopes (`local:` groups) |
| > from root parents. |
| > |
| > **What:** When managing XML manifestation directives, `extend-project` nodes |
| > must be actively restricted from inheriting contextually local execution |
| > scopes (`local:` groups). |
| > |
| > **Applies To:** XML manifest engine mappings and override properties. |
| > |
| > **Why:** Without explicit protection, extended project entities |
| > unintentionally inherited top-level project local groups, confusing the |
| > inclusion and exclusion scoping directives that drive synchronization |
| > boundaries. Failing to adhere to this typically results in **Scope |
| > Contamination**. |
| |
| **Trap 1: Omitting group inheritance constraints for dynamically overridden node |
| elements.** |
| |
| **Don't:** |
| |
| * Allow XML logic to process `extend-project` the exact same way it processes |
| raw `project` roots without negative test coverage. |
| |
| **Do:** |
| |
| * Implement testing and parsing guards proving that elements processed under |
| `extend-project` actively strip `local:` group attributes. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-03: Tuple Immutability in Membership Checks |
| |
| > **Rule:** Always use immutable tuples instead of list literals for membership |
| > tests against constant sequences within hot loops. |
| > |
| > **What:** Utilize immutable tuples `()` instead of list literals `[]` when |
| > performing membership tests (the `in` operator) against constant sequences to |
| > minimize reallocation overhead and adhere to idiomatic Python. |
| > |
| > **Applies To:** XML manifest parsing logic (`manifest_xml.py`) and hot loops |
| > across the codebase. |
| > |
| > **Why:** During recursive node parsing, list literals were instantiated on |
| > every iteration solely to check if a node name matched fixed schema elements. |
| > Failing to adhere to this typically results in **Performance Degradation**. |
| |
| **Trap 1: Instantiating transient list objects inside loop conditions.** |
| |
| **Don't:** |
| |
| ```python |
| if node.nodeName in ["include", "project"]: |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if node.nodeName in ("include", "project"): |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-04: Defensive Copying of Shared Collections Prior to In-Place Mutation |
| |
| > **Rule:** Must defensively copy shared parent collections before applying |
| > in-place update operators (`|=`), strictly handling null-safety during the |
| > process. |
| > |
| > **What:** When propagating inherited collections (e.g., manifest group sets) |
| > to child nodes, shared parent sets must be defensively copied prior to |
| > applying in-place union or update operators (`|=`). Null-safety must be |
| > handled during the copy. |
| > |
| > **Applies To:** XML Manifest Parsing (`manifest_xml.py`); hierarchical state |
| > propagation where child elements inherit and augment parent states. |
| > |
| > **Why:** Assigning a parent's group set directly to a child and then using the |
| > in-place union operator (`|=`) modified the original parent set in memory. |
| > This caused the child's specific groups to 'leak' into unrelated sibling nodes |
| > processed subsequently. Failing to adhere to this typically results in **State |
| > Corruption / Group Leakage**. |
| |
| **Trap 1: Assigning a shared collection reference and immediately using an |
| in-place operator, unknowingly mutating the original source.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: In-place modification mutates the shared parent_groups object |
| nodeGroups = parent_groups |
| if node.hasAttribute("groups"): |
| nodeGroups |= self._ParseSet(node.getAttribute("groups")) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Create a localized copy before mutation |
| nodeGroups = parent_groups.copy() if parent_groups else set() |
| if node.hasAttribute("groups"): |
| nodeGroups |= self._ParseSet(node.getAttribute("groups")) |
| ``` |
| |
| **Trap 2: Falling back to an empty set instantiation after a copy call without |
| guarding against a null reference.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Raises AttributeError if parent_groups is None |
| include_groups = parent_groups.copy() or set() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Null-safe default evaluation prior to the copy invocation |
| include_groups = (parent_groups or set()).copy() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-05: Immutable NamedTuples for Hashable Object Deduplication |
| |
| > **Rule:** Always subclass `NamedTuple` to define custom objects stored in |
| > hash-based collections. Never implement manual `__hash__` and `__eq__` |
| > functions on mutable classes. |
| > |
| > **What:** Custom objects stored in hash-based collections (like `set` or used |
| > as `dict` keys) for deduplication must guarantee immutability. Avoid manually |
| > implementing `__eq__` and `__hash__` on mutable classes; instead, subclass |
| > `NamedTuple`. |
| > |
| > **Applies To:** Manifest deduplication logic (`project.py`); specifically |
| > elements like `_CopyFile` and `_LinkFile` stored in sets. |
| > |
| > **Why:** Manually implementing `__hash__` on a standard class based on its |
| > mutable `__dict__` attributes created a landmine. If the object's properties |
| > changed after insertion into a set, the hash mismatched, rendering the object |
| > unreachable and breaking deduplication. Failing to adhere to this typically |
| > results in **Memory Leak / Deduplication Failure**. |
| |
| **Trap 1: Overriding `__hash__` and `__eq__` on a standard mutable Python class |
| to allow set insertion.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Properties can mutate, altering the hash post-insertion |
| class _CopyFile: |
| def __init__(self, src, dest): |
| self.src = src |
| self.dest = dest |
| def __hash__(self): |
| return hash(repr(sorted(self.__dict__.items()))) |
| def __eq__(self, other): |
| return self.__dict__ == other.__dict__ |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Use typing.NamedTuple to guarantee immutability |
| from typing import NamedTuple |
| |
| class _CopyFile(NamedTuple): |
| src: str |
| dest: str |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-06: Strict Empty Set Initialization Over Dictionary Literals |
| |
| > **Rule:** Must explicitly invoke `set()` when initializing empty collections |
| > meant for set operations. Never fall back to the empty bracket literal `{}`. |
| > |
| > **What:** When initializing or providing a fallback empty collection meant for |
| > set operations (such as union `|` or `|=`), explicitly instantiate a `set()` |
| > rather than using the empty literal `{}`. |
| > |
| > **Applies To:** Python collection management globally; prominently surfaced |
| > during list-to-set manifest group migrations. |
| > |
| > **Why:** During a migration from lists to sets for group parsing, an empty |
| > default was incorrectly represented as `{}`. Since `{}` evaluates to a `dict` |
| > in Python, attempting a set union between a valid set and the empty dict threw |
| > an immediate, fatal `TypeError`. Failing to adhere to this typically results |
| > in **Runtime Crash / TypeError**. |
| |
| **Trap 1: Falling back to the empty bracket literal when a null set is |
| encountered prior to a union operation.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: '{}' creates an empty dictionary, throwing a TypeError upon union |
| expanded_project_groups = {"all"} | (self.groups or {}) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Explicitly initialize an empty set |
| expanded_project_groups = {"all"} | (self.groups or set()) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-07: Immutable Manifest Elements for Reliable Deduplication |
| |
| > **Rule:** Always represent manifest file operations as inherently immutable |
| > structures to guarantee safe, order-preserving deduplication. |
| > |
| > **What:** Manifest elements representing file operations must be implemented |
| > as immutable structures (e.g., `NamedTuple`) to ensure safe deduplication and |
| > deterministic order preservation, avoiding manual string-based hashing. |
| > |
| > **Applies To:** Manifest parsing models, specifically `<copyfile>` and |
| > `<linkfile>` abstractions. |
| > |
| > **Why:** Previously, manifest objects were stored in mutable lists, relying on |
| > manual string representations (`repr`) and sorted dictionary items for |
| > hashing. This risked state corruption and failed to cleanly deduplicate |
| > repeated entries while maintaining the relative declaration sequence. Failing |
| > to adhere to this typically results in **Silent Data Corruption / Redundant |
| > File Operations**. |
| |
| **Trap 1: Implementing custom `__eq__` and `__hash__` on mutable classes and |
| using `list.append()` which allows duplicates to accrue.** |
| |
| **Don't:** |
| |
| ```python |
| class _CopyFile: |
| def __hash__(self): |
| # BAD: Manual serialization of mutable dictionary |
| return hash(repr(sorted(self.__dict__.items()))) |
| |
| self.copyfiles.append(_CopyFile(src, dest)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| class _CopyFile(NamedTuple): |
| # GOOD: Inherently immutable and hashable |
| src: str |
| dest: str |
| |
| # GOOD: Using a dict to deduplicate while preserving insertion order |
| self.copyfiles[_CopyFile(src, dest)] = True |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-08: Mutually Exclusive XML Node Parsing |
| |
| > **Rule:** Always chain mutually exclusive XML node type checks using an |
| > `if-elif` structure rather than independent sequential statements. |
| > |
| > **What:** When parsing mutually exclusive XML node types, use an `if-elif` |
| > structure to avoid redundant condition evaluations. |
| > |
| > **Applies To:** XML parsing loops (e.g., iterating over `node.childNodes` in |
| > `manifest_xml.py`). |
| > |
| > **Why:** A sequence of independent `if` statements forced unnecessary string |
| > comparisons even after a matching node name was found. Failing to adhere to |
| > this typically results in **CPU Overhead / Inefficient Parsing**. |
| |
| **Trap 1: Independent `if` conditions checking `nodeName` sequentially in a |
| loop.** |
| |
| **Don't:** |
| |
| ```python |
| if n.nodeName == "copyfile": |
| self._ParseCopyFile(p, n) |
| if n.nodeName == "linkfile": |
| self._ParseLinkFile(p, n) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if n.nodeName == "copyfile": |
| self._ParseCopyFile(p, n) |
| elif n.nodeName == "linkfile": |
| self._ParseLinkFile(p, n) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-09: JSON Serialization for File Path State Tracking |
| |
| > **Rule:** Must persist dynamically generated manifest file outputs using |
| > structured JSON serialization. Never use newline-delimited plaintext for path |
| > lists. |
| > |
| > **What:** Persistent tracking mechanisms for manifest-generated file |
| > operations must use structured JSON serialization instead of newline-delimited |
| > plaintext to support special characters in paths. |
| > |
| > **Applies To:** Manifest state management (`subcmds/sync.py`), internal |
| > `.repo` tracking files (e.g., `copy-link-files.json`). |
| > |
| > **Why:** Tracking `<copyfile>` and `<linkfile>` operations via flat, |
| > newline-delimited text files broke serialization and cleanup when file paths |
| > legally contained newline (`\n`) characters. Structured JSON protects against |
| > these bounds issues. Failing to adhere to this typically results in **Parsing |
| > Errors / Orphaned Files**. |
| |
| **Trap 1: Dumping file paths sequentially into a flat text file using newlines |
| as the only delimiter.** |
| |
| **Don't:** |
| |
| ```python |
| with open('linkfile.list', 'w') as fd: |
| for path in new_linkfiles: |
| fd.write(path + '\n') |
| ``` |
| |
| **Do:** |
| |
| ```python |
| with open('copy-link-files.json', 'w') as fd: |
| json.dump({'linkfile': new_linkfiles, 'copyfile': new_copyfiles}, fd) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-10: Set Operations for File Deltas |
| |
| > **Rule:** Always compute the delta between unordered lists of file paths using |
| > native Python set operations (`-`). |
| > |
| > **What:** When computing the delta between lists of file paths (e.g., figuring |
| > out which obsolete files to remove), utilize Python set operations rather than |
| > manual iterations with truthiness and membership checks. |
| > |
| > **Applies To:** Deduplicating objects, comparing manifest states, computing |
| > file differences. |
| > |
| > **Why:** Calculating the difference between historical tracking data and new |
| > target paths was done using O(N*M) nested iterations with redundant truthiness |
| > checks, leading to verbose and less performant code. Failing to adhere to this |
| > typically results in **Algorithmic Inefficiency**. |
| |
| **Trap 1: Using manual for-loops and list appends to calculate the difference |
| between two collections.** |
| |
| **Don't:** |
| |
| ```python |
| need_remove_files = [] |
| for old_path in old_dict.get('linkfile', []): |
| if old_path and old_path not in new_paths: |
| need_remove_files.append(old_path) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| need_remove_files = [] |
| need_remove_files.extend( |
| set(old_dict.get('linkfile', [])) - |
| set(new_paths) |
| ) |
| ``` |
| |
| **Exceptions:** Ordered constraints where file evaluation sequence matters (sets |
| are unordered). |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T4-11: Upstream Base Revision Validation in Layered Manifests |
| |
| > **Rule:** Must assert multi-layer manifest overrides (`extend-project`) |
| > against the parent project's expected state by utilizing a `base-rev` |
| > attribute. |
| > |
| > **What:** When overriding (`extend-project`) or removing (`remove-project`) |
| > projects in a layered manifest structure, changes should be validated against |
| > the expected state of the parent project via a `base-rev` attribute. |
| > |
| > **Applies To:** XML Manifest parsing logic (`manifest_xml.py`) during |
| > multi-manifest ingestion. |
| > |
| > **Why:** Downstream projects using layered manifests would unknowingly apply |
| > overrides (like custom revisions) to upstream components that had drastically |
| > changed their underlying state or branches, resulting in 'undead patches' that |
| > silently masked critical upstream changes. Failing to adhere to this typically |
| > results in **Silent Configuration Divergence**. |
| |
| **Trap 1: Overriding a project revision without asserting the previous expected |
| state.** |
| |
| **Don't:** |
| |
| ```xml |
| <!-- BAD: Blind override leaves system vulnerable to upstream changes --> |
| <extend-project name="my-project" revision="refs/changes/123" /> |
| ``` |
| |
| **Do:** |
| |
| ```xml |
| <!-- GOOD: Base-rev acts as a guardrail --> |
| <extend-project name="my-project" base-rev="upstream_hash" revision="refs/changes/123" /> |
| ``` |
| |
| **Trap 2: Checking string-based manifest attributes using implicit truthiness.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Implicit truthiness skips validation if revisionExpr is unexpectedly empty |
| if base and p.revisionExpr and p.revisionExpr != base: |
| raise Error() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Strict comparison catches empty/None states if a base is declared |
| if base and p.revisionExpr != base: |
| raise Error() |
| ``` |
| |
| **Exceptions:** The `base-rev` attribute is optional to maintain backward |
| compatibility for users migrating to the new manifest schema. |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T2 | Filesystem Atomicity & Worktree Layout - *File path |
| extraction and submodule path resolution mapped here strictly govern |
| worktree creation and legacy migration algorithms downstream.* |
| * **Downstream:** T6 | CLI Argument Parsing & UX Consistency - *Structured |
| JSON serialization outputted from manifest paths sets the direct format |
| constraint for CLI UX and tooling integrations.* |
| |
| ## Chapter: Hermetic Testing & Test Modernization |
| |
| **Context:** This domain governs the migration of legacy `unittest` suites to |
| modern `pytest` functional paradigms and the establishment of hermetic session |
| fixtures. It enforces strict environment isolation to prevent global state |
| pollution (e.g., developer `.gitconfig` bleeding) while safely intercepting |
| standard streams and filesystem paths. |
| |
| ### Summary |
| |
| | Rule ID | Principle / Constraint | Priority | Primary Symptom / | |
| : : : : Trap : |
| | :-------- | :------------------------------ | :------- | :------------------ | |
| | **T5-01** | Idiomatic Truthiness Validation | Medium | Asserting strict | |
| : : in Tests : : identity against : |
| : : : : Python boolean : |
| : : : : singletons. : |
| | **T5-02** | Pytest Over Legacy Unittest | High | Subclassing | |
| : : Framework : : `unittest.TestCase` : |
| : : : : and configuring : |
| : : : : state inside a : |
| : : : : `setUp` method for : |
| : : : : new test files. : |
| | **T5-03** | Dedicated Test Utilities for | Medium | Copy-pasting | |
| : : API Helpers : : utility functions : |
| : : : : across test modules : |
| : : : : or placing standard : |
| : : : : functions into : |
| : : : : pytest conftest : |
| : : : : files. : |
| | **T5-04** | Test Framework Modernization to | High | Modifying | |
| : : Pytest : : environment : |
| : : : : variables globally : |
| : : : : at module load to : |
| : : : : support tests. : |
| | **T5-05** | Standard Output Capture via | Medium | Using the mock | |
| : : Context Managers : : framework to : |
| : : : : intercept a core : |
| : : : : language stream. : |
| | **T5-06** | Direct Helper Functions Over | Medium | Defining a pytest | |
| : : Fixture Factories : : fixture that yields : |
| : : : : a nested function : |
| : : : : to bypass global : |
| : : : : state limitations. : |
| | **T5-07** | Elimination of Redundant Path | Medium | Chaining explicit | |
| : : Assertions : : existence checks : |
| : : : : before access : |
| : : : : checks. : |
| | **T5-08** | Pathlib Utility Constants for | Medium | Using nested | |
| : : Test Path Resolution : : `os.path.dirname` : |
| : : : : and `os.path.join` : |
| : : : : to traverse back to : |
| : : : : the parent : |
| : : : : directory. : |
| | **T5-09** | Pytest Adoption for Test Suite | High | Creating test case | |
| : : Definition : : classes that : |
| : : : : inherit from : |
| : : : : `unittest.TestCase` : |
| : : : : and asserting via : |
| : : : : `self.assertEqual`. : |
| | **T5-10** | Minimal Mocking Isolation for | High | Using stacked patch | |
| : : Test Reliability : : decorators for : |
| : : : : non-relevant : |
| : : : : sub-routines (e.g., : |
| : : : : standard error : |
| : : : : outputs or : |
| : : : : colorizers) just to : |
| : : : : trigger an embedded : |
| : : : : autocorrection : |
| : : : : flow. : |
| | **T5-11** | Canonical Resolution for System | Medium | Patching | |
| : : Library Mocks : : `time.sleep` by : |
| : : : : referencing the : |
| : : : : specific host : |
| : : : : application file : |
| : : : : where it was : |
| : : : : locally imported. : |
| | **T5-12** | Memoized Function Cache | High | Mocking the | |
| : : Clearing in Setup : : underlying behavior : |
| : : : : of a cached : |
| : : : : function but : |
| : : : : leaving the LRU : |
| : : : : cache intact across : |
| : : : : tests. : |
| | **T5-13** | Idiomatic Standard Stream | Medium | Using `mock.patch` | |
| : : Redirection : : to manually inject : |
| : : : : a `StringIO` object : |
| : : : : over `sys.stderr`. : |
| | **T5-14** | Context Manager Nesting | Medium | Adding a new level | |
| : : Consolidation : : of indentation for : |
| : : : : every context : |
| : : : : manager invoked. : |
| | **T5-15** | Anti-Change Detector Tests | Medium | Asserting exact | |
| : : : : equality of string : |
| : : : : outputs including : |
| : : : : standardized : |
| : : : : prefixes. : |
| | **T5-16** | Infrastructure Prebuilts via | Medium | Relying on tools | |
| : : vpython : : that dynamically : |
| : : : : build Python from : |
| : : : : source to test : |
| : : : : across multiple : |
| : : : : versions. : |
| | **T5-17** | Modernized Filesystem Paths via | Medium | Using | |
| : : Pathlib : : `os.path.join` and : |
| : : : : `with open(..., : |
| : : : : 'w')` for basic : |
| : : : : text file creation : |
| : : : : in tests. : |
| | **T5-18** | Standardized Module-Level Mock | Medium | Importing specific | |
| : : Imports : : classes or : |
| : : : : exceptions directly : |
| : : : : from test targets : |
| : : : : or standard : |
| : : : : libraries. : |
| | **T5-19** | Pytest Adoption for New Test | High | Creating subclasses | |
| : : Modules : : of : |
| : : : : `unittest.TestCase` : |
| : : : : and using : |
| : : : : `self.assertEqual`. : |
| | **T5-20** | High-Fidelity Test Targets over | High | Using `MagicMock` | |
| : : Permissive Mocks : : to bypass : |
| : : : : instantiating core : |
| : : : : models. : |
| | **T5-21** | Session-Scoped Hermetic Git | Medium | Executing `git | |
| : : Identity Injection : : config` inside : |
| : : : : individual test : |
| : : : : setup blocks to : |
| : : : : fake a user : |
| : : : : identity. : |
| | **T5-22** | Automated Lifecycle Management | High | Spawning detached | |
| : : for Test Filesystem Artifacts : : temporary : |
| : : : : directories and : |
| : : : : manually : |
| : : : : maintaining cleanup : |
| : : : : blocks. : |
| | **T5-23** | Idiomatic String Evaluation in | Medium | Iterating over raw | |
| : : Mocked Test Streams : : string splits and : |
| : : : : manually validating : |
| : : : : line length to : |
| : : : : count valid output : |
| : : : : strings. : |
| | **T5-24** | Hermetic Isolation of Git | High | Relying on default | |
| : : Configuration Directories in : : system paths for : |
| : : Tests : : the home directory : |
| : : : : during test : |
| : : : : execution. : |
| | **T5-25** | Scope Hierarchy Enforcement in | Medium | Injecting | |
| : : Pytest Fixtures : : `tmp_path` into a : |
| : : : : fixture decorated : |
| : : : : with : |
| : : : : `scope="session"`. : |
| | **T5-26** | Strict Python Version | Medium | Adding modern | |
| : : Compatibility for Third-Party : : third-party test : |
| : : Plugins : : dependencies to : |
| : : : : legacy-supported : |
| : : : : projects without : |
| : : : : validating the : |
| : : : : minimum required : |
| : : : : Python constraints. : |
| | **T5-27** | Hermetic Isolation of Terminal | High | Relying on the | |
| : : UI Color Configurations : : default state of : |
| : : : : the `Coloring` : |
| : : : : class without : |
| : : : : forcing it during : |
| : : : : setup, implicitly : |
| : : : : depending on : |
| : : : : environmental : |
| : : : : variables. : |
| | **T5-28** | Abstract Intent Documentation | Medium | Documenting exact | |
| : : in Unit Tests : : data values being : |
| : : : : asserted directly : |
| : : : : in the test : |
| : : : : docstring. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T5-01: Idiomatic Truthiness Validation in Tests |
| |
| > **Rule:** Always use implicit truthiness or falsiness evaluations in test |
| > assertions unless strict type matching is explicitly required. |
| > |
| > **What:** Test suites must use implicit truthy/falsy validations rather than |
| > asserting strict identity against boolean singletons unless exact type |
| > enforcement is necessary. |
| > |
| > **Applies To:** Python assertions in test frameworks (pytest, unittest). |
| > |
| > **Why:** Tests relying on strict boolean identity (`is True`) became brittle |
| > when refactoring configurations or flag implementations that returned |
| > alternative truthy values. Failing to adhere to this typically results in |
| > **Brittle Assertions**. |
| |
| **Trap 1: Asserting strict identity against Python boolean singletons.** |
| |
| **Don't:** |
| |
| ```python |
| assert opts.include_summary is True |
| ``` |
| |
| **Do:** |
| |
| ```python |
| assert opts.include_summary |
| ``` |
| |
| **Exceptions:** Situations where distinguishing between None and False is |
| structurally critical to the application logic. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-02: Pytest Over Legacy Unittest Framework |
| |
| > **Rule:** Must implement new test suites using modular `pytest` definitions |
| > instead of propagating `unittest.TestCase` boilerplate. |
| > |
| > **What:** New test modules must be implemented using standalone `pytest` |
| > functional constructs rather than inheriting from `unittest.TestCase` to avoid |
| > heavy class-based boilerplate. |
| > |
| > **Applies To:** Test module architecture (`tests/test_*.py`). |
| > |
| > **Why:** The project previously utilized deeply nested `unittest.TestCase` |
| > structures with complex `setUp` inheritance, which obfuscated test |
| > initialization and hindered code reusability. Failing to adhere to this |
| > typically results in **High Boilerplate / Lock-In**. |
| |
| **Trap 1: Subclassing `unittest.TestCase` and configuring state inside a `setUp` |
| method for new test files.** |
| |
| **Don't:** |
| |
| ```python |
| class InfoCommand(unittest.TestCase): |
| def setUp(self): |
| self.cmd = build_cmd() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def _get_cmd(): |
| return build_cmd() |
| |
| def test_command_defaults(): |
| cmd = _get_cmd() |
| assert cmd.is_valid() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-03: Dedicated Test Utilities for API Helpers |
| |
| > **Rule:** Never overload `conftest.py` with generic API helpers; always place |
| > reusable test queries in dedicated utility modules. |
| > |
| > **What:** Shared test APIs and environment-probing logic (e.g., checking Git |
| > feature support) must be placed in dedicated utility modules (like |
| > `utils_for_test.py`) rather than duplicated or shoved into `conftest.py`. |
| > |
| > **Applies To:** Test suite architecture and environment scaffolding. |
| > |
| > **Why:** Developers were duplicating identical environment checks across |
| > different test files or misusing `conftest.py` (which should only contain |
| > fixtures) to store functional APIs. Failing to adhere to this typically |
| > results in **Test Code Duplication**. |
| |
| **Trap 1: Copy-pasting utility functions across test modules or placing standard |
| functions into pytest conftest files.** |
| |
| **Don't:** |
| |
| ```python |
| # Inside test_project.py AND test_git_refs.py |
| def _SupportsReftable(): |
| return check_git_support() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Inside tests/utils_for_test.py |
| def supports_reftable(): |
| return check_git_support() |
| |
| # Inside test_project.py |
| import utils_for_test |
| @unittest.skipUnless(utils_for_test.supports_reftable(), "...") |
| ``` |
| |
| **Exceptions:** Test fixtures (such as initialized directories or mock |
| generators) which belong in `conftest.py`. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-04: Test Framework Modernization to Pytest |
| |
| > **Rule:** Avoid global module-level environment mutations; consistently |
| > leverage `pytest` global configuration fixtures. |
| > |
| > **What:** Legacy `unittest.TestCase` structures must be migrated to functional |
| > `pytest` paradigms, leveraging global configuration fixtures over class-based |
| > `setUp` and `tearDown` methods. |
| > |
| > **Applies To:** Entire Python test suite and all new unit tests. |
| > |
| > **Why:** Heavy inheritance and isolated setup code within `unittest.TestCase` |
| > resulted in repeated environment mutation logic, obscured dependencies, and an |
| > inability to easily share global setup mechanisms like dummy user identities |
| > or tracing control. Failing to adhere to this typically results in **Global |
| > State Pollution**. |
| |
| **Trap 1: Modifying environment variables globally at module load to support |
| tests.** |
| |
| **Don't:** |
| |
| ```python |
| import os |
| os.environ['REPO_TRACE'] = '0' |
| |
| class GitRefsTest(unittest.TestCase): |
| def setUp(self): |
| self.tempdir = tempfile.TemporaryDirectory() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Rely on conftest.py fixtures automatically handling environments. |
| def test_git_refs(tmp_path, disable_repo_trace, setup_user_identity): |
| gitdir = tmp_path / '.git' |
| # Run assertions using standard assert statements |
| assert head == refs.get('HEAD') |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-05: Standard Output Capture via Context Managers |
| |
| > **Rule:** Must utilize `contextlib.redirect_stdout` to capture standard output |
| > streams rather than injecting raw stream mocks. |
| > |
| > **What:** Use the standard library `contextlib.redirect_stdout` for capturing |
| > command output in test scopes, instantiated directly as a one-liner, rather |
| > than utilizing the `unittest.mock` framework to patch system streams. |
| > |
| > **Applies To:** Python test suites (Pytest/Unittest) intercepting CLI stdout |
| > payloads. |
| > |
| > **Why:** Tests were previously relying on `@mock.patch` for `sys.stdout` or |
| > utilizing multi-line context manager instantiations, which unnecessarily |
| > coupled output capture to the mocking engine and added boilerplate. Failing to |
| > adhere to this typically results in **Unnecessary Mocking / Test Bloat**. |
| |
| **Trap 1: Using the mock framework to intercept a core language stream.** |
| |
| **Don't:** |
| |
| ```python |
| @mock.patch("sys.stdout", new_callable=io.StringIO) |
| def test_something(self, mock_stdout): |
| # test logic |
| ``` |
| |
| **Do:** |
| |
| ```python |
| import contextlib |
| import io |
| |
| def test_something(): |
| with contextlib.redirect_stdout(io.StringIO()) as stdout: |
| # test logic |
| ``` |
| |
| **Trap 2: Creating the string buffer variable outside the context manager scope |
| unnecessarily.** |
| |
| **Don't:** |
| |
| ```python |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| _run_status(manifest, ["-o"]) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| with contextlib.redirect_stdout(io.StringIO()) as stdout: |
| _run_status(manifest, ["-o"]) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-06: Direct Helper Functions Over Fixture Factories |
| |
| > **Rule:** Avoid defining dummy `pytest` fixtures solely to return nested |
| > function callbacks; use straightforward module-level helpers instead. |
| > |
| > **What:** Do not abuse the Pytest fixture system to generate and return local |
| > functions. If a utility does not benefit from the fixture lifecycle management |
| > (setup/teardown), it should be a standard module-level or private helper |
| > function. |
| > |
| > **Applies To:** Pytest files requiring test data initialization or repetitive |
| > setup procedures. |
| > |
| > **Why:** Test frameworks became overly complex when developers wrapped simple |
| > initialization logic in `@pytest.fixture` annotations just to yield a callable |
| > function to the test case. Failing to adhere to this typically results in |
| > **Fixture Graph Bloat**. |
| |
| **Trap 1: Defining a pytest fixture that yields a nested function to bypass |
| global state limitations.** |
| |
| **Don't:** |
| |
| ```python |
| @pytest.fixture |
| def init_temp_git_tree(): |
| def _init_temp_git_tree(git_dir: Path) -> None: |
| # init logic |
| return _init_temp_git_tree |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def _init_temp_git_tree(git_dir: Path) -> None: |
| # init logic |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-07: Elimination of Redundant Path Assertions |
| |
| > **Rule:** Never prepend path existence assertions prior to invoking |
| > permissions or loading assertions that implicitly validate existence. |
| > |
| > **What:** Do not assert the existence of a file prior to asserting its |
| > specific permissions or state. Subsequent OS-level operations implicitly |
| > validate path existence. |
| > |
| > **Applies To:** Python unittests validating filesystem layouts, binary |
| > configurations, or hook executable permissions. |
| > |
| > **Why:** Tests contained boilerplate blocks invoking `os.path.isfile()` purely |
| > to offer a custom string format failure, even though the immediate next line |
| > invoking `os.access()` would fail correctly with an implicit lack of |
| > existence. Failing to adhere to this typically results in **Test |
| > Boilerplate**. |
| |
| **Trap 1: Chaining explicit existence checks before access checks.** |
| |
| **Don't:** |
| |
| ```python |
| self.assertTrue(os.path.isfile(repo_path), f"{repo_path} does not exist") |
| self.assertTrue(os.access(repo_path, os.X_OK), f"{repo_path} is not executable") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| self.assertTrue(os.access(repo_path, os.X_OK), f"{repo_path} is not executable") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-08: Pathlib Utility Constants for Test Path Resolution |
| |
| > **Rule:** Always resolve test filesystem boundaries using centralized |
| > `pathlib` objects instead of string-concatenating legacy `os.path` operations. |
| > |
| > **What:** Path constructions in test files should utilize modern pathlib |
| > objects via centralized utility constants (e.g., `utils_for_test.THIS_DIR`) |
| > instead of verbose `os.path` operations. |
| > |
| > **Applies To:** Python test suites interacting with the file system. |
| > |
| > **Why:** Manual path constructions using nested `os.path.dirname(__file__)` |
| > calls created verbose, hard-to-read boilerplate that cluttered the test logic. |
| > Failing to adhere to this typically results in **Verbose Code Base**. |
| |
| **Trap 1: Using nested `os.path.dirname` and `os.path.join` to traverse back to |
| the parent directory.** |
| |
| **Don't:** |
| |
| ```python |
| repo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "repo") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| repo_path = utils_for_test.THIS_DIR.parent / "repo" |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-09: Pytest Adoption for Test Suite Definition |
| |
| > **Rule:** Must author all file-level test assertions utilizing `pytest` syntax |
| > constructs over legacy `unittest` equivalents. |
| > |
| > **What:** Test files must be defined using the modern `pytest` framework, |
| > avoiding class-based `unittest.TestCase` inheritance and relying on standard |
| > `assert` semantics. |
| > |
| > **Applies To:** The entire Python testing suite. |
| > |
| > **Why:** The legacy `unittest` structure required extensive, verbose |
| > boilerplate for state setup, test case classes, and specific assertion methods |
| > that restricted readability. Failing to adhere to this typically results in |
| > **Test Verbosity**. |
| |
| **Trap 1: Creating test case classes that inherit from `unittest.TestCase` and |
| asserting via `self.assertEqual`.** |
| |
| **Don't:** |
| |
| ```python |
| import unittest |
| class RepoTests(unittest.TestCase): |
| def test_example(self): |
| self.assertEqual(a, b) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Standalone pytest functions |
| def test_example(): |
| assert a == b |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-10: Minimal Mocking Isolation for Test Reliability |
| |
| > **Rule:** Avoid deeply nested or exhaustively mocked test wrappers; isolate |
| > and invoke inner target logic primitives instead. |
| > |
| > **What:** Mock usage must be limited strictly to necessary external |
| > intercepts. Exhaustive mock environments must not be created just to drive a |
| > small piece of inner logic; instead, structural refactoring must isolate the |
| > logical boundary. |
| > |
| > **Applies To:** Test suites involving deep API structures and side-effects. |
| > |
| > **Why:** Relying on heavily nested, brittle 'house of cards' mock |
| > configurations meant tests frequently failed when unrelated code modifications |
| > broke the strict internal mock assumptions, turning tests into throwaway |
| > artifacts. Failing to adhere to this typically results in **Fragile Mock |
| > Collapse**. |
| |
| **Trap 1: Using stacked patch decorators for non-relevant sub-routines (e.g., |
| standard error outputs or colorizers) just to trigger an embedded autocorrection |
| flow.** |
| |
| **Don't:** |
| |
| ```python |
| with mock.patch("main.RepoClient", return_value=mock_client): |
| with mock.patch("main.SetDefaultColoring"): |
| repo._RunLong("tart", gopts, argv, log) |
| ``` |
| |
| **Do:** |
| |
| * Isolate the logic into `_autocorrect_command_name()` and test it by passing |
| minimal data primitives: `res = self.repo._autocorrect_command_name("tart", |
| mock_config)`. |
| |
| **Exceptions:** Testing hard, native OS-level boundary bindings. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-11: Canonical Resolution for System Library Mocks |
| |
| > **Rule:** Always patch standard libraries at their canonical origin namespace |
| > rather than targeting the local module import resolution boundary. |
| > |
| > **What:** Standard library functions must be targeted for testing patches at |
| > their canonical origin namespace rather than the local import footprint of the |
| > target module. |
| > |
| > **Applies To:** Test configuration defining `mock.patch` paths. |
| > |
| > **Why:** Patching against the local module import representation strictly |
| > coupled tests to a given file's exact inclusion syntax. Minor refactors |
| > altering module import aliases completely disabled testing verification. |
| > Failing to adhere to this typically results in **Brittle Patch Routing**. |
| |
| **Trap 1: Patching `time.sleep` by referencing the specific host application |
| file where it was locally imported.** |
| |
| **Don't:** |
| |
| ```python |
| @mock.patch("main.time.sleep") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| @mock.patch("time.sleep") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-12: Memoized Function Cache Clearing in Setup |
| |
| > **Rule:** Always flush the `lru_cache` of mocked globally-accessible memoized |
| > functions within the test initialization sequence. |
| > |
| > **What:** When mocking globally accessible functions decorated with |
| > `@functools.lru_cache`, the cache must be explicitly cleared in the test |
| > suite's `setUp` routine. |
| > |
| > **Applies To:** Unit test suites interacting with memoized functions. |
| > |
| > **Why:** Failing to clear the `lru_cache` of globally executed functions |
| > during testing caused subsequent tests in the runner to receive cached mock |
| > results injected by prior tests, leading to non-deterministic, cascading |
| > failures. Failing to adhere to this typically results in **State Contamination |
| > / Flaky Tests**. |
| |
| **Trap 1: Mocking the underlying behavior of a cached function but leaving the |
| LRU cache intact across tests.** |
| |
| **Don't:** |
| |
| ```python |
| class SshTests(unittest.TestCase): |
| def test_version(self): |
| with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"): |
| self.assertEqual(ssh.version(), (1, 2)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| class SshTests(unittest.TestCase): |
| def setUp(self): |
| ssh.version.cache_clear() |
| |
| def test_version(self): |
| with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"): |
| self.assertEqual(ssh.version(), (1, 2)) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-13: Idiomatic Standard Stream Redirection |
| |
| > **Rule:** Always employ `contextlib.redirect_stderr` directly instead of |
| > overwriting standard stream objects globally using patch frameworks. |
| > |
| > **What:** To intercept `stderr` or `stdout` in testing, utilize the standard |
| > library's context managers (`contextlib.redirect_stderr`) instead of |
| > overriding the stream objects globally using `mock.patch`. |
| > |
| > **Applies To:** Unit testing standard output logs and error streams. |
| > |
| > **Why:** Using generic mocks to replace `sys.stderr` creates brittle tests |
| > with broader side effects. The standard library provides dedicated tools |
| > strictly designed for capturing stream text safely. Failing to adhere to this |
| > typically results in **Non-Idiomatic Tooling / Brittle Mocks**. |
| |
| **Trap 1: Using `mock.patch` to manually inject a `StringIO` object over |
| `sys.stderr`.** |
| |
| **Don't:** |
| |
| ```python |
| with mock.patch("sys.stderr", new=io.StringIO()) as mock_stderr: |
| run_command() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| f = io.StringIO() |
| with contextlib.redirect_stderr(f): |
| run_command() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-14: Context Manager Nesting Consolidation |
| |
| > **Rule:** Consolidate contiguous mock or context manager deployments into |
| > single-line definitions to minimize indentation footprints. |
| > |
| > **What:** Contiguous, deeply nested context managers (`with` blocks) should be |
| > consolidated into a single `with` statement to reduce horizontal indentation |
| > and structural complexity. |
| > |
| > **Applies To:** Complex test fixtures or code paths requiring multiple |
| > synchronized states or file handles. |
| > |
| > **Why:** Individual context managers per line resulted in excessive code |
| > indentation (pyramid of doom), harming readability and consuming unnecessary |
| > screen space. Failing to adhere to this typically results in **Excessive |
| > Indentation / Pyramid of Doom**. |
| |
| **Trap 1: Adding a new level of indentation for every context manager invoked.** |
| |
| **Don't:** |
| |
| ```python |
| with mock.patch("sys.stderr") as mock_stderr: |
| with mock.patch("socket.socket") as ms: |
| run_test() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| with mock.patch("sys.stderr") as mock_stderr, mock.patch("socket.socket") as ms: |
| run_test() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-15: Anti-Change Detector Tests |
| |
| > **Rule:** Avoid exact full-string assertions when evaluating formatted logs or |
| > stringified error conditions to prevent brittle side-effect maintenance. |
| > |
| > **What:** When verifying error logging or outputs, tests must assert on |
| > flexible core substrings rather than exact line matches, and exceptions |
| > configured as mock side effects should use class types rather than |
| > instantiated string objects. |
| > |
| > **Applies To:** Unit testing string outputs, logs, and simulated mock |
| > failures. |
| > |
| > **Why:** Tests acting as 'change detectors' failed unnecessarily when minor |
| > formatting, capitalization, or prefix strings (e.g., 'WARNING' vs 'warning') |
| > were updated in the underlying code, despite the core logic remaining |
| > functional. Failing to adhere to this typically results in **Brittle Tests / |
| > Extraneous Boilerplate**. |
| |
| **Trap 1: Asserting exact equality of string outputs including standardized |
| prefixes.** |
| |
| **Don't:** |
| |
| ```python |
| self.assertEqual(stderr.getvalue(), "repo: warning: git trace2 logging failed: Mock error") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| self.assertIn("git trace2 logging failed", stderr.getvalue()) |
| ``` |
| |
| **Trap 2: Instantiating an exception with a dummy string for a simple |
| side-effect mock.** |
| |
| **Don't:** |
| |
| ```python |
| mock_socket.connect.side_effect = OSError("Mock error") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| mock.patch("socket.socket", side_effect=OSError) |
| ``` |
| |
| **Exceptions:** Tests explicitly verifying exact formatting serialization (e.g., |
| JSON schema adherence). |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-16: Infrastructure Prebuilts via vpython |
| |
| > **Rule:** Must execute continuous integration environments using `vpython` |
| > wrappers instead of relying on legacy source-compiling orchestration |
| > frameworks like `pyenv`. |
| > |
| > **What:** Leverage `vpython` for multi-version Python testing rather than |
| > `tox` combined with `pyenv` to utilize prebuilt binaries and minimize |
| > environment setup overhead. |
| > |
| > **Applies To:** Hermetic testing setups, CI pipelines, and multi-version |
| > coverage scripts. |
| > |
| > **Why:** Using `pyenv` and `tox` forced CI and local test runs to download and |
| > compile Python versions from source on the fly, adding minutes of overhead to |
| > every test run. Shifting to `vpython` leveraged prebuilt infrastructure |
| > runtimes. Failing to adhere to this typically results in **Slow CI Builds**. |
| |
| **Trap 1: Relying on tools that dynamically build Python from source to test |
| across multiple versions.** |
| |
| **Don't:** |
| |
| * Using `tox.ini` with `pyenv` to orchestrate matrix testing across Python |
| versions, incurring source-compilation penalties. |
| |
| **Do:** |
| |
| * Invoking tests via `vpython`, which provisions hermetic virtual environments |
| rapidly using precompiled infrastructure binaries. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-17: Modernized Filesystem Paths via Pathlib |
| |
| > **Rule:** Always employ `pathlib` objects for mock file I/O operations rather |
| > than manual string handling and context managers. |
| > |
| > **What:** Test case file system operations should use the modern `pathlib` |
| > library rather than legacy `os.path` and manual file handles. |
| > |
| > **Applies To:** Unit tests and file system operations across the codebase. |
| > |
| > **Why:** Legacy tests relied heavily on `os.path.join` and `with open(...)` |
| > which led to verbose, repetitive boilerplate when constructing mock repository |
| > states. Failing to adhere to this typically results in **Verbose Boilerplate / |
| > Maintainability Overhead**. |
| |
| **Trap 1: Using `os.path.join` and `with open(..., 'w')` for basic text file |
| creation in tests.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Verbose manual file writing |
| root_m = os.path.join(self.manifest_dir, "root.xml") |
| with open(root_m, "w") as fp: |
| fp.write("<manifest>...") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Pathlib usage for concise writes |
| root_m = self.manifest_dir / "root.xml" |
| root_m.write_text("<manifest>...") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-18: Standardized Module-Level Mock Imports |
| |
| > **Rule:** Import external test dependencies and internal domain targets as |
| > complete module spaces rather than cherry-picking precise classes. |
| > |
| > **What:** Test files must import `mock` as a module (`from unittest import |
| > mock`) and internal targets as a module (`from subcmds import wipe`), rather |
| > than importing specific classes directly. |
| > |
| > **Applies To:** All unit tests. |
| > |
| > **Why:** Direct class imports caused namespace pollution and deviated from the |
| > Google Python Style Guide, making it harder to track the origin of |
| > dependencies. Failing to adhere to this typically results in **Style Guide |
| > Violation / Namespace Pollution**. |
| |
| **Trap 1: Importing specific classes or exceptions directly from test targets or |
| standard libraries.** |
| |
| **Don't:** |
| |
| ```python |
| from unittest.mock import MagicMock |
| from subcmds.wipe import Wipe, WipeError |
| |
| mock_obj = MagicMock() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| from unittest import mock |
| from subcmds import wipe |
| |
| mock_obj = mock.MagicMock() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-19: Pytest Adoption for New Test Modules |
| |
| > **Rule:** Must restrict the creation of new `unittest.TestCase` files strictly |
| > unless explicitly bypassing native pytest infrastructure constraints. |
| > |
| > **What:** New test modules must be written using the `pytest` framework rather |
| > than the legacy `unittest` structure. |
| > |
| > **Applies To:** All newly added test files. |
| > |
| > **Why:** The project was modernizing its testing infrastructure, aiming to |
| > eliminate `unittest` boilerplate and leverage native `pytest` fixtures for |
| > global state management. Failing to adhere to this typically results in |
| > **Technical Debt / Legacy Test Framework Propagation**. |
| |
| **Trap 1: Creating subclasses of `unittest.TestCase` and using |
| `self.assertEqual`.** |
| |
| **Don't:** |
| |
| ```python |
| class WipeUnitTest(unittest.TestCase): |
| def test_wipe(self): |
| self.assertTrue(os.path.exists(path)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def test_wipe(tmp_path): |
| assert os.path.exists(path) |
| ``` |
| |
| **Exceptions:** Unless a compelling reason exists requiring native `unittest` |
| mechanics. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-20: High-Fidelity Test Targets over Permissive Mocks |
| |
| > **Rule:** Avoid stubbing robust architectural models like `Project` or |
| > `Manifest` via permissive `MagicMock` allocations; instantiate genuine domain |
| > classes instead. |
| > |
| > **What:** Tests should instantiate and utilize real domain objects (e.g., the |
| > `Project` class) rather than relying heavily on generic `MagicMock` instances, |
| > to ensure test fidelity. |
| > |
| > **Applies To:** Unit testing, particularly for core models like `Project` or |
| > `Manifest`. |
| > |
| > **Why:** Overuse of `MagicMock` led to tests passing despite broken |
| > implementations because the mocks lacked side-effects and realistic attribute |
| > validation. Failing to adhere to this typically results in **False Positive |
| > Test Results**. |
| |
| **Trap 1: Using `MagicMock` to bypass instantiating core models.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Mocks won't fail if the API changes |
| proj = mock.MagicMock() |
| proj.name = "test" |
| proj.DeleteWorktree.side_effect = lambda: True |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Using actual domain objects for accurate side-effects |
| proj = Project(manifest=mock_manifest, name="test", ...) |
| ``` |
| |
| **Exceptions:** External network calls or complex, deeply decoupled subsystems |
| where instantiating the real object is prohibitively difficult. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-21: Session-Scoped Hermetic Git Identity Injection |
| |
| > **Rule:** Must inject pseudo Git commit identities as session-scoped |
| > environment variables rather than manually rewriting local Git configurations |
| > per test. |
| > |
| > **What:** Automated test suites must globally stub Git's author and committer |
| > identities using session-scoped environment variables to ensure execution in |
| > clean or unconfigured environments. |
| > |
| > **Applies To:** Pytest configuration (`tests/conftest.py`) and all tests |
| > executing raw Git subprocess commands. |
| > |
| > **Why:** Tests interacting with local Git repositories crashed with |
| > `GitCommandError` on CI runners or clean developer machines lacking a global |
| > `user.name` and `user.email`. Patching this locally in tests via `git config` |
| > calls introduced I/O overhead and redundant code. Failing to adhere to this |
| > typically results in **GitCommandError / Test Flakiness**. |
| |
| **Trap 1: Executing `git config` inside individual test setup blocks to fake a |
| user identity.** |
| |
| **Don't:** |
| |
| ```python |
| def test_commit(self): |
| subprocess.run(["git", "config", "user.name", "Foo Bar"]) |
| subprocess.run(["git", "config", "user.email", "foo@bar.com"]) |
| # Run test logic |
| ``` |
| |
| **Do:** |
| |
| ```python |
| @pytest.fixture(autouse=True, scope="session") |
| def setup_user_identity(monkeysession): |
| """Set env variables for author and committer name and email.""" |
| monkeysession.setenv("GIT_AUTHOR_NAME", "Foo Bar") |
| monkeysession.setenv("GIT_COMMITTER_NAME", "Foo Bar") |
| monkeysession.setenv("GIT_AUTHOR_EMAIL", "foo@bar.baz") |
| monkeysession.setenv("GIT_COMMITTER_EMAIL", "foo@bar.baz") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-22: Automated Lifecycle Management for Test Filesystem Artifacts |
| |
| > **Rule:** Must utilize automated `TemporaryDirectory` constructs integrated |
| > securely into test teardown sequences instead of performing unmanaged manual |
| > disk sweeps. |
| > |
| > **What:** Test fixtures must use state-managed temporary directory constructs |
| > (e.g., `tempfile.TemporaryDirectory`) whose lifecycle is explicitly bound to |
| > standard unit test teardown phases, avoiding manual unmanaged path allocation |
| > and raw removal. |
| > |
| > **Applies To:** All test suites performing filesystem mock operations or Git |
| > repository initialization (`tests/test_subcmds_forall.py`). |
| > |
| > **Why:** Tests were manually allocating paths via `tempfile.mkdtemp` and |
| > issuing raw `shutil.rmtree` calls. This pattern proved fragile, resulting in |
| > leaked directories across runs or skipped cleanup phases if a test failed |
| > early. Failing to adhere to this typically results in **Orphaned Artifacts / |
| > Resource Leaks**. |
| |
| **Trap 1: Spawning detached temporary directories and manually maintaining |
| cleanup blocks.** |
| |
| **Don't:** |
| |
| ```python |
| def setUp(self): |
| self.tempdir = tempfile.mkdtemp() |
| |
| def tearDown(self): |
| shutil.rmtree(self.tempdir, ignore_errors=True) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def setUp(self): |
| self.tempdirobj = tempfile.TemporaryDirectory(prefix="forall_tests") |
| self.tempdir = self.tempdirobj.name |
| |
| def tearDown(self): |
| self.tempdirobj.cleanup() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-23: Idiomatic String Evaluation in Mocked Test Streams |
| |
| > **Rule:** Avoid manually slicing and measuring length properties for string |
| > evaluations; consistently evaluate standard streams using built-in truthiness |
| > routines. |
| > |
| > **What:** Evaluating outputs from captured stdout or stderr in tests must |
| > utilize Pythonic abstractions like `splitlines()` and implicit truthiness |
| > evaluation, rather than raw newline splitting and explicit length checks. |
| > |
| > **Applies To:** Hermetic Test suites executing mock command line calls and |
| > inspecting `sys.stdout`. |
| > |
| > **Why:** Legacy test code relied on manual string manipulation like |
| > `split("\n")` and boolean evaluations like `len(line) > 0`. This approach was |
| > unidiomatic, fragile against cross-platform line endings, and bloated test |
| > suite verbosity. Failing to adhere to this typically results in **Fragile |
| > Tests / Unidiomatic Logic**. |
| |
| **Trap 1: Iterating over raw string splits and manually validating line length |
| to count valid output strings.** |
| |
| **Don't:** |
| |
| ```python |
| line_count = 0 |
| for line in mock_stdout.getvalue().split("\n"): |
| if len(line) > 0: |
| line_count += 1 |
| ``` |
| |
| **Do:** |
| |
| ```python |
| line_count = sum(1 if x else 0 for x in mock_stdout.getvalue().splitlines()) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-24: Hermetic Isolation of Git Configuration Directories in Tests |
| |
| > **Rule:** Always isolate Git initialization by injecting temporary dummy |
| > directories via environment overrides (`HOME` / `USERPROFILE`) to restrict |
| > test interference. |
| > |
| > **What:** Tests utilizing local git operations must hermetically isolate |
| > environment variables like `HOME` and `USERPROFILE` to prevent developers' |
| > global configurations from leaking into test executions. |
| > |
| > **Applies To:** Global test initialization routines (`conftest.py`, Pytest |
| > fixtures, class-level test setup). |
| > |
| > **Why:** Historically, if a developer's global `.gitconfig` required GPG |
| > signing (`commit.gpgsign=true`), a git subprocess spawned during test setup |
| > would unexpectedly open an interactive prompt (e.g., vim) to solicit a signing |
| > key, causing the test suite to deadlock permanently. Failing to adhere to this |
| > typically results in **Test Deadlock / State Bleed**. |
| |
| **Trap 1: Relying on default system paths for the home directory during test |
| execution.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Allows test subprocesses to read ~/.gitconfig |
| subprocess.run(['git', 'tag', '--annotate']) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Override environment variables to an isolated temp directory |
| @pytest.fixture(autouse=True, scope="session") |
| def alt_home(tmp_path_factory, monkeysession): |
| var = "USERPROFILE" if platform_utils.isWindows() else "HOME" |
| monkeysession.setenv(var, str(tmp_path_factory.mktemp("home"))) |
| ``` |
| |
| **Exceptions:** Tests that explicitly exist to validate the parsing logic of a |
| user's real, existing `.gitconfig` (which is highly discouraged). |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-25: Scope Hierarchy Enforcement in Pytest Fixtures |
| |
| > **Rule:** Never propagate lower-scoped test fixtures (e.g., function-scoped |
| > states) into global session-scoped initialization procedures. |
| > |
| > **What:** A session-scoped Pytest fixture cannot depend on lower-scoped |
| > fixtures (such as a function-scoped `tmp_path`). To create temporary paths at |
| > the session level, `tmp_path_factory.mktemp` must be utilized. |
| > |
| > **Applies To:** Pytest fixture definitions, specifically within `conftest.py`. |
| > |
| > **Why:** A session-scoped setup mechanism attempted to use the function-scoped |
| > `tmp_path` fixture. This caused pytest initialization failures. Furthermore, |
| > reusing function-scoped directories for session-level state caused unintended |
| > disk thrashing as the directory was continually wiped at the teardown of every |
| > function. Failing to adhere to this typically results in **Scope Mismatch / |
| > State Masking**. |
| |
| **Trap 1: Injecting `tmp_path` into a fixture decorated with |
| `scope="session"`.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Session fixture depending on function fixture |
| @pytest.fixture(scope="session") |
| def session_tmp_homedir(tmp_path): |
| return tmp_path |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Using the factory to generate a session-level path |
| @pytest.fixture(scope="session") |
| def session_tmp_homedir(tmp_path_factory): |
| return tmp_path_factory.mktemp("home") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-26: Strict Python Version Compatibility for Third-Party Plugins |
| |
| > **Rule:** Must strictly align test dependency injection configurations against |
| > the underlying project's absolute minimum supported Python versions. |
| > |
| > **What:** Test tooling dependencies must strictly align with the project's |
| > minimum supported Python version. External plugin implementation logic should |
| > be inlined with proper attribution if the plugin requires a newer Python |
| > version than the project supports. |
| > |
| > **Applies To:** Dependency management (`tox.ini`), `conftest.py`, and CI |
| > environments. |
| > |
| > **Why:** A developer introduced a new Pytest plugin to manage isolated home |
| > directories. However, the plugin required Python 3.8+, while the project |
| > maintained compatibility down to Python 3.6+. This mismatch caused post-submit |
| > CI pipeline checks to crash upon test environment installation. Failing to |
| > adhere to this typically results in **CI Installation Failure**. |
| |
| **Trap 1: Adding modern third-party test dependencies to legacy-supported |
| projects without validating the minimum required Python constraints.** |
| |
| **Don't:** |
| |
| ```ini |
| # BAD: tox.ini adds dependency unsupported by CI target |
| deps = |
| pytest |
| pytest-home >= 0.4 |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Inline the logic and remove the external dependency |
| # adapted from pytest-home 0.5.1 |
| @pytest.fixture(autouse=True, scope="session") |
| def session_tmp_homedir(tmp_path_factory, monkeysession): |
| # implementation logic here |
| ``` |
| |
| **Exceptions:** When the project's minimum supported Python version is |
| officially bumped to match the plugin's requirement. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-27: Hermetic Isolation of Terminal UI Color Configurations |
| |
| > **Rule:** Must aggressively lock default terminal coloring states when |
| > generating UI tests to isolate regressions from variable environment |
| > pipelines. |
| > |
| > **What:** Unit tests that assert on UI or logging text containing ANSI color |
| > codes must explicitly force the coloring state to prevent ambient git |
| > configurations or CI terminal limitations from altering test outcomes. |
| > |
| > **Applies To:** Unit tests interacting with the `color` or `repo_logging` |
| > modules. |
| > |
| > **Why:** Tests validating color outputs failed non-deterministically based on |
| > the ambient environment (e.g., CI runners lacking a TTY, or developers having |
| > `color.ui=false` in their global `.gitconfig`). Failing to adhere to this |
| > typically results in **Flaky Tests / Non-Deterministic Failures**. |
| |
| **Trap 1: Relying on the default state of the `Coloring` class without forcing |
| it during setup, implicitly depending on environmental variables.** |
| |
| **Don't:** |
| |
| ```python |
| def setUp(self): |
| config_fixture = fixture("test.gitconfig") |
| self.config = git_config.GitConfig(config_fixture) |
| self.color = color.Coloring(self.config, "status") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def setUp(self): |
| config_fixture = fixture("test.gitconfig") |
| self.config = git_config.GitConfig(config_fixture) |
| color.SetDefaultColoring("always") |
| self.color = color.Coloring(self.config, "status") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T5-28: Abstract Intent Documentation in Unit Tests |
| |
| > **Rule:** Never embed exact literal assertions or explicit structural values |
| > directly inside unit test docstring commentary. |
| > |
| > **What:** Test docstrings should describe the behavioral intent or structural |
| > coverage of the test rather than hardcoding the exact assertion values or |
| > low-level implementation details. |
| > |
| > **Applies To:** Python `unittest` module docstrings. |
| > |
| > **Why:** Docstrings that hardcoded exact ANSI escape sequences or config |
| > values became stale and misleading whenever the underlying mappings or |
| > formatting codes were updated. Failing to adhere to this typically results in |
| > **Documentation Rot / Maintenance Overhead**. |
| |
| **Trap 1: Documenting exact data values being asserted directly in the test |
| docstring.** |
| |
| **Don't:** |
| |
| ```python |
| def test_Color_Parse_first_parameter_none(self): |
| """fg is black(30), bg is red(31+10=41), attr is ul(4)""" |
| val = self.color._parse(None, 'black', 'red', 'ul') |
| self.assertEqual('\x1b[4;30;41m', val) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def test_Color_Parse_first_parameter_none(self): |
| """check fg & bg & attr""" |
| val = self.color._parse(None, "black", "red", "ul") |
| self.assertEqual("\x1b[4;30;41m", val) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T2 | Filesystem Atomicity & Worktree Layout - *Managing safe |
| creation and atomic removal of temporary worktrees and `.git` directories |
| supports reliable test state fixtures.* |
| * **Downstream:** T3 | Subprocess Git Integration & Error Translation - |
| *Hermetic test constraints directly dictate how standard git streams, |
| configuration injections, and subprocess executions are mocked or |
| intercepted.* |
| * **Downstream:** T6 | CLI Argument Parsing & UX Consistency - *Strict |
| terminal environment control in testing enforces deterministic verification |
| of CLI autocorrect flows and standardized UI colorizations.* |
| |
| ## Chapter: CLI Argument Parsing & UX Consistency |
| |
| **Context:** This chapter governs the lifecycle, validation, and execution of |
| command-line arguments, enforcing strict standardization for machine-readable |
| serialization, unified logging, and deterministic, thread-safe terminal |
| interactions. |
| |
| ### Summary |
| |
| | Rule ID | Principle / | Priority | Primary Symptom / | |
| : : Constraint : : Trap : |
| | :-------- | :--------------- | :------- | :-------------------- | |
| | **T6-01** | Deterministic | High | Providing only | |
| : : Machine-Readable : : aesthetically : |
| : : Output Formats : : formatted terminal : |
| : : : : text for diagnostic : |
| : : : : commands. : |
| | **T6-02** | Elimination of | Medium | Assigning a short | |
| : : Niche Short : : option to a feature : |
| : : Options : : that isn't commonly : |
| : : : : used by developers. : |
| | **T6-03** | Extensible | Medium | Creating rigid, | |
| : : Boolean Toggle : : single-purpose : |
| : : Flags : : boolean switches for : |
| : : : : granular output : |
| : : : : components. : |
| | **T6-04** | Context-Aware | Medium | Hardcoding visual | |
| : : CLI Output : : formatting elements : |
| : : Separators : : directly after a : |
| : : : : primary block instead : |
| : : : : of conditionally : |
| : : : : checking the next : |
| : : : : block. : |
| | **T6-05** | Output Format | Medium | Handling output | |
| : : Standardization : : formatting via raw : |
| : : via Enums : : string matching and : |
| : : : : `print()` statements : |
| : : : : without suppressing : |
| : : : : the interactive : |
| : : : : pager. : |
| | **T6-06** | Symmetric | Medium | Mixing one output | |
| : : Execution : : format's dispatch : |
| : : Dispatching : : call with the inline : |
| : : : : procedural logic of : |
| : : : : the fallback format. : |
| | **T6-07** | CLI Argument | Medium | Overriding a | |
| : : Simulation over : : configuration : |
| : : Internal State : : parameter directly on : |
| : : Mutation : : the parsed namespace : |
| : : : : object. : |
| | **T6-08** | Deterministic | Medium | Iterating over a list | |
| : : Sorting of : : of accumulated : |
| : : Parallel : : multi-process results : |
| : : Execution : : without sorting. : |
| : : Results : : : |
| | **T6-09** | Strict Adherence | Medium | Adding a | |
| : : to Feature : : `--force-jobs` CLI : |
| : : Requests : : bypass flag : |
| : : : : simultaneously : |
| : : : : alongside the new : |
| : : : : `sync-j-max` manifest : |
| : : : : attribute. : |
| | **T6-10** | Minimal Logic in | High | Placing the entire | |
| : : Exception : : typo-correction : |
| : : Handlers : : logic, user prompts, : |
| : : : : and command reloading : |
| : : : : inside `except : |
| : : : : KeyError\:`. : |
| | **T6-11** | Unified Logging | Medium | Outputting warnings | |
| : : Protocol via : : directly to the : |
| : : Standard Logger : : `sys.stderr` stream : |
| : : : : using the built-in : |
| : : : : `print` function. : |
| | **T6-12** | Strict | High | Wrapping string-typed | |
| : : Translation of : : configuration : |
| : : Configuration : : variables inside : |
| : : Booleans : : `int()` and catching : |
| : : : : value exceptions to : |
| : : : : deduce defaults. : |
| | **T6-13** | Interruptibility | Critical | Placing raw time | |
| : : of Interactive : : suspension code : |
| : : Runtime Delays : : before a return : |
| : : : : statement without : |
| : : : : wrapping it in a trap : |
| : : : : for cancellation : |
| : : : : overrides. : |
| | **T6-14** | Strict | Critical | Consuming CLI option | |
| : : Pre-Access : : flags before the : |
| : : Initialization : : shared initialization : |
| : : of CLI Options : : pipeline populates : |
| : : : : default properties. : |
| | **T6-15** | Omission of | Medium | Redundantly | |
| : : Redundant : : configuring : |
| : : Optparse : : destination variables : |
| : : Destinations : : in CLI arguments. : |
| | **T6-16** | Subcommand | Medium | Writing | |
| : : Scoping in : : global-sounding : |
| : : Commit Messages : : commit messages for : |
| : : : : isolated subcommand : |
| : : : : patches. : |
| | **T6-17** | Module-Scoped | Medium | Redundantly prefixing | |
| : : Exception Naming : : the exception name : |
| : : : : with the module name : |
| : : : : inside the module : |
| : : : : definition. : |
| | **T6-18** | Explicit | High | Using terms like | |
| : : Terminology in : : 'worktree' in help : |
| : : Destructive CLI : : text without explicit : |
| : : Commands : : definition or : |
| : : : : examples. : |
| | **T6-19** | Fail-Fast | Medium | Checking for missing | |
| : : Argument : : positional arguments : |
| : : Validation in : : directly inside : |
| : : Subcommands : : `Execute()`. : |
| | **T6-20** | Respect Global | Medium | Unconditionally | |
| : : Verbosity Flags : : printing progress : |
| : : : : messages to stderr or : |
| : : : : stdout. : |
| | **T6-21** | Thread-Safe | Critical | Directly querying | |
| : : Terminal : : `sys.stderr.fileno()` : |
| : : Querying during : : inside long-running : |
| : : IO Capture : : concurrent loops. : |
| | **T6-22** | Global State via | Medium | Adding environment | |
| : : Persistent : : variable mappings : |
| : : Configuration : : into command-line : |
| : : over Environment : : parsers to support : |
| : : Variables : : 'headless' : |
| : : : : configurations. : |
| | **T6-23** | CLI Command | Medium | Adding project-wide | |
| : : Parity with Git : : or environmental : |
| : : Semantics : : metadata queries to : |
| : : : : commands designed for : |
| : : : : local working-tree : |
| : : : : analysis. : |
| | **T6-24** | Context-Aware | Medium | Triggering an | |
| : : Sync Operation : : immediate system exit : |
| : : Termination : : on an update failure. : |
| | **T6-25** | Explicit Exit | High | Using an empty | |
| : : Codes on CLI : : `return` to back out : |
| : : Cancellation : : of a declined CLI : |
| : : : : confirmation prompt. : |
| | **T6-26** | Non-Interactive | Medium | Hardcoding | |
| : : Safeties for : : interactive prompts : |
| : : Destructive : : without checking : |
| : : Commands : : configuration flags : |
| : : : : for automation modes. : |
| | **T6-27** | Strict | Medium | Accepting dependent | |
| : : Interdependent : : modifier options : |
| : : Argument : : unconditionally : |
| : : Validation : : without checking : |
| : : : : prerequisites. : |
| | **T6-28** | Actionable CLI | Medium | Failing a sync | |
| : : Error Messages : : operation with a : |
| : : for : : descriptive but : |
| : : Synchronization : : non-actionable error : |
| : : Failures : : message. : |
| | **T6-29** | Launcher Version | High | Modifying the | |
| : : Bumping for : : standalone launcher : |
| : : Important : : script with important : |
| : : Changes : : changes without : |
| : : : : incrementing its : |
| : : : : VERSION tuple. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T6-01: Deterministic Machine-Readable Output Formats |
| |
| > **Rule:** Always expose strict, structured serialization options (e.g., JSON) |
| > for CLI tools used in automated pipelines to prevent reliance on brittle text |
| > parsing. |
| > |
| > **What:** CLI tools leveraged for inspection must expose strict, structured |
| > serialization (e.g., JSON) to negate reliance on unstable text parsing in |
| > CI/CD environments. |
| > |
| > **Applies To:** Command-line interface design, specifically subcommands |
| > utilized by build pipelines or external orchestrators. |
| > |
| > **Why:** Downstream automated pipelines were forced to retrieve configuration |
| > data by executing tools like `repo info` and using string matching (`grep`) |
| > against human-readable formatting, creating a brittle dependency susceptible |
| > to UI modifications. Failing to adhere to this typically results in **Pipeline |
| > Parsing Failure**. |
| |
| **Trap 1: Providing only aesthetically formatted terminal text for diagnostic |
| commands.** |
| |
| **Don't:** |
| |
| * Command executes and prints human-readable strings. Consumers must run `repo |
| info | grep "Manifest revision"` to extract variables. |
| |
| **Do:** |
| |
| * Integrate a `--format=json` command argument that bypasses human-readable |
| display logic and cleanly serializes underlying data models directly to |
| standard output. |
| |
| **Exceptions:** Interactive-only terminal prompts that are heavily stateful and |
| fundamentally incompatible with automation. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-02: Elimination of Niche Short Options |
| |
| > **Rule:** Never assign single-character short options for niche or rarely used |
| > flags to preserve namespace availability. |
| > |
| > **What:** Avoid assigning single-character short options for niche or rarely |
| > used command-line flags to prevent namespace bloat. |
| > |
| > **Applies To:** CLI argument parsing (e.g., `optparse` or `argparse` |
| > definitions). |
| > |
| > **Why:** Short options were being added arbitrarily for specific data |
| > summaries, leading to a cluttered short-option namespace and hindering future |
| > expansion. Failing to adhere to this typically results in **Cluttered CLI |
| > UX**. |
| |
| **Trap 1: Assigning a short option to a feature that isn't commonly used by |
| developers.** |
| |
| **Don't:** |
| |
| ```python |
| p.add_option("-s", "--summary", action="store_true") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| p.add_option("--include-summary", action="store_true") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-03: Extensible Boolean Toggle Flags |
| |
| > **Rule:** Always implement paired positive/negative toggle flags for granular |
| > outputs rather than relying on rigid, single-purpose boolean switches. |
| > |
| > **What:** Conditional output sections must be controlled by paired |
| > positive/negative toggle flags (e.g., `--include-x` and `--no-include-x`) |
| > rather than single-purpose boolean flags. |
| > |
| > **Applies To:** CLI design and option definitions for output manipulation. |
| > |
| > **Why:** Rigid, single-purpose flags for rendering specific summaries limited |
| > extensibility and prevented developers from precisely tailoring outputs for |
| > scripts or automation. Failing to adhere to this typically results in **Rigid |
| > Output Rendering**. |
| |
| **Trap 1: Creating rigid, single-purpose boolean switches for granular output |
| components.** |
| |
| **Don't:** |
| |
| ```python |
| p.add_option("--summary", action="store_true", help="show only manifest summary") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| p.add_option("--include-summary", action="store_true", default=True) |
| p.add_option("--no-include-summary", dest="include_summary", action="store_false") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-04: Context-Aware CLI Output Separators |
| |
| > **Rule:** Must conditionally verify that succeeding content blocks will render |
| > before printing visual formatting elements like horizontal separators. |
| > |
| > **What:** Structural visual elements, such as horizontal separators or |
| > newlines, must only be printed if the succeeding content block is actually |
| > verified to render. |
| > |
| > **Applies To:** CLI output rendering logic. |
| > |
| > **Why:** Output formatting logic printed hardcoded line separators after |
| > summaries, resulting in dangling visual artifacts when subsequent project |
| > details were dynamically excluded by user flags. Failing to adhere to this |
| > typically results in **Redundant Output Noise**. |
| |
| **Trap 1: Hardcoding visual formatting elements directly after a primary block |
| instead of conditionally checking the next block.** |
| |
| **Don't:** |
| |
| ```python |
| print_summary() |
| print_separator() |
| if include_projects: |
| print_projects() |
| ``` |
| |
| **Do:** |
| |
| ```python |
| print_summary() |
| if not include_projects: |
| return |
| print_separator() |
| print_projects() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-05: Output Format Standardization via Enums |
| |
| > **Rule:** Must manage and standardize target output formats using an explicit |
| > Python `Enum` class rather than brittle string matching. |
| > |
| > **What:** Command-line output formats (like TEXT or JSON) must be managed |
| > using an explicit Python Enum class and standardize formatting behaviors |
| > (e.g., disabling pagers for JSON, enforcing sort keys). |
| > |
| > **Applies To:** CLI data serialization and `OutputFormat` dispatching. |
| > |
| > **Why:** Unstandardized JSON serialization methods across subcommands resulted |
| > in inconsistent output spacing, failure to suppress interactive pagers, and |
| > brittle string-based format matching. Failing to adhere to this typically |
| > results in **Broken Machine Automation**. |
| |
| **Trap 1: Handling output formatting via raw string matching and `print()` |
| statements without suppressing the interactive pager.** |
| |
| **Don't:** |
| |
| ```python |
| if opt.format == "json": |
| print(json.dumps(data)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| class OutputFormat(enum.Enum): |
| TEXT = enum.auto() |
| JSON = enum.auto() |
| |
| if output_format == OutputFormat.JSON: |
| sys.stdout.write(json.dumps(data, indent=2, sort_keys=True) + "\n") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-06: Symmetric Execution Dispatching |
| |
| > **Rule:** Must symmetrically separate procedural logic into format-specific |
| > handlers rather than mixing inline logic with format conditional statements. |
| > |
| > **What:** Complex commands handling multiple output formats must keep the main |
| > execution function clean by symmetrically dispatching logic to format-specific |
| > handlers (e.g., `_ExecuteText` and `_ExecuteJson`). |
| > |
| > **Applies To:** Command entry points (e.g., the `Execute` method in subcommand |
| > classes). |
| > |
| > **Why:** Inline output rendering mixed with conditional format dispatching |
| > created monolithic execution methods that were difficult to maintain and test. |
| > Failing to adhere to this typically results in **Spaghetti Logic**. |
| |
| **Trap 1: Mixing one output format's dispatch call with the inline procedural |
| logic of the fallback format.** |
| |
| **Don't:** |
| |
| ```python |
| def Execute(self, opt, args): |
| if opt.format == 'json': |
| return self._ExecuteJson(opt, args) |
| # 50 lines of inline text rendering logic... |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def Execute(self, opt, args): |
| if opt.format == OutputFormat.JSON: |
| self._ExecuteJson(opt, args) |
| else: |
| self._ExecuteText(opt, args) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-07: CLI Argument Simulation over Internal State Mutation |
| |
| > **Rule:** Always inject simulated execution parameters into the command-line |
| > `argv` arguments list rather than manually overwriting parsed attribute states |
| > during tests. |
| > |
| > **What:** When executing a command programmatically within a test, operational |
| > parameters (e.g., job counts) must be injected into the simulated `argv` |
| > string list rather than manually mutating the parsed options object. |
| > |
| > **Applies To:** Command testing and execution harnesses where parameters alter |
| > system behavior (e.g., multiprocessing threads). |
| > |
| > **Why:** Test cases were bypassing the CLI parser and manually setting |
| > `opts.jobs = 1` to force single-threaded execution. This prevented the test |
| > from exercising the command's full option-parsing and validation lifecycle. |
| > Failing to adhere to this typically results in **Test Invalidation / False |
| > Positives**. |
| |
| **Trap 1: Overriding a configuration parameter directly on the parsed namespace |
| object.** |
| |
| **Don't:** |
| |
| ```python |
| opts, args = cmd.OptionParser.parse_args(argv) |
| cmd.CommonValidateOptions(opts, args) |
| opts.jobs = 1 |
| cmd.Execute(opts, args) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| opts, args = cmd.OptionParser.parse_args(argv + ["--jobs=1"]) |
| cmd.CommonValidateOptions(opts, args) |
| cmd.Execute(opts, args) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-08: Deterministic Sorting of Parallel Execution Results |
| |
| > **Rule:** Must explicitly sort accumulated payload data from asynchronous |
| > thread loops before broadcasting standard outputs or logs. |
| > |
| > **What:** Elements collected asynchronously from a multiprocessing pool must |
| > be explicitly sorted before being emitted to standard output or logging |
| > pipelines. |
| > |
| > **Applies To:** Command-line output rendering and any loop iterating over |
| > aggregated parallel results. |
| > |
| > **Why:** Warning messages regarding bloated project directories were printed |
| > based on the order that asynchronous threads returned their payload, leading |
| > to flaky UX and non-deterministic command outputs. Failing to adhere to this |
| > typically results in **Non-Deterministic Console Output**. |
| |
| **Trap 1: Iterating over a list of accumulated multi-process results without |
| sorting.** |
| |
| **Don't:** |
| |
| ```python |
| for project_name in self._bloated_projects: |
| logger.warning(f'warning: Project {project_name} is bloated.') |
| ``` |
| |
| **Do:** |
| |
| ```python |
| for project_name in sorted(self._bloated_projects): |
| logger.warning(f'warning: Project {project_name} is bloated.') |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-09: Strict Adherence to Feature Requests |
| |
| > **Rule:** Never implement speculative bypass flags, preemptive CLI overrides, |
| > or configurations without a documented and verifiable user request. |
| > |
| > **What:** Avoid preemptively implementing fallback features, command-line |
| > overrides, or configurations without a documented user request. |
| > |
| > **Applies To:** CLI argument parsers and manifest parsing engines. |
| > |
| > **Why:** Speculative feature additions, like bypass flags for manifest limits, |
| > caused feature creep and bloated the CLI toolset with unused, undocumented |
| > edge cases. Failing to adhere to this typically results in **Feature Creep**. |
| |
| **Trap 1: Adding a `--force-jobs` CLI bypass flag simultaneously alongside the |
| new `sync-j-max` manifest attribute.** |
| |
| **Don't:** |
| |
| * Implementing an override flag in `main.py` just in case users want to bypass |
| the newly added manifest restriction. |
| |
| **Do:** |
| |
| * Deploying the manifest constraint directly without a CLI override, and |
| documenting it in `docs/manifest-format.md`. Wait for a bug tracker request |
| for an override flag. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-10: Minimal Logic in Exception Handlers |
| |
| > **Rule:** Must restrict `try...except` blocks entirely to targeted error |
| > handling and never bury structural flow control or terminal loops within them. |
| > |
| > **What:** Exception handling blocks (`try...except`) must only contain the |
| > specific error-handling logic and never encapsulate complex business logic, |
| > terminal loops, or major execution routing. |
| > |
| > **Applies To:** Command dispatcher routing and system control flows. |
| > |
| > **Why:** Burying significant CLI routing tasks—like spelling suggestions, |
| > autocorrection countdowns, and subprocess recursion—inside a `KeyError` |
| > handler obscured program logic and suppressed nested tracebacks. Failing to |
| > adhere to this typically results in **Opaque Routing State**. |
| |
| **Trap 1: Placing the entire typo-correction logic, user prompts, and command |
| reloading inside `except KeyError:`.** |
| |
| **Don't:** |
| |
| ```python |
| try: |
| cmd = self.commands[name]() |
| except KeyError: |
| # 50 lines of complex autocorrection logic |
| return self._RunLong(...) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if name not in self.commands: |
| name = self._autocorrect_command_name(name) |
| if not name: |
| return 1 |
| # Execute minimal command logic outside exception |
| cmd = self.commands[name]() |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-11: Unified Logging Protocol via Standard Logger |
| |
| > **Rule:** Dispatch all console messages using the standard centralized logging |
| > framework. Never rely on raw `print` statements mapped directly to terminal |
| > error streams. |
| > |
| > **What:** All console outputs must be dispatched via the system's centralized |
| > `RepoLogger` mechanism, completely replacing raw terminal standard-error |
| > prints. |
| > |
| > **Applies To:** CLI presentation logic and application-wide console |
| > communications. |
| > |
| > **Why:** The use of raw `print(file=sys.stderr)` bypassed the system's unified |
| > logging configuration, resulting in unformatted outputs and broken terminal |
| > interactivity. Failing to adhere to this typically results in **Unmanaged |
| > Output Spillage**. |
| |
| **Trap 1: Outputting warnings directly to the `sys.stderr` stream using the |
| built-in `print` function.** |
| |
| **Don't:** |
| |
| ```python |
| print(f"WARNING: You called a command named '{name}'", file=sys.stderr) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| logger.warning("You called a repo command named '%s'", name) |
| ``` |
| |
| **Exceptions:** Prompts that require strict, synchronously unformatted |
| interaction via `input()`. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-12: Strict Translation of Configuration Booleans |
| |
| > **Rule:** Always cast string-based Git configurations to lowercase and map |
| > them strictly against Git's recognized array of boolean equivalents. |
| > |
| > **What:** Parsers querying Git configuration properties must implement strict |
| > hardcoding to safely capture Git's expansive list of non-standard boolean |
| > string representations (e.g., 'yes', 'on', 'true', 'off'). |
| > |
| > **Applies To:** Configuration parsers mapping user `git config` state to |
| > internal state variables. |
| > |
| > **Why:** Using standard integer casts on retrieved git configuration strings |
| > caused runtime failures when users specified string-based boolean values |
| > sanctioned by the official Git system. Failing to adhere to this typically |
| > results in **Configuration Casting Panic**. |
| |
| **Trap 1: Wrapping string-typed configuration variables inside `int()` and |
| catching value exceptions to deduce defaults.** |
| |
| **Don't:** |
| |
| ```python |
| try: |
| autocorrect = int(autocorrect) |
| except ValueError: |
| autocorrect = 0 |
| ``` |
| |
| **Do:** |
| |
| ```python |
| autocorrect = str(autocorrect).lower() |
| if autocorrect in ("0", "false", "off", "no", "show"): |
| autocorrect = 0 |
| elif autocorrect in ("1", "true", "on", "yes", "immediate"): |
| autocorrect = -1 |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-13: Interruptibility of Interactive Runtime Delays |
| |
| > **Rule:** Must wrap artificially injected execution delays in handlers that |
| > correctly detect and gracefully back out of operations upon a keyboard |
| > interrupt signal. |
| > |
| > **What:** Any thread-blocking delay generated by the system (e.g., |
| > autocorrection warning timers) must actively handle thread interrupt signals |
| > so users can gracefully cancel impending state mutations. |
| > |
| > **Applies To:** Thread blocks, specifically interactive or automated execution |
| > delays. |
| > |
| > **Why:** Forced sequential time sleeps preceding an automated command |
| > execution trapped users. If a user detected an erroneous correction during the |
| > sleep interval, initiating a keyboard interrupt would fail to abort execution, |
| > resulting in destructive command routing. Failing to adhere to this typically |
| > results in **Irrevocable Execution**. |
| |
| **Trap 1: Placing raw time suspension code before a return statement without |
| wrapping it in a trap for cancellation overrides.** |
| |
| **Don't:** |
| |
| ```python |
| time.sleep(delay) |
| return assumed_command |
| ``` |
| |
| **Do:** |
| |
| ```python |
| try: |
| time.sleep(delay) |
| except KeyboardInterrupt: |
| return None |
| return assumed_command |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-14: Strict Pre-Access Initialization of CLI Options |
| |
| > **Rule:** Never retrieve attributes from a parsed options namespace until the |
| > shared subcommand option validation layer has fully completed processing. |
| > |
| > **What:** Command-line option attributes (e.g., `copts.verbose`) must strictly |
| > be accessed only after the options object has fully passed through the common |
| > subcommand validation layer. |
| > |
| > **Applies To:** CLI entry points (`main.py`) and option routing blocks. |
| > |
| > **Why:** Accessing attributes on the configuration options object prior to |
| > invoking `CommonValidateOptions` (which populates global defaults) resulted in |
| > `AttributeError` crashes, completely breaking primary application functions |
| > like `repo sync`. Failing to adhere to this typically results in |
| > **AttributeError / Process Crash**. |
| |
| **Trap 1: Consuming CLI option flags before the shared initialization pipeline |
| populates default properties.** |
| |
| **Don't:** |
| |
| ```python |
| git_trace2_event_log.verbose = copts.verbose |
| cmd.CommonValidateOptions(copts, cargs) |
| cmd.ValidateOptions(copts, cargs) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| cmd.CommonValidateOptions(copts, cargs) |
| cmd.ValidateOptions(copts, cargs) |
| git_trace2_event_log.verbose = copts.verbose |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-15: Omission of Redundant Optparse Destinations |
| |
| > **Rule:** Avoid manually injecting `dest=` bindings in argument declarations |
| > if the implicit destination perfectly mirrors the target variable name. |
| > |
| > **What:** Do not explicitly pass `dest=` arguments in CLI option definitions |
| > if the value identically matches `optparse`'s implicit fallback behavior |
| > (which strips leading dashes and replaces inner dashes with underscores). |
| > |
| > **Applies To:** Command-line argument definitions using the `optparse` library |
| > within `subcmds/`. |
| > |
| > **Why:** Developers repeatedly specified `dest=` overrides that exactly |
| > mirrored the auto-generated variable name, adding unnecessary boilerplate and |
| > triggering automated regression test failures. Failing to adhere to this |
| > typically results in **Code Bloat / Test Failure**. |
| |
| **Trap 1: Redundantly configuring destination variables in CLI arguments.** |
| |
| **Don't:** |
| |
| ```python |
| p.add_option( |
| "--no-interleaved", |
| dest="no_interleaved", |
| action="store_false", |
| ) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| p.add_option( |
| "--no-interleaved", |
| action="store_false", |
| ) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-16: Subcommand Scoping in Commit Messages |
| |
| > **Rule:** Always prefix your commit messages with the explicit CLI subcommand |
| > or modular subsystem the patch directly targets. |
| > |
| > **What:** Git commit message subject lines must explicitly scope changes to |
| > the CLI subcommand or module they affect (e.g., `sync: ` or `project: `). |
| > |
| > **Applies To:** Commit message authoring. |
| > |
| > **Why:** Vague commit messages such as 'Default to interleaved mode' |
| > obfuscated history. Scoping was mandated to ensure maintainers could quickly |
| > trace changes within the mono-repo. Failing to adhere to this typically |
| > results in **Unclear Git History**. |
| |
| **Trap 1: Writing global-sounding commit messages for isolated subcommand |
| patches.** |
| |
| **Don't:** |
| |
| * Default to interleaved mode |
| |
| **Do:** |
| |
| * sync: Default to interleaved mode |
| |
| **Exceptions:** Changes that structurally alter the core framework or affect |
| multiple commands concurrently. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-17: Module-Scoped Exception Naming |
| |
| > **Rule:** Declare isolated module exceptions natively as `Error` instead of |
| > prefixing them with the resident module's name. |
| > |
| > **What:** Custom exceptions specific to a module (especially subcommands) |
| > should be named `Error` rather than prefixing them with the module name. |
| > |
| > **Applies To:** Module-level exception definitions, specifically within |
| > subcommand implementations. |
| > |
| > **Why:** Developers inconsistently named module exceptions (e.g., `WipeError` |
| > in `wipe.py`), violating the style guide and polluting the namespace upon |
| > import. Failing to adhere to this typically results in **Style Guide Violation |
| > / API Inconsistency**. |
| |
| **Trap 1: Redundantly prefixing the exception name with the module name inside |
| the module definition.** |
| |
| **Don't:** |
| |
| ```python |
| # inside wipe.py |
| class WipeError(RepoExitError): |
| pass |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # inside wipe.py |
| class Error(RepoExitError): |
| pass |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-18: Explicit Terminology in Destructive CLI Commands |
| |
| > **Rule:** Must rigorously define nuanced Git terminology natively within the |
| > terminal help output if a command has destructive capabilities. |
| > |
| > **What:** Destructive commands must clearly define ambiguous terms (like |
| > 'worktree') in their help documentation and provide concrete usage examples. |
| > |
| > **Applies To:** CLI help text, specifically `helpDescription` properties in |
| > `Command` subclasses executing file deletion. |
| > |
| > **Why:** Users misunderstood destructive operations because terminology |
| > overlapped with internal Git jargon, increasing the risk of accidental |
| > repository deletions. Failing to adhere to this typically results in |
| > **Accidental Data Loss**. |
| |
| **Trap 1: Using terms like 'worktree' in help text without explicit definition |
| or examples.** |
| |
| **Don't:** |
| |
| * helpDescription = "The repo wipe command removes the specified projects from |
| the worktree." |
| |
| **Do:** |
| |
| * helpDescription = "The repo wipe command removes the specified projects from |
| the worktree (the checked out source code)... \n\nExamples:\n %prog |
| <project>..." |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-19: Fail-Fast Argument Validation in Subcommands |
| |
| > **Rule:** Must perform strict threshold checks for required argument counts at |
| > the validation layer before the command is permitted to execute. |
| > |
| > **What:** Validation checks for mandatory command-line arguments must occur in |
| > the `ValidateOptions` hook rather than within the core `Execute` block. |
| > |
| > **Applies To:** All `Command` subclasses. |
| > |
| > **Why:** Performing validation in the execution body delayed error feedback, |
| > triggering unnecessary setup logic and violating the command lifecycle |
| > architecture. Failing to adhere to this typically results in **Delayed |
| > Execution Errors / Architectural Inconsistency**. |
| |
| **Trap 1: Checking for missing positional arguments directly inside |
| `Execute()`.** |
| |
| **Don't:** |
| |
| ```python |
| def Execute(self, opt, args): |
| if not args: |
| raise UsageError("no projects specified") |
| # ... execution logic |
| ``` |
| |
| **Do:** |
| |
| ```python |
| def ValidateOptions(self, opt, args): |
| if not args: |
| self.OptionParser.error("no projects specified") |
| |
| def Execute(self, opt, args): |
| # ... execution logic safely assumes valid args |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-20: Respect Global Verbosity Flags |
| |
| > **Rule:** Always wrap non-critical progress prints or logging updates behind |
| > global verbosity checks. |
| > |
| > **What:** Commands must remain silent by default. Output must be guarded by |
| > checking the global `opt.verbose` flag, preventing log noise during standard |
| > execution. |
| > |
| > **Applies To:** All `Command` subclasses executing file operations or progress |
| > updates. |
| > |
| > **Why:** Commands were unconditionally printing internal states (e.g., |
| > directory deletions), cluttering terminal output for users expecting standard |
| > silent behavior. Failing to adhere to this typically results in **Terminal |
| > Spam / UX Degradation**. |
| |
| **Trap 1: Unconditionally printing progress messages to stderr or stdout.** |
| |
| **Don't:** |
| |
| ```python |
| print(f"Deleting objects directory: {objdir}", file=sys.stderr) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if opt.verbose: |
| print(f"Deleting objects directory: {objdir}", file=sys.stderr) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-21: Thread-Safe Terminal Querying during IO Capture |
| |
| > **Rule:** Must query terminal geometry using a safely cached baseline file |
| > descriptor to prevent crash failures when the standard error stream is |
| > redirected to memory buffers. |
| > |
| > **What:** Background threads that query terminal properties (like width) must |
| > reference a persistently cached file descriptor of the original `sys.stderr`, |
| > as the global stream can be temporarily redirected to in-memory objects |
| > lacking file descriptors. |
| > |
| > **Applies To:** progress.py, UI monitoring loops, and subprocesses executed |
| > during `stderr`/`stdout` redirection. |
| > |
| > **Why:** When running parallel processes that captured output into an |
| > `io.StringIO()` buffer, a background progress thread attempting to dynamically |
| > measure the terminal width via `sys.stderr.fileno()` triggered an |
| > `io.UnsupportedOperation: fileno` exception, crashing the entire UI loop. |
| > Failing to adhere to this typically results in **Thread Crash / |
| > io.UnsupportedOperation**. |
| |
| **Trap 1: Directly querying `sys.stderr.fileno()` inside long-running concurrent |
| loops.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Fails if stderr is mocked to memory |
| col = os.get_terminal_size(sys.stderr.fileno()).columns |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Querying a module-level cached reference mapped at boot |
| col = os.get_terminal_size(_STDERR.fileno()).columns |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-22: Global State via Persistent Configuration over Environment Variables |
| |
| > **Rule:** Never propagate novel OS-level environment variables to modify |
| > executable behavior, strictly enforcing the existing `.repoconfig/config` |
| > framework instead. |
| > |
| > **What:** Global user or CI settings (e.g., Git-LFS defaults) must be managed |
| > using the explicit `.repoconfig/config` framework. Do not introduce novel |
| > OS-level environment variables to modify executable behavior. |
| > |
| > **Applies To:** Configuration parsers, command-line argument mapping, and |
| > option handlers across the CLI. |
| > |
| > **Why:** A contributor attempted to add a `REPO_GIT_LFS` environment variable |
| > to ease CI automation. Maintainers rejected the patch, citing that the |
| > proliferation of disjointed environment variables builds technical debt |
| > compared to maintaining a single, consistent file-based configuration layer. |
| > Failing to adhere to this typically results in **Configuration Fragmentation / |
| > Tech Debt**. |
| |
| **Trap 1: Adding environment variable mappings into command-line parsers to |
| support 'headless' configurations.** |
| |
| **Don't:** |
| |
| * Read configuration defaults from `os.environ.get("REPO_GIT_LFS")` alongside |
| CLI flags. |
| |
| **Do:** |
| |
| * Instruct users to provision a `~/.repoconfig/config` file in automated |
| setups instead of changing process environment logic. |
| |
| **Exceptions:** Legacy environment variables (e.g., `REPO_URL`) remain supported |
| for backward compatibility but are explicitly designated as technical debt. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-23: CLI Command Parity with Git Semantics |
| |
| > **Rule:** Tightly bind local CLI utility semantics and argument definitions to |
| > match analogous Git operations explicitly. |
| > |
| > **What:** Internal CLI command responsibilities and user-facing terminology |
| > must align with established Git semantics and project-wide conventions. |
| > |
| > **Applies To:** Command-line interface modules (e.g., `subcmds/status.py`, |
| > `subcmds/info.py`). |
| > |
| > **Why:** Global metadata (like superproject commit IDs) was mistakenly added |
| > to `repo status`, which cluttered the local working-tree status. Additionally, |
| > the term 'hash' was used instead of the standard 'revision'. Failing to adhere |
| > to this typically results in **UX Inconsistency / Clutter**. |
| |
| **Trap 1: Adding project-wide or environmental metadata queries to commands |
| designed for local working-tree analysis.** |
| |
| **Don't:** |
| |
| * Injecting global repository state (e.g., `--superproject-hash`) into the |
| `status` command, which breaks parity with `git status`. |
| |
| **Do:** |
| |
| * Relocating environmental and global metadata display to the `info` command, |
| integrating it cleanly into the default summary header. |
| |
| **Trap 2: Using non-standard terminology for Git object identifiers in |
| user-facing CLI flags.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Using 'hash' which is inconsistent with existing repo terminology |
| p.add_option("--superproject-hash", help="print the superproject hash") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Using 'revision' |
| p.add_option("--superproject-rev", help="print the superproject revision") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-24: Context-Aware Sync Operation Termination |
| |
| > **Rule:** Must catch and defer local sync iteration failures gracefully rather |
| > than forcing immediate unrecoverable exits across all active streams. |
| > |
| > **What:** Non-critical update failures during local sync iterations must set |
| > an error flag but must only force a hard exit if the user explicitly provided |
| > the `--fail-fast` CLI argument. |
| > |
| > **Applies To:** All local project iteration workflows within |
| > `subcmds/sync.py`. |
| > |
| > **Why:** A single project failing an isolated sub-task (like a symlink update) |
| > would immediately trigger `sys.exit(1)`, aborting the entire pipeline for all |
| > other projects and ignoring standard error aggregation strategies. Failing to |
| > adhere to this typically results in **Premature Pipeline Termination**. |
| |
| **Trap 1: Triggering an immediate system exit on an update failure.** |
| |
| **Don't:** |
| |
| ```python |
| if self.UpdateCopyLinkfileList(): |
| print('\nerror: Local update copyfile or linkfile failed.', file=sys.stderr) |
| sys.exit(1) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| if not self.UpdateCopyLinkfileList(): |
| err_event.set() |
| err_update_linkfiles = True |
| if opt.fail_fast: |
| print('\nerror: Local update copyfile or linkfile failed.', file=sys.stderr) |
| sys.exit(1) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-25: Explicit Exit Codes on CLI Cancellation |
| |
| > **Rule:** Always force the process to yield a non-zero exit code if an |
| > interactive confirmation prompt is declined or cancelled. |
| > |
| > **What:** Interactive CLI workflows that are cancelled or declined by the user |
| > must immediately return a non-zero exit code to signal termination to parent |
| > processes. |
| > |
| > **Applies To:** All interactive subcommands (`gc`, `sync` with prompts, etc.). |
| > |
| > **Why:** If a tool cleanly exited (returning None / 0) after a user denied a |
| > destructive action prompt, shell automation wrappers and CI systems falsely |
| > assumed the destructive cleanup successfully completed. Failing to adhere to |
| > this typically results in **Silent Automation Failure**. |
| |
| **Trap 1: Using an empty `return` to back out of a declined CLI confirmation |
| prompt.** |
| |
| **Don't:** |
| |
| ```python |
| ask = input("Proceed? [y/N] ") |
| if ask.lower() != "y": |
| return |
| ``` |
| |
| **Do:** |
| |
| ```python |
| ask = input("Proceed? [y/N] ") |
| if ask.lower() != "y": |
| return 1 |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-26: Non-Interactive Safeties for Destructive Commands |
| |
| > **Rule:** Must implement explicit automation bypass flags for any command |
| > capable of irreversibly destroying system state. |
| > |
| > **What:** Any command capable of destroying state (e.g., garbage collection, |
| > wiping local commits) must provide both a `--dry-run` and a `--yes` (or |
| > `--force`) flag to support safe CI automation. |
| > |
| > **Applies To:** Command-line definitions for potentially destructive |
| > operations. |
| > |
| > **Why:** Tools that rely exclusively on `input()` prompts block automated |
| > scripts indefinitely. By explicitly enforcing non-interactive modes, scripts |
| > can safely trigger automated cleanups or log dry-runs without freezing. |
| > Failing to adhere to this typically results in **Automation Hang / CI Pipeline |
| > Freeze**. |
| |
| **Trap 1: Hardcoding interactive prompts without checking configuration flags |
| for automation modes.** |
| |
| **Don't:** |
| |
| * Prompting the user with `input('Proceed?')` unconditionally before executing |
| a destructive delete. |
| |
| **Do:** |
| |
| * Checking `if opt.dry_run:` to skip execution, or `if opt.yes:` to skip the |
| confirmation prompt. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-27: Strict Interdependent Argument Validation |
| |
| > **Rule:** Always enforce strict prerequisites checks when parsing command-line |
| > flags intended to modify or inherit context from other flags. |
| > |
| > **What:** Command-line options that exist solely to modify or qualify another |
| > specific command-line option must be explicitly validated to raise an error if |
| > their prerequisite option is absent. |
| > |
| > **Applies To:** CLI argument parsing routines (`subcmds/init.py`, |
| > `ValidateOptions`). |
| > |
| > **Why:** A new `--manifest-upstream-branch` argument was added to qualify a |
| > provided commit hash. However, without strict validation, users could supply |
| > it independently, leading to ambiguous or unhandled configuration states. |
| > Failing to adhere to this typically results in **CLI Ambiguity / Unhandled |
| > Option State**. |
| |
| **Trap 1: Accepting dependent modifier options unconditionally without checking |
| prerequisites.** |
| |
| **Don't:** |
| |
| * Accepting `--manifest-upstream-branch` in the CLI parser and directly |
| assigning it to the internal state regardless of the presence of |
| `--manifest-branch`. |
| |
| **Do:** |
| |
| * Explicitly checking the parsed options and raising an `OptionParser.error` |
| if the modifier is present but the target `--manifest-branch` is null. |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-28: Actionable CLI Error Messages for Synchronization Failures |
| |
| > **Rule:** Embed direct, explicit commands and flag references within terminal |
| > error payloads to rescue users from failing execution states. |
| > |
| > **What:** When a terminal failure occurs during CLI operations, the error |
| > payload must provide specific, actionable flags or manual steps for the user |
| > to resolve the failing state. |
| > |
| > **Applies To:** Sync command error reporting (`subcmds/sync.py`, |
| > `project.py`). |
| > |
| > **Why:** When a local branch had published commits but was behind upstream, |
| > the sync failed with a generic 'punt' message, leaving users unaware that |
| > passing `--rebase` could force the sync. Failing to adhere to this typically |
| > results in **User Friction / Dead End**. |
| |
| **Trap 1: Failing a sync operation with a descriptive but non-actionable error |
| message.** |
| |
| **Don't:** |
| |
| ```python |
| fail(LocalSyncFail( |
| "branch %s is published (but not merged) and is now %d commits behind" |
| % (branch.name, len(upstream_gain)), |
| project=self.name, |
| )) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| fail(LocalSyncFail( |
| "branch %s is published (but not merged) and is now %d commits behind. " |
| "Fix this manually or rerun with the --rebase option to force a rebase." |
| % (branch.name, len(upstream_gain)), |
| project=self.name, |
| )) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| #### T6-29: Launcher Script Version Upgrades for Important Changes |
| |
| > **Rule:** Increment the `VERSION` tuple within the standalone `repo` launcher script when making important or significant functional changes. |
| > |
| > **What:** If you modify the standalone `repo` launcher file with an important or large change (e.g., updates to Python version checks, significant logic fixes, or major argument parsing updates), you must increment the `VERSION = (X, Y)` tuple. |
| > |
| > **Applies To:** Standalone `repo` launcher script, release engineering workflows. |
| > |
| > **Why:** The repo source code entry point (`main.py`) compares the runner wrapper version to the copy of the launcher in the source tree to prompt users to upgrade. If an important launcher modification is submitted without a corresponding version bump, users executing older wrappers won't receive the required update prompts. Failing to adhere to this typically results in **Launcher Version Skew / Missing Update Warnings**. |
| > |
| > **Exceptions:** Minor or cosmetic changes (like fixing typos in comments or minor docstrings) do not require a version bump. |
| |
| **Trap 1: Submitting a major or important patch to the `repo` launcher script without updating its `VERSION` definition.** |
| |
| **Don't:** |
| |
| ```python |
| # Making significant changes to Python version handling logic |
| # but leaving VERSION unchanged: |
| VERSION = (2, 65) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # Incrementing the minor version tuple to signal the update: |
| VERSION = (2, 66) |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T1 | Concurrent Synchronization & IPC - *CLI rendering relies |
| on sorted, deterministic results emitted from asynchronous multiprocessing |
| pools.* |
| * **Downstream:** T3 | Subprocess Git Integration & Error Translation - |
| *Command-line terminology and state configurations must tightly align with |
| wrapped Git subprocess semantics.* |
| |
| ## Chapter: Repo Hooks Framework |
| |
| **Context:** The Repo Hooks Framework governs the execution, parameter |
| validation, and lifecycle management of user-defined scripts within the |
| repository ecosystem. It ensures seamless integration of extensions like |
| `post-sync` or `pre-upload` while strictly isolating their execution failures |
| from core operational workflows. |
| |
| ### Summary |
| |
| | Rule ID | Principle / | Priority | Primary Symptom / Trap | |
| : : Constraint : : : |
| | :-------- | :----------------- | :------- | :------------------------------- | |
| | **T7-01** | Explicit | Critical | Passing a new variable to | |
| : : Registration of : : `hook.Run()` without : |
| : : Hook Parameters in : : whitelisting it in the : |
| : : API Contract : : `_API_ARGS` map. : |
| | **T7-02** | Floating-Point | High | Casting float calculations | |
| : : Precision for Hook : : directly to integers before : |
| : : Telemetry Metrics : : passing to downstream systems. : |
| | **T7-03** | Non-Blocking | High | Using raw `subprocess` calls to | |
| : : Post-Sync Hook : : run custom scripts and tying the : |
| : : Integration : : command's exit code to the main : |
| : : : : tool's exit code. : |
| | **T7-04** | Dynamic CLI Option | Medium | Bypassing standard option | |
| : : Injection for Repo : : parsers by passing a mock object : |
| : : Hooks : : with hardcoded values into hook : |
| : : : : factory methods. : |
| | **T7-05** | Boolean Evaluation | High | Wrapping a framework execution | |
| : : for Repo Hook : : method in a generic exception : |
| : : Execution Status : : handler. : |
| | **T7-06** | Universal Trigger | High | Documenting hook triggers based | |
| : : Specification for : : on an idealized use-case (a full : |
| : : Execution : : checkout) rather than the : |
| : : Lifecycle : : literal programmatic trigger. : |
| | **T7-07** | Forward-Compatible | High | Defining a hook entrypoint with | |
| : : kwargs for Repo : : a strict argument list that : |
| : : Hooks Signatures : : cannot gracefully handle new : |
| : : : : inputs. : |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Rules |
| |
| #### T7-01: Explicit Registration of Hook Parameters in API Contract |
| |
| > **Rule:** Always register new metadata or arguments intended for user-defined |
| > lifecycle scripts within the internal `_API_ARGS` validation dictionary. |
| > |
| > **What:** Any new metadata or argument passed into user-defined lifecycle |
| > scripts must be explicitly defined in the hook's internal validation |
| > dictionary (`_API_ARGS`). |
| > |
| > **Applies To:** Repo Hooks Framework (`hooks.py` and Hook Runners). |
| > |
| > **Why:** New arguments sent to the post-sync execution context triggered |
| > internal exceptions because the core application strictly validates `**kwargs` |
| > against a registered list of supported arguments. Failing to adhere to this |
| > typically results in **Runtime Hook Failure**. |
| |
| **Trap 1: Passing a new variable to `hook.Run()` without whitelisting it in the |
| `_API_ARGS` map.** |
| |
| **Don't:** |
| |
| ```python |
| # In subcmds/sync.py |
| hook.Run(repo_topdir=self.client.topdir, sync_duration_seconds=dur) |
| |
| # In hooks.py (Missing registration) |
| _API_ARGS = { |
| 'post-sync': {'repo_topdir'} |
| } |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # In hooks.py |
| _API_ARGS = { |
| 'post-sync': {'repo_topdir', 'sync_duration_seconds'} |
| } |
| ``` |
| |
| #### T7-02: Floating-Point Precision for Hook Telemetry Metrics |
| |
| > **Rule:** Must pass system telemetry and operational duration metrics to |
| > downstream extension hooks as precise floating-point values. |
| > |
| > **What:** System telemetry, specifically operational duration metrics, must be |
| > passed to downstream extension hooks as granular floating-point values. The |
| > executing system must not truncate or cast telemetry to integers. |
| > |
| > **Applies To:** Performance telemetry generation and hook contexts. |
| > |
| > **Why:** Internal performance timers originally cast their delta outputs to |
| > integers before emitting the context to the hook system. This caused a loss of |
| > sub-second precision, limiting telemetry usefulness. Failing to adhere to this |
| > typically results in **Telemetry Data Loss**. |
| |
| **Trap 1: Casting float calculations directly to integers before passing to |
| downstream systems.** |
| |
| **Don't:** |
| |
| ```python |
| sync_duration_seconds = time.time() - start_time |
| self._RunPostSyncHook(opt, sync_duration_seconds=int(sync_duration_seconds)) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| sync_duration_seconds = time.time() - start_time |
| self._RunPostSyncHook(opt, sync_duration_seconds=sync_duration_seconds) |
| ``` |
| |
| #### T7-03: Non-Blocking Post-Sync Hook Integration |
| |
| > **Rule:** Never allow post-operation custom hooks to block or alter the |
| > success state of the core operation. |
| > |
| > **What:** Custom hooks executed strictly after an operation finishes (like |
| > `post-sync`) must run via the encapsulated `RepoHook` framework and be |
| > entirely non-blocking, so their failure does not negatively affect the core |
| > operation's recorded success state. |
| > |
| > **Applies To:** subcmds/sync.py and `RepoHook` integrations executing after |
| > repository data modifications. |
| > |
| > **Why:** Teams needed a reliable way to provision local Git `commit-msg` hooks |
| > even if developers bypassed `repo upload` with manual `git push`. A |
| > `post-sync` hook was added, but the rigid requirement was that if the user |
| > script failed, the actual project synchronization must still report as a |
| > success to avoid corrupting UX. Failing to adhere to this typically results in |
| > **Blocked Sync Operations**. |
| |
| **Trap 1: Using raw `subprocess` calls to run custom scripts and tying the |
| command's exit code to the main tool's exit code.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Post-action failure corrupts the sync outcome |
| if subprocess.run(["run_hooks.sh"]).returncode != 0: |
| raise SyncError("Hook failed") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Managed execution safely wrapping and suppressing exceptions |
| hook = RepoHook("post-sync") |
| hook.Run() # Designed strictly to warn on failure |
| ``` |
| |
| #### T7-04: Dynamic CLI Option Injection for Repo Hooks |
| |
| > **Rule:** Always utilize the RepoHook option group injection framework for |
| > command-line integration rather than synthesizing dummy configuration objects. |
| > |
| > **What:** Command-line integration for new hooks must utilize the RepoHook |
| > option group injection framework rather than synthesizing dummy configuration |
| > objects. |
| > |
| > **Applies To:** Repo Hooks Framework; specifically subcommand definitions |
| > involving hook invocations (e.g., `subcmds/sync.py`). |
| > |
| > **Why:** When integrating the post-sync hook, a developer initially created a |
| > minimal 'DummyOpt' object to satisfy parameter checks because the sync command |
| > lacked native hook flags. This diverged from standard hook architecture and |
| > could cause attribute errors. Failing to adhere to this typically results in |
| > **AttributeError / Option Divergence**. |
| |
| **Trap 1: Bypassing standard option parsers by passing a mock object with |
| hardcoded values into hook factory methods.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Using a dummy object to satisfy missing CLI arguments |
| class DummyOpt: |
| bypass_hooks = False |
| allow_all_hooks = True |
| ignore_hooks = True |
| |
| hook = RepoHook.FromSubcmd(opt=DummyOpt(), ...) |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Injecting the standard hook option group into the parser |
| RepoHook.AddOptionGroup(parser, 'post-sync') |
| |
| # Later during execution, pass the actual resolved options: |
| hook = RepoHook.FromSubcmd(opt=actual_opt, ...) |
| ``` |
| |
| #### T7-05: Boolean Evaluation for Repo Hook Execution Status |
| |
| > **Rule:** Always evaluate hook execution success using the boolean return |
| > value of `Run()`, never by wrapping the call in external exception handlers. |
| > |
| > **What:** Execution wrappers for Repo Hooks must rely on the boolean return |
| > value of `Run()` to evaluate success, rather than wrapping the call in broad |
| > `try/except Exception` blocks. |
| > |
| > **Applies To:** Repo Hooks Framework; specifically the invocation layer for |
| > any hook type (`post-sync`, `pre-upload`). |
| > |
| > **Why:** A developer wrapped the hook execution in a broad Exception catch |
| > block. The hook framework inherently traps and standardizes exceptions |
| > internally; wrapping it externally duplicates logic, masks context, and leads |
| > to silent failure processing. Failing to adhere to this typically results in |
| > **Swallowed Exceptions / Logic Duplication**. |
| |
| **Trap 1: Wrapping a framework execution method in a generic exception |
| handler.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Redundant outer try/except block swallowing state |
| try: |
| hook.Run() |
| except Exception as e: |
| print(f"Warning: hook failed: {e}") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Relying on the established boolean return contract |
| success = hook.Run() |
| if not success: |
| print("Warning: hook reported failure.") |
| ``` |
| |
| #### T7-06: Universal Trigger Specification for Execution Lifecycle |
| |
| > **Rule:** Must document the exact, literal programmatic triggers for hooks |
| > rather than relying on idealized use-cases like full checkouts. |
| > |
| > **What:** Hook documentation must strictly delineate the exact success states |
| > that trigger execution, explicitly noting conditions where operational state |
| > (like a local checkout) might be bypassed or skipped. |
| > |
| > **Applies To:** Repo Hooks API contracts and documentation |
| > (`docs/repo-hooks.md`). |
| > |
| > **Why:** Users of the new post-sync hook might assume it only runs after a |
| > complete workspace checkout. However, it effectively fires on any |
| > zero-exit-code sync operation, including network-only fetches where the local |
| > filesystem state remains completely untouched. Failing to adhere to this |
| > typically results in **State Assumption Bugs**. |
| |
| **Trap 1: Documenting hook triggers based on an idealized use-case (a full |
| checkout) rather than the literal programmatic trigger.** |
| |
| **Don't:** |
| |
| * Documenting that the hook is to install post-processing tasks specifically |
| for successful full checkouts. |
| |
| **Do:** |
| |
| * Documenting that the hook runs when `repo sync` completes without errors, |
| explicitly warning that this does not guarantee all projects were synced or |
| checked out (e.g., `repo sync -n` performs network fetches only). |
| |
| #### T7-07: Forward-Compatible kwargs for Repo Hooks Signatures |
| |
| > **Rule:** Always design hook script entrypoints to accept variable keyword |
| > arguments (`**kwargs`) to ensure forward compatibility with future framework |
| > expansions. |
| > |
| > **What:** All repository hook script entrypoints (`main` functions) must |
| > accept variable keyword arguments (`**kwargs`) to maintain forward |
| > compatibility with future API augmentations. |
| > |
| > **Applies To:** Repo Hooks scripts and internal examples (e.g., |
| > `post-sync.py`, `pre-upload.py`). |
| > |
| > **Why:** Historically, if the `repo` tool evolved to pass new environmental |
| > context (like `repo_topdir`), user-defined scripts with strict function |
| > signatures would crash with a TypeError. Failing to adhere to this typically |
| > results in **TypeError / API Breakage**. |
| |
| **Trap 1: Defining a hook entrypoint with a strict argument list that cannot |
| gracefully handle new inputs.** |
| |
| **Don't:** |
| |
| ```python |
| # BAD: Strict signature that will crash on API update |
| def main(repo_topdir=None): |
| print("Running tasks...") |
| ``` |
| |
| **Do:** |
| |
| ```python |
| # GOOD: Accepts forward-compatible **kwargs |
| def main(repo_topdir=None, **kwargs): |
| """ |
| Args: |
| kwargs: Leave this here for forward-compatibility. |
| """ |
| print("Running tasks...") |
| ``` |
| |
| -------------------------------------------------------------------------------- |
| |
| ### Cross-Domain Dependencies |
| |
| * **Upstream:** T6 | CLI Argument Parsing & UX Consistency - *The dynamic hook |
| injection framework fundamentally relies on standardized option parsers to |
| map CLI flags cleanly into hook runner contexts.* |