Initial commit: modular bash configuration
Reinitialised repo to purge credential history. Credential files are now gitignored with .example templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
4cfec0b336
22 changed files with 364 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Credential files — never commit real secrets
|
||||||
|
99-claude
|
||||||
|
99-gemini
|
||||||
|
99-google
|
||||||
|
99-huggingface
|
||||||
|
99-replicate
|
||||||
19
00-credential-guard
Executable file
19
00-credential-guard
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Helper to warn if a credential file has loose permissions
|
||||||
|
|
||||||
|
# require_private <file>
|
||||||
|
# Emits a warning to stderr if the file is group- or world-readable.
|
||||||
|
require_private() {
|
||||||
|
local file="$1"
|
||||||
|
[[ -f "$file" ]] || return 0
|
||||||
|
local perms
|
||||||
|
perms=$(stat -c %a "$file" 2>/dev/null) || {
|
||||||
|
echo "bash.d: WARNING: cannot check permissions on $file (stat failed)" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# Check that group and other bits are both zero (e.g., 600, 700)
|
||||||
|
# Uses arithmetic on octal value to handle both 3- and 4-digit modes
|
||||||
|
if (( (8#$perms) & 8#077 )); then
|
||||||
|
echo "bash.d: WARNING: $file is group/world-accessible (mode $perms). Run: chmod 600 $file" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
13
00-path-helper
Executable file
13
00-path-helper
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Helper functions to safely modify PATH
|
||||||
|
# Both check that the directory exists AND prevent duplicates on re-source
|
||||||
|
|
||||||
|
# Append directory to PATH (lower priority than existing entries)
|
||||||
|
path_append() {
|
||||||
|
[[ -d "$1" ]] && [[ ":$PATH:" != *":$1:"* ]] && PATH="$PATH:$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepend directory to PATH (higher priority than existing entries)
|
||||||
|
path_prepend() {
|
||||||
|
[[ -d "$1" ]] && [[ ":$PATH:" != *":$1:"* ]] && PATH="$1:$PATH"
|
||||||
|
}
|
||||||
2
10-bun-path
Executable file
2
10-bun-path
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
path_append "$HOME/.bun/bin"
|
||||||
3
10-go-path
Executable file
3
10-go-path
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
export GOPATH="$HOME/go"
|
||||||
|
path_append "$GOPATH/bin"
|
||||||
3
10-rust-path
Executable file
3
10-rust-path
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
export CARGO_HOME="$HOME/.cargo"
|
||||||
|
path_append "$CARGO_HOME/bin"
|
||||||
9
20-ninja
Executable file
9
20-ninja
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
if command -v ninja &>/dev/null; then
|
||||||
|
export NINJA_STATUS="[%e sec | %p (%u remaining) | %o / sec] "
|
||||||
|
export CMAKE_GENERATOR=Ninja
|
||||||
|
fi
|
||||||
|
if command -v ccache &>/dev/null; then
|
||||||
|
export CMAKE_C_COMPILER_LAUNCHER=ccache
|
||||||
|
export CMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||||
|
fi
|
||||||
13
20-oneapi
Executable file
13
20-oneapi
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Intel oneAPI toolkit — source the environment setup if installed
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
ONEAPI_SETVARS="/opt/intel/oneapi/setvars.sh"
|
||||||
|
if [[ -f "$ONEAPI_SETVARS" ]]; then
|
||||||
|
. "$ONEAPI_SETVARS" --force > /dev/null
|
||||||
|
if [[ -n "$ONEAPI_ROOT" ]]; then
|
||||||
|
case ":${LD_LIBRARY_PATH}:" in
|
||||||
|
*":${ONEAPI_ROOT}/lib:"*) ;;
|
||||||
|
*) export LD_LIBRARY_PATH="${ONEAPI_ROOT}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
6
30-starship
Executable file
6
30-starship
Executable file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Initialize Starship prompt
|
||||||
|
# Requires: starship binary in PATH (installed to ~/.local/bin)
|
||||||
|
if command -v starship &>/dev/null; then
|
||||||
|
eval "$(starship init bash)"
|
||||||
|
fi
|
||||||
3
50-asdf-completion
Executable file
3
50-asdf-completion
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
command -v asdf &>/dev/null && . <(asdf completion bash)
|
||||||
72
50-claude-completion
Executable file
72
50-claude-completion
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# shellcheck disable=SC2207 # Standard pattern for bash completions
|
||||||
|
# Bash completion for Claude Code CLI
|
||||||
|
|
||||||
|
command -v claude &>/dev/null || return
|
||||||
|
|
||||||
|
_claude_completions() {
|
||||||
|
local cur prev opts commands
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
|
||||||
|
# Subcommands
|
||||||
|
commands="doctor install mcp plugin setup-token update"
|
||||||
|
|
||||||
|
# All options
|
||||||
|
opts="-c -d -h -p -r -v
|
||||||
|
--add-dir --agent --agents --allow-dangerously-skip-permissions
|
||||||
|
--allowed-tools --append-system-prompt --betas --chrome --continue
|
||||||
|
--dangerously-skip-permissions --debug --debug-file --disable-slash-commands
|
||||||
|
--disallowed-tools --fallback-model --file --fork-session --help --ide
|
||||||
|
--include-partial-messages --input-format --json-schema --max-budget-usd
|
||||||
|
--mcp-config --model --no-chrome --no-session-persistence --output-format
|
||||||
|
--permission-mode --plugin-dir --print --replay-user-messages --resume
|
||||||
|
--session-id --setting-sources --settings --strict-mcp-config
|
||||||
|
--system-prompt --tools --verbose --version"
|
||||||
|
|
||||||
|
# Handle option arguments
|
||||||
|
case "${prev}" in
|
||||||
|
--model|--fallback-model)
|
||||||
|
COMPREPLY=( $(compgen -W "sonnet opus haiku" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--permission-mode)
|
||||||
|
COMPREPLY=( $(compgen -W "acceptEdits bypassPermissions default delegate dontAsk plan" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--input-format)
|
||||||
|
COMPREPLY=( $(compgen -W "text stream-json" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--output-format)
|
||||||
|
COMPREPLY=( $(compgen -W "text json stream-json" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--add-dir|--plugin-dir|--debug-file)
|
||||||
|
COMPREPLY=( $(compgen -d -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--settings|--mcp-config)
|
||||||
|
COMPREPLY=( $(compgen -f -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
COMPREPLY=( $(compgen -W "stable latest" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Complete subcommands or options
|
||||||
|
if [[ ${cur} == -* ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
elif [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "${commands} ${opts}" -- "${cur}") )
|
||||||
|
else
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _claude_completions claude
|
||||||
3
50-fj-completion
Executable file
3
50-fj-completion
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
command -v fj &>/dev/null && . <(fj completion bash)
|
||||||
3
50-tailscale-completion
Executable file
3
50-tailscale-completion
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
command -v tailscale &>/dev/null && . <(tailscale completion bash)
|
||||||
3
50-uv-completion
Executable file
3
50-uv-completion
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Enable bash completion for uv (Python package manager)
|
||||||
|
command -v uv &>/dev/null && eval "$(uv generate-shell-completion bash)"
|
||||||
9
99-android
Executable file
9
99-android
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
if [[ -d "$HOME/android-sdk" ]]; then
|
||||||
|
export ANDROID_HOME="$HOME/android-sdk"
|
||||||
|
path_append "$ANDROID_HOME/cmdline-tools/latest/bin"
|
||||||
|
path_append "$ANDROID_HOME/platform-tools"
|
||||||
|
fi
|
||||||
|
if [[ -d /usr/lib/jvm/java-17-openjdk-amd64 ]]; then
|
||||||
|
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
fi
|
||||||
4
99-claude.example
Normal file
4
99-claude.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Copy to 99-claude and fill in your token, then: chmod 700 99-claude
|
||||||
|
require_private "${BASH_SOURCE[0]}"
|
||||||
|
export FORGEJO_ISSUE_TOKEN_FOR_CLAUDE=your-token-here
|
||||||
4
99-gemini.example
Normal file
4
99-gemini.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Copy to 99-gemini and fill in your key, then: chmod 700 99-gemini
|
||||||
|
require_private "${BASH_SOURCE[0]}"
|
||||||
|
export GEMINI_API_KEY=your-key-here
|
||||||
4
99-google.example
Normal file
4
99-google.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Copy to 99-google and fill in your key, then: chmod 700 99-google
|
||||||
|
require_private "${BASH_SOURCE[0]}"
|
||||||
|
export GOOGLE_API_KEY=your-key-here
|
||||||
4
99-huggingface.example
Normal file
4
99-huggingface.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Copy to 99-huggingface and fill in your token, then: chmod 700 99-huggingface
|
||||||
|
require_private "${BASH_SOURCE[0]}"
|
||||||
|
export HF_TOKEN=your-token-here
|
||||||
4
99-replicate.example
Normal file
4
99-replicate.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Copy to 99-replicate and fill in your token, then: chmod 700 99-replicate
|
||||||
|
require_private "${BASH_SOURCE[0]}"
|
||||||
|
export REPLICATE_API_TOKEN=your-token-here
|
||||||
63
CLAUDE.md
Normal file
63
CLAUDE.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is a modular bash configuration directory (`~/.bash.d/`). Files here are sourced by `~/.bashrc` to set up the shell environment. Each file handles a specific concern (PATH additions, environment variables, shell completions). See `README.md` for a full description of the directory and its contents.
|
||||||
|
|
||||||
|
## File Naming Convention
|
||||||
|
|
||||||
|
Files use numeric prefixes to control load order:
|
||||||
|
- **00-** : Helper functions (loads first — used by other scripts)
|
||||||
|
- **10-** : PATH configuration (foundational)
|
||||||
|
- **20-** : Build tool settings (depends on paths)
|
||||||
|
- **30-** : Shell/prompt setup
|
||||||
|
- **50-** : Shell completions (named `50-<tool>-completion`)
|
||||||
|
- **99-** : Application config, credentials (loads last)
|
||||||
|
|
||||||
|
## Available Helpers (defined in `00-*`)
|
||||||
|
|
||||||
|
Use these in all scripts — do not manipulate `PATH` or check permissions manually:
|
||||||
|
- `path_append <dir>` — add directory to end of `$PATH` (checks existence, prevents duplicates)
|
||||||
|
- `path_prepend <dir>` — add directory to start of `$PATH`
|
||||||
|
- `require_private <file>` — warn if file is group/world-accessible; use in credential files
|
||||||
|
|
||||||
|
## Writing Scripts
|
||||||
|
|
||||||
|
- Every file must start with `# shellcheck shell=bash`
|
||||||
|
- Guard external tools with `command -v <tool> &>/dev/null` before using them
|
||||||
|
- Guard directory-dependent exports with `[[ -d <path> ]]` before exporting
|
||||||
|
- Do not suppress stderr from external scripts — only redirect stdout when needed
|
||||||
|
- Avoid `eval` when `. <(cmd)` (process substitution) works
|
||||||
|
- Prevent `LD_LIBRARY_PATH` duplicates with a `case` guard (see `20-oneapi` for example)
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
- Directory `~/.bash.d/` itself: mode `700`
|
||||||
|
- Regular scripts: mode `755` (executable is **required** for sourcing)
|
||||||
|
- Credential files (`99-*`): mode `700` and must call `require_private "${BASH_SOURCE[0]}"` as the first functional line
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Validate all shell scripts with shellcheck before committing:
|
||||||
|
```bash
|
||||||
|
shellcheck <filename>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Credential files are **excluded from git** via `.gitignore`. Each has a tracked `.example` template with placeholder values.
|
||||||
|
|
||||||
|
To set up a credential:
|
||||||
|
1. Copy the template: `cp 99-foo.example 99-foo`
|
||||||
|
2. Fill in your real secret
|
||||||
|
3. Restrict permissions: `chmod 700 99-foo`
|
||||||
|
|
||||||
|
When adding new credential files:
|
||||||
|
- Create a `.example` template (mode `644`) tracked in git
|
||||||
|
- Add the real filename to `.gitignore`
|
||||||
|
- Use mode `700` on the real file: `chmod 700 <file>`
|
||||||
|
- Call `require_private "${BASH_SOURCE[0]}"` as the first functional line
|
||||||
|
- **Never commit real secrets**
|
||||||
|
- Be aware that `export`ed tokens are visible to all child processes
|
||||||
114
README.md
Normal file
114
README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# ~/.bash.d — Modular Bash Configuration
|
||||||
|
|
||||||
|
This directory contains modular shell configuration files that are automatically sourced by `~/.bashrc` on every new shell session. Instead of maintaining one monolithic `.bashrc`, each concern lives in its own file — making the setup easier to understand, maintain, and extend.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The `~/.bashrc` iterates over all files in `~/.bash.d/` and sources each one that has the **executable bit** set. Non-executable files are silently skipped, which provides a simple way to temporarily disable a configuration (just `chmod -x` the file).
|
||||||
|
|
||||||
|
Files are sourced in **lexicographic order**, so numeric prefixes control the load sequence. This matters because later files may depend on functions or variables defined by earlier ones.
|
||||||
|
|
||||||
|
## File Naming Convention
|
||||||
|
|
||||||
|
| Prefix | Purpose | Example |
|
||||||
|
|--------|----------------------------------|---------------------------|
|
||||||
|
| `00-` | Helper functions (loaded first) | `00-path-helper` |
|
||||||
|
| `10-` | PATH configuration | `10-go-path`, `10-rust-path` |
|
||||||
|
| `20-` | Build tool settings | `20-ninja` |
|
||||||
|
| `30-` | Shell/prompt setup | `30-starship` |
|
||||||
|
| `50-` | Shell completions | `50-claude-completion` |
|
||||||
|
| `99-` | Credentials and app config (last)| `99-gemini`, `99-huggingface` |
|
||||||
|
|
||||||
|
Lower numbers load first, so foundational pieces like helper functions (`00-`) and PATH entries (`10-`) are available before anything that depends on them.
|
||||||
|
|
||||||
|
## Current Files
|
||||||
|
|
||||||
|
### Helpers (`00-`)
|
||||||
|
|
||||||
|
- **`00-path-helper`** — Defines `path_append` and `path_prepend` functions that safely add directories to `$PATH` (checking the directory exists and avoiding duplicates).
|
||||||
|
- **`00-credential-guard`** — Defines `require_private`, which warns at shell startup if a credential file has overly permissive permissions (anything beyond owner-only access).
|
||||||
|
|
||||||
|
### PATH (`10-`)
|
||||||
|
|
||||||
|
- **`10-bun-path`** — Adds Bun (JavaScript runtime) binaries to PATH.
|
||||||
|
- **`10-go-path`** — Sets `$GOPATH` and adds Go binaries to PATH.
|
||||||
|
- **`10-rust-path`** — Sets `$CARGO_HOME` and adds Cargo binaries to PATH.
|
||||||
|
|
||||||
|
### Build Tools (`20-`)
|
||||||
|
|
||||||
|
- **`20-ninja`** — Configures Ninja as the default CMake generator and enables ccache for C/C++ builds (only when installed).
|
||||||
|
- **`20-oneapi`** — Sources the Intel oneAPI environment and adds its libraries to `LD_LIBRARY_PATH`.
|
||||||
|
|
||||||
|
### Prompt (`30-`)
|
||||||
|
|
||||||
|
- **`30-starship`** — Initialises the [Starship](https://starship.rs) cross-shell prompt, if installed.
|
||||||
|
|
||||||
|
### Completions (`50-`)
|
||||||
|
|
||||||
|
- **`50-asdf-completion`** — Tab completion for the asdf version manager.
|
||||||
|
- **`50-claude-completion`** — Tab completion for [Claude Code](https://claude.com/claude-code) CLI.
|
||||||
|
- **`50-fj-completion`** — Tab completion for the Forgejo CLI (`fj`).
|
||||||
|
- **`50-tailscale-completion`** — Tab completion for Tailscale.
|
||||||
|
- **`50-uv-completion`** — Tab completion for [uv](https://github.com/astral-sh/uv) (Python package manager).
|
||||||
|
|
||||||
|
### Credentials & App Config (`99-`)
|
||||||
|
|
||||||
|
- **`99-android`** — Sets `$ANDROID_HOME`, `$JAVA_HOME`, and adds Android SDK tools to PATH (only when installed).
|
||||||
|
- **`99-claude.example`** — Template for Forgejo issue token for Claude Code integrations.
|
||||||
|
- **`99-gemini.example`** — Template for Gemini API key.
|
||||||
|
- **`99-google.example`** — Template for Google API key.
|
||||||
|
- **`99-huggingface.example`** — Template for HuggingFace token (`$HF_TOKEN`).
|
||||||
|
- **`99-replicate.example`** — Template for Replicate API token.
|
||||||
|
|
||||||
|
## Adding a New File
|
||||||
|
|
||||||
|
1. Create the file with the appropriate numeric prefix:
|
||||||
|
```bash
|
||||||
|
vim ~/.bash.d/50-mytool-completion
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the file with the shellcheck directive:
|
||||||
|
```bash
|
||||||
|
# shellcheck shell=bash
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Make it executable (required for it to be sourced):
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.bash.d/50-mytool-completion
|
||||||
|
```
|
||||||
|
|
||||||
|
4. For credential files, create a `.example` template and the real file:
|
||||||
|
```bash
|
||||||
|
# Create the template (tracked in git)
|
||||||
|
cat > ~/.bash.d/99-mytool.example << 'EOF'
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Copy to 99-mytool and fill in your token, then: chmod 700 99-mytool
|
||||||
|
require_private "${BASH_SOURCE[0]}"
|
||||||
|
export MY_SECRET_TOKEN=your-token-here
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create the real file from the template
|
||||||
|
cp ~/.bash.d/99-mytool.example ~/.bash.d/99-mytool
|
||||||
|
# Edit 99-mytool and fill in your real secret
|
||||||
|
chmod 700 ~/.bash.d/99-mytool
|
||||||
|
```
|
||||||
|
Then add `99-mytool` to `.gitignore`.
|
||||||
|
|
||||||
|
5. Validate with shellcheck:
|
||||||
|
```bash
|
||||||
|
shellcheck ~/.bash.d/50-mytool-completion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Credential files (`99-*`) are **excluded from git** via `.gitignore` and are never committed. Each credential has a tracked `.example` template with placeholder values. To set up credentials:
|
||||||
|
|
||||||
|
1. Copy the template: `cp 99-foo.example 99-foo`
|
||||||
|
2. Fill in your real secret
|
||||||
|
3. Restrict permissions: `chmod 700 99-foo`
|
||||||
|
|
||||||
|
Additional protections:
|
||||||
|
|
||||||
|
- **File permissions** — Credential files use mode `700` (owner-only).
|
||||||
|
- **Runtime checks** — The `require_private` helper warns on shell startup if a credential file has been accidentally loosened.
|
||||||
|
- **Never commit real secrets** — Only `.example` templates are tracked in git.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue