Post

Dotfiles Management with Bare Git Repository Bootstrap

Dotfiles Management with Bare Git Repository Bootstrap

Configuration files scattered across a home directory present a significant challenge for system administrators and developers. Tracking changes to shell configurations, editor settings, and application preferences requires a systematic approach. This post presents a bootstrap script that establishes a bare git repository workflow for managing dotfiles, including integration with Fish shell and Neovim.

Problem Statement

The Dotfiles Challenge

Unix-like systems store user configuration in hidden files (dotfiles) throughout the home directory. These files accumulate over years of customization:

  • Shell configurations (.bashrc, .zshrc, .config/fish/)
  • Editor settings (.vimrc, .config/nvim/)
  • Tool configurations (.gitconfig, .tmux.conf)
  • Application preferences (.config/ subdirectories)

Manual backup of these files leads to several problems:

  1. Version History Loss: Manual copying provides no change history
  2. Synchronization Difficulty: Keeping multiple machines consistent requires manual effort
  3. Restoration Complexity: Setting up a new system involves remembering which files to copy
  4. Conflict Resolution: Changes made on different machines may conflict without visibility

The Git Problem

Standard git repositories store their .git directory alongside tracked files. For dotfiles, this creates a conflict: the home directory cannot become a standard git repository without interfering with other projects and cluttering git status output.

Technical Background

Bare Repository Architecture

A bare git repository contains only the git database (objects/, refs/, HEAD, etc.) without a working tree. This architecture enables separation of the repository location from the working directory.

Standard repository structure:

1
2
3
4
~/project/
├── .git/           # Repository data
├── src/            # Working tree
└── README.md       # Working tree

Bare repository structure for dotfiles:

1
2
3
4
5
~/.dotfiles/        # Repository data (bare)
~/                  # Working tree (separate)
├── .bashrc
├── .config/
└── .gitconfig

Advantages of Bare Repository Approach

  1. No Repository Conflicts: The .git directory does not exist in $HOME
  2. Native Git Operations: Standard git commands work with a wrapper alias
  3. Selective Tracking: Only explicitly added files are tracked
  4. Clean Status Output: Untracked files can be hidden from status

Bootstrap Implementation

Script Architecture

The bootstrap script follows a modular design with separate functions for each operation:

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

REPO_URL="git@github.com:username/dotfiles.git"
GIT_DIR="$HOME/.dotfiles"
WORK_TREE="$HOME"
BACKUP_DIR="$HOME/.dotfiles-backup"

dotfiles() {
  git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" "$@"
}

The dotfiles function wraps git commands, specifying:

  • --git-dir: Location of the bare repository
  • --work-tree: Location of the working tree (home directory)

Clone Operation

1
2
3
4
clone_repo() {
  echo ">> Cloning bare repo into $GIT_DIR"
  git clone --bare "$REPO_URL" "$GIT_DIR"
}

The --bare flag instructs git to clone only the repository data, without checking out a working tree. The repository resides entirely within ~/.dotfiles/.

Checkout with Conflict Resolution

Checkout attempts may fail when existing files conflict with repository contents. The script handles this automatically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
checkout_dotfiles() {
  echo ">> Checking out dotfiles into $WORK_TREE"
  if ! dotfiles checkout; then
    echo "!! Conflicts found. Moving existing files to $BACKUP_DIR"
    mkdir -p "$BACKUP_DIR"
    dotfiles checkout 2>&1 | grep -E "^\s+\." | awk '{print $1}' | while read -r file; do
      mkdir -p "$(dirname "$BACKUP_DIR/$file")"
      mv "$WORK_TREE/$file" "$BACKUP_DIR/$file"
    done
    echo ">> Retrying checkout"
    dotfiles checkout
  fi
  echo ">> Dotfiles successfully checked out."
}

The conflict resolution process:

flowchart TD
    A["Initial Checkout Attempt"] --> B{Conflicts Detected?}
    B -->|No| C["Checkout Complete"]
    B -->|Yes| D["Parse Error Output<br/>for Conflicting Files"]
    D --> E["Create Backup Directory<br/>~/.dotfiles-backup/"]
    E --> F["Move Conflicting Files<br/>to Backup Location"]
    F --> G["Retry Checkout"]
    G --> C
    C --> H["Preserve Existing<br/>Configurations"]

This approach preserves existing configurations rather than overwriting them, enabling manual review and selective restoration.

Untracked File Configuration

After initial setup, the bare repository configuration should hide untracked files:

1
dotfiles config --local status.showUntrackedFiles no

This setting prevents dotfiles status from listing every file in the home directory. Only tracked files and their modifications appear in status output.

Fish Shell Integration

The Alias Function Pattern

Fish shell uses functions rather than aliases. The dotfiles command requires a Fish function:

function dotfiles
    /usr/bin/git --git-dir=$HOME/.dotfiles --work-tree=$HOME $argv
end

Key implementation details:

  • Absolute Path: /usr/bin/git ensures the system git binary is used, avoiding conflicts with git wrapper scripts
  • Variable Expansion: $HOME resolves to the user’s home directory
  • Argument Passing: $argv passes all function arguments to git

This function should reside in ~/.config/fish/config.fish or as a separate file in ~/.config/fish/functions/dotfiles.fish.

Fish Bootstrap Integration

The bootstrap script includes Fish plugin and completion setup:

1
2
3
4
5
6
7
8
9
10
11
12
13
run_fish_bootstrap() {
  if command -v fish >/dev/null 2>&1; then
    if [ -f "$FISH_BOOTSTRAP" ]; then
      echo ">> Running Fish bootstrap: $FISH_BOOTSTRAP"
      fish "$FISH_BOOTSTRAP"
      fish_update_completions
    else
      echo "!! Fish bootstrap file not found: $FISH_BOOTSTRAP"
    fi
  else
    echo "!! Fish is not installed. Skipping Fish bootstrap."
  fi
}

The Fish bootstrap file (~/.config/fish/bootstrap.fish) typically:

  • Installs plugin managers (Fisher, Oh My Fish)
  • Installs plugins for enhanced functionality
  • Configures shell completions

Neovim Bootstrap Integration

Headless Plugin Installation

Neovim requires plugin installation after configuration files are in place. The bootstrap script runs Neovim in headless mode:

1
2
3
4
5
6
7
8
9
10
11
run_nvim_setup() {
  if command -v nvim >/dev/null 2>&1; then
    echo ">> Running Neovim Mason, Treesitter, and Lazy setup..."
    nvim --headless "+MasonInstallAll" +qall 2>/dev/null
    nvim --headless "+TSUpdate" +qall 2>/dev/null
    nvim --headless "+Lazy! sync" +qall 2>/dev/null
    echo ">> Neovim bootstrapping complete."
  else
    echo "!! Neovim is not installed. Skipping Neovim setup."
  fi
}

The sequence installs:

  1. Mason: LSP servers, linters, and formatters
  2. Treesitter: Language parsers for syntax highlighting
  3. Lazy.nvim: Plugin manager synchronization

Headless mode (--headless) enables automated execution without a terminal interface.

Per-Machine Branching Strategy

Branch-Per-Machine Architecture

Different machines require different configurations. A branching strategy addresses this:

gitGraph
    commit id: "Initial Commit"
    commit id: "Common Config"
    branch machine-a
    branch machine-b
    branch machine-c
    branch machine-d

Each branch contains machine-specific configurations:

  • Different shell aliases for different roles
  • Machine-specific paths and environment variables
  • Hardware-specific settings (display scaling, power management)
  • Tool availability differences (work vs. personal machines)

Branch Management

On initial setup, checkout the appropriate branch:

1
dotfiles checkout machine-name

For new machines, create a branch from an existing configuration:

1
2
dotfiles checkout -b new-machine
dotfiles push -u origin new-machine

Sharing Common Configuration

Common configurations can be maintained in a shared branch and merged into machine branches:

1
2
3
dotfiles checkout machine-a
dotfiles merge common
dotfiles push

This approach enables both shared defaults and machine-specific customization.

Complete Bootstrap Sequence

Full Installation Process

The script supports a full installation mode:

1
2
3
4
5
6
run_full_install() {
  clone_repo
  checkout_dotfiles
  run_fish_bootstrap
  run_nvim_setup
}

Execution flow:

sequenceDiagram
    participant User
    participant Script as bootstrap.sh
    participant Git
    participant Fish
    participant Neovim

    User->>Script: ./bootstrap.sh install
    Script->>Git: Clone bare repository
    Git-->>Script: Repository cloned
    Script->>Git: Checkout dotfiles
    Git-->>Script: Dotfiles checked out
    Script->>Fish: Run Fish bootstrap
    Fish-->>Script: Plugins installed
    Script->>Neovim: Run Neovim setup
    Note over Neovim: Mason, Treesitter, Lazy
    Neovim-->>Script: Setup complete
    Script-->>User: Installation finished

Modular Operations

Individual components can be executed separately for partial setup or reinstallation:

1
2
./bootstrap.sh --run-fish-bootstrap   # Fish plugins only
./bootstrap.sh --run-nvim-setup       # Neovim plugins only

This modularity supports scenarios where only specific components require re-initialization.

Usage After Bootstrap

Daily Operations

Common dotfiles operations after initial setup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Check status
dotfiles status

# View changes
dotfiles diff

# Stage and commit
dotfiles add .config/fish/config.fish
dotfiles commit -m "add fish configuration"

# Push to remote
dotfiles push

# Pull changes from another machine
dotfiles pull

Adding New Files

To track a new configuration file:

1
2
3
dotfiles add ~/.config/newapp/config.yaml
dotfiles commit -m "track newapp configuration"
dotfiles push

Restoration on New System

On a fresh system installation:

  1. Install git
  2. Run the bootstrap script
  3. Install Fish shell and Neovim (if not present)
  4. Re-run component bootstraps if needed

Summary

The bare git repository approach provides a robust solution for dotfiles management:

  • Clean Separation: Repository data stored separately from working files
  • Standard Git Workflow: Familiar commands with a simple wrapper
  • Conflict Handling: Automatic backup of existing files during initial setup
  • Multi-Machine Support: Branch-per-machine strategy enables customization
  • Integrated Tooling: Fish shell and Neovim bootstrap in a single script

The bootstrap script transforms system configuration from manual file copying to a reproducible, version-controlled workflow. New machine setup reduces from hours of manual configuration to a single script execution.

This post is licensed under CC BY 4.0 by the author.