Skip to content

Proposal: modularize bin/git-forgit into a stable entrypoint plus lib/ #507

@wfxr

Description

@wfxr

Background

bin/git-forgit currently acts as the executable entrypoint, the shared helper library, the implementation of every command, the implementation of preview/private commands, and the dispatcher. As forgit keeps growing, this single-file structure increases review cost, makes reuse harder, and raises the chance of accidental coupling between unrelated commands.

I would like to propose a larger refactor that keeps the current user-facing behavior intact, while reorganizing the implementation into a clearer internal structure.

Goals

  • Keep bin/git-forgit as the stable executable entrypoint.
  • Split shared helpers and command entrypoints into lib/.
  • Keep the first phase simple: load everything up front, then dispatch as today.
  • Preserve both primary usage modes:
    • shell plugin usage in bash/zsh/fish
    • git forgit <command> usage via git-forgit on $PATH
  • Make internal helpers easier to reuse without copying code or sourcing the entire bin/git-forgit.

Non-goals

  • No command renames.
  • No alias or completion behavior changes.
  • No lazy-loading in the first phase.
  • No promise that lib/* becomes a stable public API.

Proposed structure

bin/
  git-forgit

lib/
  bashunit
  core.sh
  helpers/
    ansi.sh
    git.sh
    parse.sh
    pager.sh
    path.sh
    preview.sh
    repo.sh
    select.sh
    text.sh
  forgit.sh
  • bin/git-forgit
    • stays as the stable executable entrypoint
    • is responsible for setup, loading modules, and dispatch
  • lib/core.sh
    • contains paths, constants, environment setup, and the most fundamental shared helpers
  • lib/helpers/*.sh
    • contains reusable helpers grouped by capability rather than by command/domain
  • lib/forgit.sh
    • contains command entrypoints and private command entrypoints
    • would ideally stay thin and orchestration-focused

Why keep a separate lib/forgit.sh?

Keeping a separate lib/forgit.sh makes the boundary clearer:

  • bin/git-forgit becomes a small and stable entrypoint
  • command implementation can continue to evolve without repeatedly reshaping the executable wrapper itself

Why group helpers by capability?

One goal here is to encourage reuse rather than local reimplementation where possible.

For that reason, it may be more useful to group helpers primarily by capability, such as parsing, selection, preview building, text processing, git wrappers, and path handling, instead of grouping them by command/domain from the start.

That makes it easier for a new command to compose existing helpers, instead of adding yet another command-local helper block.

lib/api.sh

It may also be worth reserving room for a future lib/api.sh, without making it part of phase 1.

The idea would be:

  • lib/api.sh can expose many internal helpers
  • this is for convenience and reuse, not as an officially supported stable API
  • external consumers may use it, but it should be understood as depending on forgit internals
  • helpers exposed there may change, move, be renamed, or be removed at any time

So api.sh would be a centralized export surface for internal implementation, not a compatibility promise.

Loading model

One possible way to keep phase 1 simple is:

  1. bin/git-forgit resolves FORGIT_INSTALL_DIR
  2. it loads lib/core.sh
  3. it loads the helper modules
  4. it loads lib/forgit.sh
  5. it dispatches to _forgit_"${cmd}"

The main point of this phase would be structural cleanup rather than changing execution semantics.

Compatibility requirements

In particular, it would be important not to break either of the two main ways people use forgit:

1. Shell plugin mode

Ideally these would continue to work without requiring user config changes:

  • forgit.plugin.zsh
  • forgit.plugin.sh
  • conf.d/forgit.plugin.fish

That includes:

  • existing wrapper functions
  • existing aliases / abbreviations
  • existing FORGIT_* configuration behavior
  • worktree wrappers with cd behavior

2. Git subcommand mode

These would ideally keep working as they do today:

  • git forgit <command>
  • git aliases that expand to forgit ...

That suggests git-forgit should remain the executable name and should stay self-contained as an entrypoint.

3. Completions

The existing bash/zsh/fish completion behavior would ideally remain unchanged in phase 1.

Risks

  • reviewers may interpret lib/api.sh as a stable API unless we explicitly document otherwise
  • packaging/install paths may miss newly added lib/** files

Alternatives considered

Keep everything in one file and only reorganize sections

Pros:

  • lowest risk
  • minimal packaging/testing impact

Cons:

  • does not create real module boundaries
  • is less helpful for improving helper reuse
  • continues to keep bin/git-forgit as the main implementation container

Add lazy-loading from the start

Pros:

  • could enable finer-grained loading later

Cons:

  • adds more shell complexity immediately
  • makes the first refactor harder to reason about and review

Open questions

  • How coarse- or fine-grained should the initial lib/helpers/*.sh split be?
  • Is it worth splitting lib/forgit.sh further at all, and if so, when?
  • Is it worth preserving existing helper names during the initial move and deferring naming cleanup to a later pass?
  • Is it worth eventually consolidating subcommand metadata—such as names, descriptions, and completion behavior—so bash, zsh, and fish completions can share a single definition?

Summary

My current inclination would be to:

  • keep bin/git-forgit as the stable entrypoint
  • move shared logic into lib/
  • keep phase 1 fully eager-loaded
  • organize helpers by capability
  • treat any future lib/api.sh as a convenience export layer for internals, not as a stable public API

@cjappl @carlfriedrich @sandr01d would love to hear what you all think too. Does this direction seem reasonable? Especially curious whether the split between bin/git-forgit, lib/forgit.sh, and lib/helpers/* feels right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions