Skip to content

API Reference

This page documents the Python API for wcheck. You can use these functions programmatically in your own scripts.

Core Functions

check_workspace_status(workspace_directory, full=False, verbose=False, show_time=False, fetch=False, gui=False, tui=False)

Check and display the status of all repositories in a workspace.

Scans the workspace for git repositories and displays their current branch, uncommitted changes, and remote sync status.

Parameters:

Name Type Description Default
workspace_directory Path

Path to the workspace directory containing repositories.

required
full bool

If True, show all repositories. If False, only show those with changes.

False
verbose bool

If True, print additional information about each repository.

False
show_time bool

If True, include time since last commit in the output.

False
fetch bool

If True, fetch from remotes before checking status.

False
gui bool

If True, launch the GUI interface instead of console output.

False
tui bool

If True, launch the TUI interface instead of console output.

False
Source code in src/wcheck/wcheck.py
def check_workspace_status(
    workspace_directory: Path,
    full: bool = False,
    verbose: bool = False,
    show_time: bool = False,
    fetch: bool = False,
    gui: bool = False,
    tui: bool = False,
) -> None:
    """Check and display the status of all repositories in a workspace.

    Scans the workspace for git repositories and displays their current
    branch, uncommitted changes, and remote sync status.

    Args:
        workspace_directory: Path to the workspace directory containing repositories.
        full: If True, show all repositories. If False, only show those with changes.
        verbose: If True, print additional information about each repository.
        show_time: If True, include time since last commit in the output.
        fetch: If True, fetch from remotes before checking status.
        gui: If True, launch the GUI interface instead of console output.
        tui: If True, launch the TUI interface instead of console output.
    """
    # Load workspace
    source_repos = get_workspace_repos(workspace_directory)

    if gui:
        _show_gui(source_repos)
        return

    if tui:
        _show_tui(source_repos)
        return

    if fetch and source_repos:
        # Fetch in parallel
        def fetch_repo(repo: Repo) -> None:
            for remote in repo.remotes:
                remote.fetch()

        with ThreadPoolExecutor(max_workers=min(len(source_repos), 8)) as executor:
            executor.map(fetch_repo, source_repos.values())

    # Get repo info in parallel
    repo_info = get_repo_info_parallel(source_repos, include_status=True, include_time=show_time)

    # Get current branch for each repo
    workspace_current_branch_version = {}
    workspace_current_branch_version["Current Workspace"] = {}
    for repo_name in sorted(repo_info.keys()):
        info = repo_info[repo_name]
        status_str = info["status"]
        if not full and status_str == "":
            continue
        repo_display_name = repo_name + status_str
        if show_time:
            repo_display_name += " (" + info["elapsed_time"] + ")"
        workspace_current_branch_version["Current Workspace"][repo_display_name] = info["head_ref"]

    show_repos_config_versions(workspace_current_branch_version, full=True)

compare_workspace_to_config(workspace_directory, config_filename, full=False, verbose=False, show_time=False, gui=False, tui=False)

Compare workspace repository versions with a configuration file.

Displays a table comparing the current branch/version of each repository in the workspace with the version specified in the configuration file.

Parameters:

Name Type Description Default
workspace_directory Path

Path to the workspace directory containing repositories.

required
config_filename str

Path to the YAML configuration file specifying expected versions.

required
full bool

If True, show all repositories. If False, only show mismatches.

False
verbose bool

If True, print additional information during processing.

False
show_time bool

If True, include time since last commit in the output.

False
gui bool

If True, launch the GUI interface instead of console output.

False
tui bool

If True, launch the TUI interface instead of console output.

False
Source code in src/wcheck/wcheck.py
def compare_workspace_to_config(
    workspace_directory: Path,
    config_filename: str,
    full: bool = False,
    verbose: bool = False,
    show_time: bool = False,
    gui: bool = False,
    tui: bool = False,
) -> None:
    """Compare workspace repository versions with a configuration file.

    Displays a table comparing the current branch/version of each repository
    in the workspace with the version specified in the configuration file.

    Args:
        workspace_directory: Path to the workspace directory containing repositories.
        config_filename: Path to the YAML configuration file specifying expected versions.
        full: If True, show all repositories. If False, only show mismatches.
        verbose: If True, print additional information during processing.
        show_time: If True, include time since last commit in the output.
        gui: If True, launch the GUI interface instead of console output.
        tui: If True, launch the TUI interface instead of console output.
    """

    # Load workspace
    source_repos = get_workspace_repos(workspace_directory)

    # Get current branch for each repo
    workspace_current_branch_version = {}
    for repo_name in source_repos:
        workspace_current_branch_version[repo_name] = get_repo_head_ref(
            source_repos[repo_name], verbose
        )

    # Read config file
    with open(config_filename, "r") as file:
        configuration_file_dict = yaml.safe_load(file)["repositories"]

    # Check if source directory exists
    for repo_local_path in configuration_file_dict:
        if not os.path.exists(workspace_directory / repo_local_path) and verbose:
            print(f"{configuration_file_dict[repo_local_path]} does not exist")

    config_file_version = {}
    for config_file_path in configuration_file_dict:
        repo_local_path = config_file_path.split("/")[-1]
        config_file_version[repo_local_path] = configuration_file_dict[
            config_file_path
        ]["version"]

    if gui:
        _show_gui(source_repos, config_filename, config_file_version)
        return

    if tui:
        _show_tui(source_repos, config_filename, config_file_version)
        return

    repos_workspace_config_versions = {}
    repos_workspace_config_versions["Workspace version"] = (
        workspace_current_branch_version
    )
    repos_workspace_config_versions["Config version"] = config_file_version

    show_repos_config_versions(repos_workspace_config_versions, full)

compare_config_files(*config_files, full=False, verbose=False, show_time=False, full_name=False)

Compare repository versions across multiple configuration files.

Reads each configuration file and displays a table comparing the repository versions specified in each file.

Parameters:

Name Type Description Default
*config_files str

Variable number of paths to configuration files.

()
full bool

If True, show all repositories. If False, only show differences.

False
verbose bool

If True, print additional information during processing.

False
show_time bool

If True, include modification time in the output.

False
full_name bool

If True, show full file paths. If False, show only filenames.

False
Source code in src/wcheck/wcheck.py
def compare_config_files(
    *config_files: str,
    full: bool = False,
    verbose: bool = False,
    show_time: bool = False,
    full_name: bool = False,
) -> None:
    """Compare repository versions across multiple configuration files.

    Reads each configuration file and displays a table comparing the
    repository versions specified in each file.

    Args:
        *config_files: Variable number of paths to configuration files.
        full: If True, show all repositories. If False, only show differences.
        verbose: If True, print additional information during processing.
        show_time: If True, include modification time in the output.
        full_name: If True, show full file paths. If False, show only filenames.
    """

    repos_config_versions = {}
    print(f"Comparing {len(config_files)} config files")
    for config_filename in config_files:
        if full_name:
            config_name = config_filename
        else:
            config_name = config_filename.split("/")[-1]
        print(f"Reading {config_filename}")
        try:
            with open(config_filename, "r") as file:
                configuration_file_dict = yaml.safe_load(file)["repositories"]
        except yaml.YAMLError:
            print(f"Config file {config_filename} is not valid YAML")
            continue
        repos_config_versions[config_name] = {}
        for repo_name in configuration_file_dict:
            repos_config_versions[config_name][repo_name] = configuration_file_dict[
                repo_name
            ]["version"]

    show_repos_config_versions(repos_config_versions, full)

compare_config_versions(config_filename, full=False, verbose=False, show_time=False, version_filter=None, stash=False)

Compare versions of a config file across different git branches.

Checks out each branch in the config file's repository and compares the repository versions specified in the config file.

Parameters:

Name Type Description Default
config_filename str

Path to the configuration file to compare.

required
full bool

If True, show all repositories. If False, only show differences.

False
verbose bool

If True, print additional information during processing.

False
show_time bool

If True, include modification time in the output.

False
version_filter list[str] | None

List of regex patterns to filter which branches to compare.

None
stash bool

If True, stash uncommitted changes before switching branches.

False
Source code in src/wcheck/wcheck.py
def compare_config_versions(
    config_filename: str,
    full: bool = False,
    verbose: bool = False,
    show_time: bool = False,
    version_filter: list[str] | None = None,
    stash: bool = False,
) -> None:
    """Compare versions of a config file across different git branches.

    Checks out each branch in the config file's repository and compares
    the repository versions specified in the config file.

    Args:
        config_filename: Path to the configuration file to compare.
        full: If True, show all repositories. If False, only show differences.
        verbose: If True, print additional information during processing.
        show_time: If True, include modification time in the output.
        version_filter: List of regex patterns to filter which branches to compare.
        stash: If True, stash uncommitted changes before switching branches.
    """

    print(f"Comparing config versions in {config_filename}")
    if stash:
        stashed = False
    # Read config file
    try:
        config_repo = Repo(config_filename, search_parent_directories=True)
    except Exception:
        print(f"Config file is not inside a git repository, {config_filename}")
        return

    if config_repo.is_dirty():
        print(
            f"Config repository '{config_repo.working_dir}' is not clean. Commit or stash changes."
        )
        if stash:
            print(f"Stashing changes in {config_repo.working_dir}")
            stashed = True
            config_repo.git.stash()
        else:
            return

    original_branch = config_repo.active_branch.name

    if version_filter is not None:
        print(f"Using filter {version_filter}")

    # Gather branches
    repos_config_versions = {}
    for ref in config_repo.references:
        if version_filter is not None and not matches_any(ref.name, version_filter):
            continue

        config_repo.git.checkout(ref)
        # Read config file
        try:
            with open(config_filename, "r") as file:
                configuration_file_dict = yaml.safe_load(file)["repositories"]
        except yaml.YAMLError:
            if verbose:
                print(f"Config file in {ref} ref is not valid YAML")
            continue

        # Skip remote branches if there are remotes
        if config_repo.remotes and ref.name.startswith(config_repo.remotes[0].name):
            continue  # skip remote branches

        if verbose:
            print(f"parsing {ref}")

        ref_name = ref.name
        if show_time:
            ref_name += (
                " (modified "
                # + pendulum.format_diff(
                # today_datime - ref.commit.authored_datetime, absolute=False
                # )
                + ")"
            )
        repos_config_versions[ref_name] = {}
        for repo_name in configuration_file_dict:
            repos_config_versions[ref_name][repo_name] = configuration_file_dict[
                repo_name
            ]["version"]

    config_repo.git.checkout(original_branch)
    if stash and stashed:
        config_repo.git.stash("pop")
        print(f"Stashed changes back in {config_repo.working_dir}")
        stashed = False

    show_repos_config_versions(repos_config_versions, full)

Utility Functions

get_workspace_repos(workspace_directory)

Find all git repositories in a workspace directory.

Walks the workspace directory and identifies all subdirectories that are git repositories (contain a .git folder). Skips nested repositories for efficiency.

Parameters:

Name Type Description Default
workspace_directory Path

Path to the workspace directory to scan.

required

Returns:

Type Description
dict[str, Repo]

Dictionary mapping repository directory names to Repo objects.

dict[str, Repo]

Returns empty dictionary if workspace_directory is not a directory.

Source code in src/wcheck/wcheck.py
def get_workspace_repos(workspace_directory: Path) -> dict[str, Repo]:
    """Find all git repositories in a workspace directory.

    Walks the workspace directory and identifies all subdirectories
    that are git repositories (contain a .git folder). Skips nested
    repositories for efficiency.

    Args:
        workspace_directory: Path to the workspace directory to scan.

    Returns:
        Dictionary mapping repository directory names to Repo objects.
        Returns empty dictionary if workspace_directory is not a directory.
    """
    source_repos = {}
    if not workspace_directory.is_dir():
        print(f"{workspace_directory} is not a directory")
        return source_repos

    # Gather all repositories in source directory
    for root, dirs, files in os.walk(workspace_directory):
        # Check each subdirectory
        dirs_to_remove = []
        for dir_in_source in dirs:
            d = Path(root) / dir_in_source
            # Check if directory is a git repository
            if (d / ".git").exists():
                source_repos[dir_in_source] = Repo(d)
                # Don't descend into git repos (skip nested repos)
                dirs_to_remove.append(dir_in_source)
        # Remove git repos from dirs to prevent descending
        for d in dirs_to_remove:
            dirs.remove(d)
    return source_repos

get_status_repo(repo)

Get a formatted status string for a repository.

Returns a rich-formatted string showing: - Number of untracked files (U) - Number of modified files (M) - Number of staged files (S) - Number of commits ahead/behind remote

Parameters:

Name Type Description Default
repo Repo

Git repository object.

required

Returns:

Type Description
str

Formatted status string with rich markup, or empty string if clean.

Source code in src/wcheck/wcheck.py
def get_status_repo(repo: Repo) -> str:
    """Get a formatted status string for a repository.

    Returns a rich-formatted string showing:
    - Number of untracked files (U)
    - Number of modified files (M)
    - Number of staged files (S)
    - Number of commits ahead/behind remote

    Args:
        repo: Git repository object.

    Returns:
        Formatted status string with rich markup, or empty string if clean.
    """
    # Check if repo has any commits
    try:
        head_commit = repo.head.commit
        has_commits = True
    except ValueError:
        has_commits = False

    if (repo.is_dirty()) or len(repo.untracked_files) > 0:
        if has_commits:
            n_staged = len(repo.index.diff(head_commit))
        else:
            # For repos with no commits, all indexed files are "staged"
            n_staged = len(list(repo.index.entries.keys()))
        n_changes = len(repo.index.diff(None))
        n_untracked = len(repo.untracked_files)
        print_output = " ("
        if n_untracked > 0:
            print_output += "[orange1]" + str(n_untracked) + "U[/orange1]"
        if n_changes > 0:
            print_output += "[bright_red]" + str(n_changes) + "M[/bright_red]"
        if n_staged > 0:
            print_output += "[bright_magenta]" + str(n_staged) + "S[/bright_magenta]"
        print_output += ")"
    else:
        print_output = ""
        n_staged = 0
        n_changes = 0
        n_untracked = 0

    n_push, n_pull = get_remote_status(repo)
    if n_push > 0 or n_pull > 0:
        print_output += " ["
        if n_push > 0:
            print_output += (
                "[bright_green]" + str(n_push) + "[/bright_green]" + arrow_up + " "
            )
        if n_pull > 0:
            print_output += (
                "[bright_yellow]" + str(n_pull) + "[/bright_yellow]" + arrow_down + " "
            )
        print_output += "]"

    return print_output

get_repo_head_ref(repo, verbose_output=False)

Get the current HEAD reference for a repository.

Returns the branch name, tag name, or commit SHA depending on the state: - If on a branch: returns branch name - If detached at a tag: returns tag name - If detached at a commit: returns commit SHA - If no commits: returns branch name with '(no commits)' suffix

Parameters:

Name Type Description Default
repo Repo

Git repository object.

required
verbose_output bool

If True, print additional information about detached HEAD states.

False

Returns:

Type Description
str

String representing the current HEAD reference.

Source code in src/wcheck/wcheck.py
def get_repo_head_ref(repo: Repo, verbose_output: bool = False) -> str:
    """Get the current HEAD reference for a repository.

    Returns the branch name, tag name, or commit SHA depending on the state:
    - If on a branch: returns branch name
    - If detached at a tag: returns tag name
    - If detached at a commit: returns commit SHA
    - If no commits: returns branch name with '(no commits)' suffix

    Args:
        repo: Git repository object.
        verbose_output: If True, print additional information about detached HEAD states.

    Returns:
        String representing the current HEAD reference.
    """
    # Check if repo has any commits
    try:
        _ = repo.head.commit
    except ValueError:
        # No commits yet, return branch name or "(no commits)"
        try:
            return repo.active_branch.name + " (no commits)"
        except TypeError:
            return "(no commits)"

    if repo.head.is_detached:
        # Use the head commit
        repo_commit = repo.head.commit.hexsha
        head_ref = repo_commit
        repo_name = repo.working_dir.split("/")[-1]
        if verbose_output:
            print(f"{repo_name} DETACHED head at {repo_commit}")
        for tag in repo.tags:
            if (
                tag.commit.hexsha == repo_commit
            ):  # check if the current commit has an associated tag
                if verbose_output:
                    print(f"{repo_name} TAGGED at {tag.name}")
                return tag.name  # use tag_name instead if available
        return head_ref

    else:  # head points to a branch
        return repo.active_branch.name

get_remote_status(repo)

Get the number of commits ahead and behind the remote tracking branch.

Compares the current local branch with its remote tracking branch to determine how many commits need to be pushed and pulled. Uses efficient iter_commits with range notation.

Parameters:

Name Type Description Default
repo Repo

Git repository object.

required

Returns:

Type Description
int

Tuple of (commits_to_push, commits_to_pull). Returns (0, 0) if:

int
  • Repository has no commits
tuple[int, int]
  • HEAD is detached
tuple[int, int]
  • No remotes configured
tuple[int, int]
  • Branch is not tracking a remote
Source code in src/wcheck/wcheck.py
def get_remote_status(repo: Repo) -> tuple[int, int]:
    """Get the number of commits ahead and behind the remote tracking branch.

    Compares the current local branch with its remote tracking branch to determine
    how many commits need to be pushed and pulled. Uses efficient iter_commits
    with range notation.

    Args:
        repo: Git repository object.

    Returns:
        Tuple of (commits_to_push, commits_to_pull). Returns (0, 0) if:
        - Repository has no commits
        - HEAD is detached
        - No remotes configured
        - Branch is not tracking a remote
    """
    # Check if repo has any commits
    try:
        _ = repo.head.commit
    except ValueError:
        return 0, 0  # no commits yet

    if repo.head.is_detached:
        return 0, 0  # no remote status for detached head

    # Check if there are any remotes
    if not repo.remotes:
        return 0, 0  # no remotes configured

    # Try to get tracking branch using GitPython's built-in method
    try:
        branch = repo.active_branch
        tracking = branch.tracking_branch()

        if tracking is None:
            return 0, 0  # branch not tracking a remote

        # Use efficient iter_commits with range notation
        # Commits ahead (local commits not in remote)
        ahead = sum(1 for _ in repo.iter_commits(f"{tracking.name}..{branch.name}"))

        # Commits behind (remote commits not in local)
        behind = sum(1 for _ in repo.iter_commits(f"{branch.name}..{tracking.name}"))

        return ahead, behind
    except Exception:
        return 0, 0

get_elapsed_time_repo(repo)

Get a human-readable string of time since the last commit.

Parameters:

Name Type Description Default
repo Repo

Git repository object.

required

Returns:

Type Description
str

Formatted time difference string (e.g., '2 days', '3 hours'),

str

or 'no commits' if repository has no commits.

Source code in src/wcheck/wcheck.py
def get_elapsed_time_repo(repo: Repo) -> str:
    """Get a human-readable string of time since the last commit.

    Args:
        repo: Git repository object.

    Returns:
        Formatted time difference string (e.g., '2 days', '3 hours'),
        or 'no commits' if repository has no commits.
    """
    try:
        return pendulum.format_diff(
            pendulum.now() - repo.head.commit.committed_datetime, absolute=True
        )
    except ValueError:
        return "no commits"

show_repos_config_versions(repos_config_versions, full=False, gui=True)

Display a table comparing repository versions across configurations.

Creates a rich table showing repository versions from different sources (e.g., workspace vs config file, or multiple config files). Highlights repositories that differ between versions.

Parameters:

Name Type Description Default
repos_config_versions dict[str, dict[str, str]]

Nested dictionary where outer keys are version/config names and inner dictionaries map repo names to their versions.

required
full bool

If True, show all repositories. If False, only show repositories that differ between versions.

False
gui bool

Unused parameter (kept for API compatibility).

True
Source code in src/wcheck/wcheck.py
def show_repos_config_versions(
    repos_config_versions: dict[str, dict[str, str]],
    full: bool = False,
    gui: bool = True,
) -> None:
    """Display a table comparing repository versions across configurations.

    Creates a rich table showing repository versions from different sources
    (e.g., workspace vs config file, or multiple config files). Highlights
    repositories that differ between versions.

    Args:
        repos_config_versions: Nested dictionary where outer keys are version/config
            names and inner dictionaries map repo names to their versions.
        full: If True, show all repositories. If False, only show repositories
            that differ between versions.
        gui: Unused parameter (kept for API compatibility).
    """
    # Get list with all repositories
    repos_set = set()
    for version_name in repos_config_versions:
        for repo_name in repos_config_versions[version_name]:
            repos_set.add(repo_name)

    # Get list with unique repositories
    if len(repos_config_versions) > 1:
        unique_set = set()
        for repo_name in repos_set:
            repo_version = None
            for version_name in repos_config_versions:
                if repo_name not in repos_config_versions[version_name]:
                    if version_name != "Config version":
                        unique_set.add(
                            repo_name
                        )  ## add repo that is not in some version
                    break
                if repo_version is None:  ## first versions
                    repo_version = repos_config_versions[version_name][repo_name]
                elif repo_version != repos_config_versions[version_name][repo_name]:
                    unique_set.add(
                        repo_name
                    )  ## add repo that is different in different versions
                    break
    else:
        unique_set = repos_set

    if full:
        display_set = repos_set
    else:
        display_set = unique_set

    # sort set alphabetically
    display_set = sorted(display_set)

    # Create table
    table = Table(show_header=True, header_style="bold magenta")
    table.add_column("Repo Name")
    for version_name in repos_config_versions:
        table.add_column(version_name)

    # Compare config
    for repo_name in display_set:
        if repo_name in unique_set:
            row_list = [repo_name]
        else:
            row_list = ["[dim]" + repo_name + "[/dim]"]
        for version_name in repos_config_versions:
            if repo_name in repos_config_versions[version_name]:
                if repo_name in unique_set:
                    row_list.append(repos_config_versions[version_name][repo_name])
                else:
                    row_list.append(
                        "[dim]"
                        + repos_config_versions[version_name][repo_name]
                        + "[/dim]"
                    )

            else:
                row_list.append("[dim]N/A[/dim]")
        table.add_row(*row_list)

    if len(table.rows) > 0:
        console.print(table)
    else:
        print("All configurations are identical")

matches_any(name, patternlist)

Match any of the patterns in patternlist.

Parameters:

Name Type Description Default
name str

String to match against.

required
patternlist list[str] | None

List of regular expressions or exact strings to match with.

required

Returns:

Type Description
bool

True if any of the patterns match the string, False otherwise.

Source code in src/wcheck/wcheck.py
def matches_any(name: str, patternlist: list[str] | None) -> bool:
    """Match any of the patterns in patternlist.

    Args:
        name: String to match against.
        patternlist: List of regular expressions or exact strings to match with.

    Returns:
        True if any of the patterns match the string, False otherwise.
    """
    if patternlist is None or len(patternlist) == 0:
        return False
    for pattern in patternlist:
        if str(name).strip() == pattern:
            return True
        if re.match("^[a-zA-Z0-9_/]+$", pattern) is None:
            if re.match(pattern, name.strip()) is not None:
                return True
    return False

fetch_all(repos)

Fetch all remotes for all repositories.

Parameters:

Name Type Description Default
repos dict[str, Repo]

Dictionary mapping repository names to Repo objects.

required
Source code in src/wcheck/wcheck.py
def fetch_all(repos: dict[str, Repo]) -> None:
    """Fetch all remotes for all repositories.

    Args:
        repos: Dictionary mapping repository names to Repo objects.
    """
    for repo in repos:
        for remote in repos[repo].remotes:
            print(f"Fetching {remote.name} from {remote.name}")
            fetch_result = remote.fetch()
            if len(fetch_result) > 0:
                print(f"Fetch {repo}: {fetch_result}")

GUI Module

show_gui(repos, config_file_path='', config_repo=None)

Launch the GUI application for managing repositories.

Creates and displays the main WCheckGUI window. This function does not return as it enters the Qt event loop and exits the program when the window is closed.

Parameters:

Name Type Description Default
repos dict[str, Repo]

Dictionary mapping repository names to Repo objects.

required
config_file_path str

Path to the configuration file (displayed in UI).

''
config_repo dict[str, str] | None

Dictionary mapping repository names to their configured versions.

None
Source code in src/wcheck/gui.py
def show_gui(
    repos: dict[str, Repo],
    config_file_path: str = "",
    config_repo: dict[str, str] | None = None,
) -> NoReturn:
    """Launch the GUI application for managing repositories.

    Creates and displays the main WCheckGUI window. This function does not
    return as it enters the Qt event loop and exits the program when the
    window is closed.

    Args:
        repos: Dictionary mapping repository names to Repo objects.
        config_file_path: Path to the configuration file (displayed in UI).
        config_repo: Dictionary mapping repository names to their configured versions.
    """
    app = QApplication(sys.argv)
    window = WCheckGUI(repos, config_file_path, config_repo)
    window.setWindowTitle("Workspace Repositories")
    window.show()
    sys.exit(app.exec())

WCheckGUI

Bases: QWidget

Main GUI window for wcheck application.

Displays a grid of repositories with their current branches and provides controls for switching branches and opening in editor.

Attributes:

Name Type Description
repo_objects

Dictionary mapping repository names to RepoObject instances.

Source code in src/wcheck/gui.py
class WCheckGUI(QWidget):
    """Main GUI window for wcheck application.

    Displays a grid of repositories with their current branches and
    provides controls for switching branches and opening in editor.

    Attributes:
        repo_objects: Dictionary mapping repository names to RepoObject instances.
    """

    def __init__(
        self,
        repos: dict[str, Repo],
        config_file_path: str = "",
        config_repo: dict[str, str] | None = None,
    ) -> None:
        """Initialize the WCheckGUI window.

        Args:
            repos: Dictionary mapping repository names to Repo objects.
            config_file_path: Path to the configuration file (displayed in UI).
            config_repo: Dictionary mapping repository names to their configured versions.
        """
        super(WCheckGUI, self).__init__()
        self.initUI(repos, config_file_path, config_repo)

    def initUI(
        self,
        repos: dict[str, Repo],
        config_file_path: str = "",
        config_repo: dict[str, str] | None = None,
    ) -> None:
        """Initialize the user interface.

        Creates the layout with repository controls including:
        - Repository name label
        - Branch selection combo box
        - Checkout button
        - Open in editor button
        - Config version label (if config_repo provided)

        Args:
            repos: Dictionary mapping repository names to Repo objects.
            config_file_path: Path to the configuration file (displayed in UI).
            config_repo: Dictionary mapping repository names to their configured versions.
        """
        layout = QVBoxLayout()
        if config_repo is not None:
            layout.addWidget(QLabel(f"Configuration file: {config_file_path}"))
        repo_layout = QGridLayout()
        layout.addLayout(repo_layout)
        self.repo_objects = {}
        for repo_i, repo_name in enumerate(repos):
            self.repo_objects[repo_name] = RepoObject(repos[repo_name], repo_name)

            repo_layout.addWidget(self.repo_objects[repo_name].qlabel, repo_i, 0)
            repo_layout.addWidget(self.repo_objects[repo_name].combo_box, repo_i, 1)
            repo_layout.addWidget(
                self.repo_objects[repo_name].checkout_button, repo_i, 2
            )
            repo_layout.addWidget(self.repo_objects[repo_name].editor_button, repo_i, 3)
            if config_repo is not None:
                if repo_name in config_repo:
                    label_config = QLabel(f"Config {config_repo[repo_name]}")
                    if (
                        config_repo[repo_name]
                        != self.repo_objects[repo_name].active_branch
                    ):
                        label_config.setStyleSheet("background-color: Red")
                    repo_layout.addWidget(label_config, repo_i, 4)
                else:
                    label_config = QLabel("Not in config")
                    label_config.setStyleSheet("color: Gray")
                    repo_layout.addWidget(label_config, repo_i, 4)
        self.setLayout(layout)

__init__(repos, config_file_path='', config_repo=None)

Initialize the WCheckGUI window.

Parameters:

Name Type Description Default
repos dict[str, Repo]

Dictionary mapping repository names to Repo objects.

required
config_file_path str

Path to the configuration file (displayed in UI).

''
config_repo dict[str, str] | None

Dictionary mapping repository names to their configured versions.

None
Source code in src/wcheck/gui.py
def __init__(
    self,
    repos: dict[str, Repo],
    config_file_path: str = "",
    config_repo: dict[str, str] | None = None,
) -> None:
    """Initialize the WCheckGUI window.

    Args:
        repos: Dictionary mapping repository names to Repo objects.
        config_file_path: Path to the configuration file (displayed in UI).
        config_repo: Dictionary mapping repository names to their configured versions.
    """
    super(WCheckGUI, self).__init__()
    self.initUI(repos, config_file_path, config_repo)

initUI(repos, config_file_path='', config_repo=None)

Initialize the user interface.

Creates the layout with repository controls including: - Repository name label - Branch selection combo box - Checkout button - Open in editor button - Config version label (if config_repo provided)

Parameters:

Name Type Description Default
repos dict[str, Repo]

Dictionary mapping repository names to Repo objects.

required
config_file_path str

Path to the configuration file (displayed in UI).

''
config_repo dict[str, str] | None

Dictionary mapping repository names to their configured versions.

None
Source code in src/wcheck/gui.py
def initUI(
    self,
    repos: dict[str, Repo],
    config_file_path: str = "",
    config_repo: dict[str, str] | None = None,
) -> None:
    """Initialize the user interface.

    Creates the layout with repository controls including:
    - Repository name label
    - Branch selection combo box
    - Checkout button
    - Open in editor button
    - Config version label (if config_repo provided)

    Args:
        repos: Dictionary mapping repository names to Repo objects.
        config_file_path: Path to the configuration file (displayed in UI).
        config_repo: Dictionary mapping repository names to their configured versions.
    """
    layout = QVBoxLayout()
    if config_repo is not None:
        layout.addWidget(QLabel(f"Configuration file: {config_file_path}"))
    repo_layout = QGridLayout()
    layout.addLayout(repo_layout)
    self.repo_objects = {}
    for repo_i, repo_name in enumerate(repos):
        self.repo_objects[repo_name] = RepoObject(repos[repo_name], repo_name)

        repo_layout.addWidget(self.repo_objects[repo_name].qlabel, repo_i, 0)
        repo_layout.addWidget(self.repo_objects[repo_name].combo_box, repo_i, 1)
        repo_layout.addWidget(
            self.repo_objects[repo_name].checkout_button, repo_i, 2
        )
        repo_layout.addWidget(self.repo_objects[repo_name].editor_button, repo_i, 3)
        if config_repo is not None:
            if repo_name in config_repo:
                label_config = QLabel(f"Config {config_repo[repo_name]}")
                if (
                    config_repo[repo_name]
                    != self.repo_objects[repo_name].active_branch
                ):
                    label_config.setStyleSheet("background-color: Red")
                repo_layout.addWidget(label_config, repo_i, 4)
            else:
                label_config = QLabel("Not in config")
                label_config.setStyleSheet("color: Gray")
                repo_layout.addWidget(label_config, repo_i, 4)
    self.setLayout(layout)

RepoObject

Represents a repository in the GUI with associated widgets and actions.

Manages the UI components for a single repository including: - Label showing repository name and status - Combo box for branch/tag selection - Checkout button to switch branches - Editor button to open in external editor

Attributes:

Name Type Description
repo

The git repository object.

repo_dirty

Whether the repository has uncommitted changes.

abs_path

Absolute path to the repository.

qlabel

QLabel widget showing repository name.

combo_box

QComboBox widget for branch selection.

checkout_button

QPushButton to checkout selected branch.

editor_button

QPushButton to open repository in editor.

active_branch

Name of the currently active branch.

Source code in src/wcheck/gui.py
class RepoObject:
    """Represents a repository in the GUI with associated widgets and actions.

    Manages the UI components for a single repository including:
    - Label showing repository name and status
    - Combo box for branch/tag selection
    - Checkout button to switch branches
    - Editor button to open in external editor

    Attributes:
        repo: The git repository object.
        repo_dirty: Whether the repository has uncommitted changes.
        abs_path: Absolute path to the repository.
        qlabel: QLabel widget showing repository name.
        combo_box: QComboBox widget for branch selection.
        checkout_button: QPushButton to checkout selected branch.
        editor_button: QPushButton to open repository in editor.
        active_branch: Name of the currently active branch.
    """

    def __init__(self, repo: Repo, repo_name: str, ignore_remote: bool = False) -> None:
        """Initialize the RepoObject with repository and UI components.

        Args:
            repo: Git repository object.
            repo_name: Display name for the repository.
            ignore_remote: If True, exclude remote branches from the combo box.
        """
        # status_str = get_status_repo(repo)
        self.repo_dirty = repo.is_dirty()

        self.repo = repo
        self.abs_path = repo.working_tree_dir + "/"
        self.qlabel = QLabel(f"{repo_name} ")
        if self.repo_dirty:
            self.qlabel.setStyleSheet("background-color: Yellow")
        self.combo_box = QComboBox()
        self.checkout_button = QPushButton("Checkout selected")
        self.editor_button = QPushButton("Open in editor")
        self.active_branch = get_repo_head_ref(repo)

        self.checkout_button.clicked.connect(self.checkout_branch)
        self.editor_button.clicked.connect(self.editor_button_pressed)
        self.checkout_button.setEnabled(False)

        self.combo_box.addItem(str(self.active_branch))

        for ref in self.repo.references:
            if ignore_remote and ref.name.startswith(repo.remotes[0].name):
                continue
            if ref.name != self.active_branch:
                self.combo_box.addItem(str(ref))
        self.combo_box.currentIndexChanged.connect(self.selectionchange)

    def selectionchange(self, index: int) -> None:
        """Handle branch selection change in the combo box.

        Enables the checkout button if a different branch is selected,
        disables it if the current branch is selected.

        Args:
            index: Index of the selected item in the combo box.
        """
        print(f"Selection changed to {self.combo_box.currentText()}")
        branch_name = self.combo_box.currentText()
        if branch_name.startswith("origin/"):
            branch_name = branch_name.replace("origin/", "", 1)
        if branch_name != self.active_branch:
            self.checkout_button.setEnabled(True)
        else:
            self.checkout_button.setEnabled(False)

    def checkout_branch(self) -> None:
        """Checkout the selected branch in the repository.

        Handles both local and remote branch names, stripping the 'origin/'
        prefix from remote branches before checkout.
        """
        print(
            f"Checkout button pressed for repo {self.repo.working_tree_dir}, current label {self.qlabel.text()}"
        )
        print(f" - Checking out branch, {self.combo_box.currentText()}")
        # if the branch is from origin, checkout local branch instead of remote
        branch_name = self.combo_box.currentText()
        if branch_name.startswith("origin/"):
            branch_name = branch_name.replace("origin/", "", 1)

        resutl = self.repo.git.checkout(branch_name)
        print(f" - Result: {resutl}")
        self.active_branch = get_repo_head_ref(self.repo)
        self.selectionchange(0)

    def editor_button_pressed(self) -> None:
        """Open the repository in an external editor.

        Uses the EDITOR environment variable, defaulting to 'code' (VS Code).
        """
        print(f"editor button pressed, {self.repo.working_tree_dir}")
        print(f"{self.abs_path}")
        editor_command_name = os.getenv("EDITOR", "code")
        subprocess.run([editor_command_name, self.abs_path], check=True)

__init__(repo, repo_name, ignore_remote=False)

Initialize the RepoObject with repository and UI components.

Parameters:

Name Type Description Default
repo Repo

Git repository object.

required
repo_name str

Display name for the repository.

required
ignore_remote bool

If True, exclude remote branches from the combo box.

False
Source code in src/wcheck/gui.py
def __init__(self, repo: Repo, repo_name: str, ignore_remote: bool = False) -> None:
    """Initialize the RepoObject with repository and UI components.

    Args:
        repo: Git repository object.
        repo_name: Display name for the repository.
        ignore_remote: If True, exclude remote branches from the combo box.
    """
    # status_str = get_status_repo(repo)
    self.repo_dirty = repo.is_dirty()

    self.repo = repo
    self.abs_path = repo.working_tree_dir + "/"
    self.qlabel = QLabel(f"{repo_name} ")
    if self.repo_dirty:
        self.qlabel.setStyleSheet("background-color: Yellow")
    self.combo_box = QComboBox()
    self.checkout_button = QPushButton("Checkout selected")
    self.editor_button = QPushButton("Open in editor")
    self.active_branch = get_repo_head_ref(repo)

    self.checkout_button.clicked.connect(self.checkout_branch)
    self.editor_button.clicked.connect(self.editor_button_pressed)
    self.checkout_button.setEnabled(False)

    self.combo_box.addItem(str(self.active_branch))

    for ref in self.repo.references:
        if ignore_remote and ref.name.startswith(repo.remotes[0].name):
            continue
        if ref.name != self.active_branch:
            self.combo_box.addItem(str(ref))
    self.combo_box.currentIndexChanged.connect(self.selectionchange)

checkout_branch()

Checkout the selected branch in the repository.

Handles both local and remote branch names, stripping the 'origin/' prefix from remote branches before checkout.

Source code in src/wcheck/gui.py
def checkout_branch(self) -> None:
    """Checkout the selected branch in the repository.

    Handles both local and remote branch names, stripping the 'origin/'
    prefix from remote branches before checkout.
    """
    print(
        f"Checkout button pressed for repo {self.repo.working_tree_dir}, current label {self.qlabel.text()}"
    )
    print(f" - Checking out branch, {self.combo_box.currentText()}")
    # if the branch is from origin, checkout local branch instead of remote
    branch_name = self.combo_box.currentText()
    if branch_name.startswith("origin/"):
        branch_name = branch_name.replace("origin/", "", 1)

    resutl = self.repo.git.checkout(branch_name)
    print(f" - Result: {resutl}")
    self.active_branch = get_repo_head_ref(self.repo)
    self.selectionchange(0)

editor_button_pressed()

Open the repository in an external editor.

Uses the EDITOR environment variable, defaulting to 'code' (VS Code).

Source code in src/wcheck/gui.py
def editor_button_pressed(self) -> None:
    """Open the repository in an external editor.

    Uses the EDITOR environment variable, defaulting to 'code' (VS Code).
    """
    print(f"editor button pressed, {self.repo.working_tree_dir}")
    print(f"{self.abs_path}")
    editor_command_name = os.getenv("EDITOR", "code")
    subprocess.run([editor_command_name, self.abs_path], check=True)

selectionchange(index)

Handle branch selection change in the combo box.

Enables the checkout button if a different branch is selected, disables it if the current branch is selected.

Parameters:

Name Type Description Default
index int

Index of the selected item in the combo box.

required
Source code in src/wcheck/gui.py
def selectionchange(self, index: int) -> None:
    """Handle branch selection change in the combo box.

    Enables the checkout button if a different branch is selected,
    disables it if the current branch is selected.

    Args:
        index: Index of the selected item in the combo box.
    """
    print(f"Selection changed to {self.combo_box.currentText()}")
    branch_name = self.combo_box.currentText()
    if branch_name.startswith("origin/"):
        branch_name = branch_name.replace("origin/", "", 1)
    if branch_name != self.active_branch:
        self.checkout_button.setEnabled(True)
    else:
        self.checkout_button.setEnabled(False)