Git, GitHub, and Configuration¶
The modules that reach outside the directory being scanned: the local Git repository, GitHub, and the user and project configuration files.
Git Status¶
recursivist.git_status
¶
Git status lookup.
Wraps git status --porcelain and maps changed/untracked paths back to a location
relative to the directory being visualised. Pure standard library.
get_git_status
¶
Get Git status for files relative to a given directory.
Runs git status --porcelain -z --untracked-files=all from the repository root
and maps every changed/untracked path back to a path relative to directory,
filtering out files that live outside of it.
Status characters returned:
'U': Untracked (??in porcelain output)'M': Modified (working-tree or staged modification)'A': Added / staged for the first time (includes renames)'D': Deleted (working-tree or staged deletion)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
str
|
Absolute path to the directory being visualised. Must be inside a Git repository. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
|
dict[str, str]
|
regardless of OS, or an empty dict when Git is unavailable or the directory is |
dict[str, str]
|
not tracked. |
Source code in recursivist/git_status.py
GitHub¶
A GitHub repository URL passed to visualize, export, or compare is resolved here. parse_github_url turns a URL into a GitHubTarget, and checkout_repository downloads the repository's source archive into a temporary directory and yields a RepoCheckout whose local_root is scanned like any other directory. apply_github_urls rewrites file paths to GitHub blob URLs for --full-path output.
recursivist.github
¶
Remote GitHub repository support.
Lets the visualize, export and compare commands accept a GitHub repository
URL anywhere they accept a local directory. A repository is materialized by
downloading its source archive from codeload.github.com and extracting it into a
temporary directory, which is then scanned, rendered and exported exactly like a local
directory.
The archive endpoint is used rather than the REST API on purpose: the REST API limits
unauthenticated clients to 60 requests/hour (shared per public IP), which is easily
exhausted, whereas archive downloads are not subject to that limit. Only the default
branch is resolved through a lightweight, unlimited info/refs request when the
caller did not pin a ref explicitly.
Because a hosted repository already reflects its ignore rules — files excluded by a
.gitignore are simply absent — and because every file in a checkout shares the same
Git status and effective modification time (the tip commit's), the --ignore-file,
--git-status, --sort-by-git-status, --mtime and --sort-by-mtime options
are not meaningful for a GitHub input and are skipped. The lines-of-code and size
annotations are retained, since they are derived from the file contents. The
--full-path option still applies, but instead of a filesystem path it shows each
file's canonical GitHub blob URL.
Only the Python standard library is used. When a token is present in the
GITHUB_TOKEN or GH_TOKEN environment variable it is sent with every request,
which raises the rate limits and enables access to private repositories.
GitHubError
¶
Bases: Exception
Raised when a GitHub repository cannot be resolved or downloaded.
Carries a human-readable message suitable for surfacing directly to the user (e.g. an invalid URL, a missing repository, a rate-limit response, or a network/extraction failure).
Source code in recursivist/github.py
GitHubTarget
dataclass
¶
A parsed reference to a GitHub repository or a subtree within one.
Attributes:
| Name | Type | Description |
|---|---|---|
owner |
str
|
Repository owner (user or organization). |
repo |
str
|
Repository name, without any trailing |
ref |
str | None
|
The branch, tag, or commit the caller pinned via |
subpath |
str
|
A forward-slashed path within the repository to treat as the root of
the scan, or |
Source code in recursivist/github.py
display_name
property
¶
A short label for the scanned root (subpath basename or repo name).
blob_url
¶
Return the canonical GitHub blob URL for a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
str
|
The concrete ref (branch, tag, or commit) to embed in the URL. |
required |
relpath
|
str
|
The file's forward-slashed path relative to the repository root
(already including any |
required |
Returns:
| Type | Description |
|---|---|
str
|
A URL of the form |
str
|
with the ref and path percent-encoded so that characters such as spaces, |
str
|
|
Source code in recursivist/github.py
RepoCheckout
dataclass
¶
A materialized GitHub repository on the local filesystem.
Attributes:
| Name | Type | Description |
|---|---|---|
target |
GitHubTarget
|
The |
local_root |
str
|
Absolute path to the directory to scan — the extracted repository root, or the requested subpath within it (the containing directory, when the subpath pointed at a file). |
ref |
str
|
The concrete ref that was downloaded (the pinned ref, or the resolved default branch). |
root_name |
str
|
The display name for the scanned root (the repository name, or the last segment of the scanned subpath). |
Source code in recursivist/github.py
get_github_token
¶
Return a GitHub token from the environment, if configured.
Looks up GITHUB_TOKEN first and then GH_TOKEN. When set, the token is sent
with archive and ref requests, raising rate limits and permitting access to private
repositories.
Returns:
| Type | Description |
|---|---|
str | None
|
The token string, or |
Source code in recursivist/github.py
parse_github_url
¶
parse_github_url(text: str) -> GitHubTarget | None
Parse a GitHub repository URL into a
GitHubTarget.
Accepts the common HTTPS forms (with or without scheme, www. or a trailing
.git), an optional /tree/<ref>[/<subpath>] or /blob/<ref>/<subpath>
selector, and the SSH form git@github.com:owner/repo.git. Percent-encoded
characters in the ref and subpath (e.g. %20, %23) are decoded, so URLs
copied from a browser address bar resolve to the real names. The scheme and host are
matched case-insensitively, as URL hosts are; owner, repository, ref and subpath are
kept exactly as written.
A form without a scheme (github.com/owner/repo) is also a valid relative
filesystem path, e.g. a GOPATH-style src/github.com/golang/go checkout entered
from src. When such an argument names an existing local file or directory, it is
treated as that local path and None is returned. A URL with an explicit
http(s):// scheme, or the SSH form, is always treated as GitHub.
The subpath is returned as written, whether it names a directory or a file: the two
cannot be told apart from the URL alone. A subpath that turns out to be a file — as
in the /blob/<ref>/<file> URL of a GitHub file page — is resolved to its
containing directory by
checkout_repository.
When a /tree or /blob selector is present, the segment immediately after it
is taken as the ref and everything beyond it as the subpath. Refs that themselves
contain slashes (e.g. feature/x) therefore cannot be distinguished from a
subpath by URL alone; pass such a repository without a selector, or pin the ref with
a plain branch name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The raw argument to parse. |
required |
Returns:
| Type | Description |
|---|---|
GitHubTarget | None
|
The parsed |
GitHubTarget | None
|
text is not a recognizable GitHub URL. |
Source code in recursivist/github.py
resolve_default_branch
¶
resolve_default_branch(target: GitHubTarget, token: str | None = None) -> str
Resolve a repository's default branch without using the REST API.
Reads the symbolic HEAD reference from the repository's Git smart-HTTP
info/refs advertisement, which is not subject to the REST API's unauthenticated
rate limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
GitHubTarget
|
The repository whose default branch is wanted. |
required |
token
|
str | None
|
Optional GitHub token for private repositories. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The default branch name (e.g. |
Raises:
| Type | Description |
|---|---|
GitHubError
|
If the repository is missing or private without a valid token, or if the default branch cannot be determined. |
Source code in recursivist/github.py
resolve_commit_shas
¶
resolve_commit_shas(target: GitHubTarget, refs: list[str | None], token: str | None = None) -> list[str | None]
Resolve each ref in refs to a commit SHA using one advertisement fetch.
A single info/refs request is made and reused for every ref, so this is cheap
even for several refs on the same repository. Each entry is resolved as follows:
Noneresolves to the commit the default branch (HEAD) points at.- A branch or tag name resolves to its tip commit; annotated tags resolve to the commit they dereference to.
- A value that is not an advertised ref but looks like a commit SHA (7-40 hex characters) is returned as-is, lowercased, so explicit commit pins are supported.
- Anything else resolves to
None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
GitHubTarget
|
The repository to resolve against. |
required |
refs
|
list[str | None]
|
The refs to resolve, in order. |
required |
token
|
str | None
|
Optional GitHub token for private repositories. |
None
|
Returns:
| Type | Description |
|---|---|
list[str | None]
|
A list the same length as refs, each a lowercase commit SHA or |
list[str | None]
|
the ref could not be resolved. |
Raises:
| Type | Description |
|---|---|
GitHubError
|
If the repository is missing, private, or unreachable. |
Source code in recursivist/github.py
commit_shas_equal
¶
Return whether two commit SHAs identify the same commit.
Handles abbreviated SHAs (as short as 7 characters, Git's conventional minimum) by
treating one as a match for the other when it is a case-insensitive prefix. None
never matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sha1
|
str | None
|
The first commit SHA, or |
required |
sha2
|
str | None
|
The second commit SHA, or |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in recursivist/github.py
same_github_target
¶
same_github_target(target1: GitHubTarget, target2: GitHubTarget, token: str | None = None) -> bool
Return whether two GitHub targets refer to the same scanned tree.
Owner and repository names are compared case-insensitively because GitHub treats them that way, while the subpath is compared case-sensitively because file paths are case-sensitive.
When both sides pin the same ref (or neither does, so both use the default branch), no network access is needed. Otherwise the two refs are resolved to the commits they point at and compared, so that distinct refs that name the same commit — a branch and a tag on the same tip, a branch and the default branch, or a branch and an explicit commit SHA — are recognized as the same. If either ref cannot be resolved (repository missing, private, unreachable, or the ref does not exist), the targets are treated as not the same so the normal comparison flow can surface the real error rather than a misleading "compare with itself" message.
Subpaths are compared as given. A subpath that names a file is only resolved to its
containing directory by
checkout_repository, so two URLs for
different files in one directory are recognized as the same tree only when the
targets passed here are the checked-out ones (RepoCheckout.target).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target1
|
GitHubTarget
|
The first GitHub target. |
required |
target2
|
GitHubTarget
|
The second GitHub target. |
required |
token
|
str | None
|
Optional GitHub token used for the ref lookups. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
else |
Source code in recursivist/github.py
checkout_repository
¶
checkout_repository(target: GitHubTarget, token: str | None = None) -> Generator[RepoCheckout]
Download and extract a GitHub repository into a temporary directory.
Resolves the ref (using the default branch when the target does not pin one), downloads the source archive, and safely extracts it. The extracted files are removed when the context exits.
When the target's subpath names a file rather than a directory, the directory
containing that file is checked out instead, and the yielded checkout's target
carries that directory as its subpath.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
GitHubTarget
|
The repository (and optional subtree) to check out. |
required |
token
|
str | None
|
Optional GitHub token; defaults to
|
None
|
Yields:
| Type | Description |
|---|---|
Generator[RepoCheckout]
|
A |
Generator[RepoCheckout]
|
extraction. |
Raises:
| Type | Description |
|---|---|
GitHubError
|
If the repository cannot be resolved, downloaded, or extracted, or the requested subpath does not exist in it. |
Source code in recursivist/github.py
apply_github_urls
¶
apply_github_urls(structure: Directory, checkout: RepoCheckout) -> Directory
Rewrite each file's display path to its GitHub blob URL, in place.
Used when --full-path is requested for a GitHub input: it walks structure and
replaces every FileEntry path with the file's
canonical blob URL, so the renderers and exporters display GitHub URLs instead of
temporary filesystem paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
Directory
|
A scanned structure produced by
|
required |
checkout
|
RepoCheckout
|
The checkout the structure was scanned from, supplying the owner, repo, ref, and subpath used to build URLs. |
required |
Returns:
| Type | Description |
|---|---|
Directory
|
The same structure object, with file paths rewritten. |
Source code in recursivist/github.py
Configuration¶
recursivist.config
¶
User and project configuration.
Preferences come from two layers. The user layer is recursivist's JSON settings file,
whose location is resolved with Typer's platform-aware application directory and which
recursivist config set writes. The project layer is a TOML file that lives with
the directory being scanned: either a dedicated .recursivist.toml or a
[tool.recursivist] table in pyproject.toml.
resolve_config merges them, each layer overriding
the ones before it: built-in defaults, then the user file, then the project file. A
command-line flag overrides all three. The preferences are the icon style, the name of
the ignore file to honor, and the directories to exclude.
resolve_config_layers gives the same
resolution layer by layer, naming the file each value comes from. It is what
recursivist config list prints.
Both files can be edited by hand, so both are validated as they are loaded: an unknown key or an unacceptable value is reported with a warning and left out, and the setting falls through to the layer below.
PROJECT_CONFIG_FILE
module-attribute
¶
Dedicated project configuration file, holding the settings at its top level.
PYPROJECT_FILE
module-attribute
¶
Shared project file, holding the settings in its [tool.recursivist] table.
IconStyle
module-attribute
¶
Icon styles accepted by --icon-style and the icon-style config key.
CONFIG_KEYS
module-attribute
¶
CONFIG_KEYS: dict[str, tuple[str, ...] | None] = {'icon_style': ICON_STYLES, 'ignore_file': None, 'exclude': None}
Recognized configuration keys (in their stored, underscored form) mapped to the
strings each one accepts: a tuple of choices, or None for a key that accepts any
string that is not blank. A key in LIST_KEYS holds a list of such strings.
LIST_KEYS
module-attribute
¶
Configuration keys whose value is a list of strings rather than a single string.
DEFAULT_CONFIG
module-attribute
¶
Built-in value of every configuration key, used when no layer sets it. A value of
None leaves the setting without a value: by default, no ignore file is honored and
no directory is excluded.
LAYER_PROJECT
module-attribute
¶
Name of the layer read from the project configuration file.
LAYER_USER
module-attribute
¶
Name of the layer read from the user configuration file.
LAYER_DEFAULT
module-attribute
¶
Name of the layer holding the built-in defaults.
ConfigLayer
dataclass
¶
What one configuration layer holds for a single setting.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The layer: |
value |
str | list[str] | None
|
The layer's value for the setting, or |
source |
Path | None
|
The file the layer is read from, or |
Source code in recursivist/config.py
accepts_value
¶
Return whether value is one the configuration key accepts.
A key with a set of choices accepts exactly those strings; any other key accepts a
string that is not empty or made of whitespace only. A key in LIST_KEYS accepts a
list of one or more such strings, and every other key a single one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A key of |
required |
value
|
Any
|
The value to check, of any type. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in recursivist/config.py
describe_accepted_values
¶
Return a phrase naming the values the configuration key accepts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A key of |
required |
Returns:
| Type | Description |
|---|---|
str
|
A phrase that completes the sentence "Use ...": the quoted choices of a key |
str
|
that has them ( |
str
|
strings |
str
|
key. |
Source code in recursivist/config.py
get_config_path
¶
Return the path to the configuration file.
The file is config.json inside the directory given by typer.get_app_dir, so
its location follows each platform's convention for application data. Nothing is
created on disk: the file and its directory may not exist until save_config
writes them.
Source code in recursivist/config.py
read_config_file
¶
Read the user configuration file exactly as it is stored.
Nothing is validated, so the mapping may hold keys and values that recursivist does
not recognize. It is what recursivist config set and config unset edit, so
that changing one setting leaves the rest of the file untouched. Use load_config
for settings that are safe to act on.
A file that cannot be read, is not valid JSON, or does not hold a JSON object is reported with a warning and treated as empty. Reading never writes: a missing configuration directory is left missing.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The stored mapping, or an empty one when the file is missing or unusable. |
Source code in recursivist/config.py
load_config
¶
Load the valid settings of the user configuration file.
The file is validated as it is loaded, so a hand-edited mistake never reaches the
rest of the program: an unknown key or an unacceptable value is reported with a
warning and left out. The warning for an unknown key gives the recursivist config
unset command that removes it. Keys may be written with dashes or underscores.
Reading never writes, so the file itself is left as it is.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The valid settings, keyed in their underscored form. Settings the file does not |
dict[str, Any]
|
set, or sets wrongly, are absent; the mapping is empty when the file is missing |
dict[str, Any]
|
or unusable. Use |
Source code in recursivist/config.py
save_config
¶
Write the user configuration to disk as indented JSON.
Creates the configuration directory if it does not already exist.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
dict[str, Any]
|
Configuration mapping to persist. Overwrites any existing file at the configuration path. |
required |
Source code in recursivist/config.py
delete_config_file
¶
Delete the user configuration file.
The file is removed whatever it holds, including one that cannot be read as configuration, so every setting falls back to the layers below. The directory that holds the file is left in place, and nothing is created.
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
OSError
|
If the file exists but cannot be removed. |
Source code in recursivist/config.py
resolve_config_layers
¶
resolve_config_layers(project_dir: Path | None = None) -> dict[str, list[ConfigLayer]]
Return what every configuration layer holds for each setting.
The layers of a setting are listed from the highest precedence to the lowest: the
project configuration that applies to project_dir, the user configuration file,
then the built-in default. The first layer that has a value is the one in effect;
a setting whose built-in default is None has no value when neither file sets
it. Both files are validated as they are loaded, so every value is one its key
accepts, and a layer whose file sets a key wrongly counts as not setting it. Reading
never writes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_dir
|
Path | None
|
Directory whose project configuration should apply, normally the
one being scanned. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, list[ConfigLayer]]
|
A mapping from every key in |
Source code in recursivist/config.py
resolve_config
¶
Return the effective configuration for a run.
Layers are applied in order, each overriding the previous one: the built-in
defaults, the user configuration file, then the project configuration that applies
to project_dir. Both files are validated as they are loaded, so every value in the
result is one its key accepts, or None for a setting that no layer gives a
value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_dir
|
Path | None
|
Directory whose project configuration should apply, normally the
one being scanned. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The merged configuration mapping, with every key in |