Skip to content

Git Config Guide

Interactive cross-platform Git configuration checker and setup wizard

Metadata

Code

python
#!/usr/bin/env python3
"""
@title Git Config Guide
@description Interactive cross-platform Git configuration checker and setup wizard
@version 1.0.0
@see https://scripts.aceapp.dev

Checks and interactively updates global Git configuration across macOS, Linux,
and Windows (Git Bash / WSL). Covers identity, credential helpers, line endings,
encoding, performance, and useful aliases. Also prints ready-to-use project-level
.gitattributes content on demand.

@example
    python git-config-guide.py
    python git-config-guide.py --no-color
"""

import subprocess
import sys
import os
import platform
import shutil
import argparse

# ─────────────────────────────────────────────────────────────
# ANSI colors (auto-disabled on Windows cmd or when --no-color)
# ─────────────────────────────────────────────────────────────

def _supports_color(no_color: bool) -> bool:
    if no_color:
        return False
    if platform.system() == "Windows" and "WT_SESSION" not in os.environ:
        return False
    return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


class C:
    """Terminal color helpers – patched to no-ops when color is off."""
    RESET = BOLD = DIM = GREEN = YELLOW = RED = CYAN = MAGENTA = BLUE = ""

    @classmethod
    def enable(cls):
        cls.RESET   = "\033[0m"
        cls.BOLD    = "\033[1m"
        cls.DIM     = "\033[2m"
        cls.GREEN   = "\033[32m"
        cls.YELLOW  = "\033[33m"
        cls.RED     = "\033[31m"
        cls.CYAN    = "\033[36m"
        cls.MAGENTA = "\033[35m"
        cls.BLUE    = "\033[34m"

    @classmethod
    def ok(cls, s):   return f"{cls.GREEN}{s}{cls.RESET}"
    @classmethod
    def warn(cls, s): return f"{cls.YELLOW}{s}{cls.RESET}"
    @classmethod
    def err(cls, s):  return f"{cls.RED}{s}{cls.RESET}"
    @classmethod
    def hi(cls, s):   return f"{cls.CYAN}{s}{cls.RESET}"
    @classmethod
    def b(cls, s):    return f"{cls.BOLD}{s}{cls.RESET}"
    @classmethod
    def dim(cls, s):  return f"{cls.DIM}{s}{cls.RESET}"


# ─────────────────────────────────────────────────────────────
# Git helpers
# ─────────────────────────────────────────────────────────────

def git_get(key: str) -> str:
    """Return current global git config value, or '' if not set."""
    try:
        result = subprocess.run(
            ["git", "config", "--global", key],
            capture_output=True, text=True
        )
        return result.stdout.strip()
    except FileNotFoundError:
        return ""


def git_set(key: str, value: str) -> bool:
    """Set a global git config key. Returns True on success."""
    result = subprocess.run(
        ["git", "config", "--global", key, value],
        capture_output=True, text=True
    )
    return result.returncode == 0


def git_available() -> bool:
    return shutil.which("git") is not None


# ─────────────────────────────────────────────────────────────
# UI helpers
# ─────────────────────────────────────────────────────────────

def section(title: str):
    width = 60
    print()
    print(C.b(C.BLUE + "─" * width + C.RESET))
    print(C.b(f"  {title}"))
    print(C.b(C.BLUE + "─" * width + C.RESET))


def prompt_update(key: str, current: str, recommended: str, hint: str = "") -> bool:
    """
    Show current value, suggest recommended, ask user whether to update.
    Returns True if a change was made.
    """
    label   = C.hi(key)
    cur_str = C.ok(current) if current else C.warn("(not set)")
    rec_str = C.b(recommended)

    print(f"\n  {label}")
    if hint:
        print(f"  {C.dim(hint)}")
    print(f"  Current   : {cur_str}")
    print(f"  Recommended: {rec_str}")

    if current == recommended:
        print(f"  {C.ok('✓ Already correct, skipping.')}")
        return False

    try:
        answer = input(f"  Set to {C.b(recommended)}? [Enter=skip / y=yes / custom value]: ").strip()
    except (EOFError, KeyboardInterrupt):
        print()
        return False

    if answer.lower() == "y":
        value = recommended
    elif answer == "":
        print(f"  {C.dim('Skipped.')}")
        return False
    else:
        value = answer  # user typed a custom value

    if git_set(key, value):
        print(f"  {C.ok(f'✓ Set: {key} = {value}')}")
        return True
    else:
        print(f"  {C.err('✗ Failed to set value.')}")
        return False


def show_readonly(key: str, current: str, note: str = ""):
    """Just display a key's value without prompting."""
    cur_str = C.ok(current) if current else C.warn("(not set)")
    print(f"\n  {C.hi(key)}: {cur_str}")
    if note:
        print(f"  {C.dim(note)}")


# ─────────────────────────────────────────────────────────────
# Platform detection
# ─────────────────────────────────────────────────────────────

def detect_platform() -> str:
    """Returns 'macos', 'linux', 'wsl', or 'windows'."""
    system = platform.system()
    if system == "Darwin":
        return "macos"
    if system == "Windows":
        return "windows"
    if system == "Linux":
        # Check for WSL
        try:
            with open("/proc/version", "r") as f:
                if "microsoft" in f.read().lower():
                    return "wsl"
        except OSError:
            pass
        return "linux"
    return "linux"


def recommended_autocrlf(plat: str) -> str:
    return "true" if plat == "windows" else "input"


def recommended_credential_helper(plat: str) -> str:
    if plat == "macos":
        return "osxkeychain"
    if plat == "windows":
        return "manager"
    # linux / wsl
    if shutil.which("git-credential-libsecret"):
        return "/usr/lib/git-core/git-credential-libsecret"
    return "cache --timeout=3600"


# ─────────────────────────────────────────────────────────────
# Check sections
# ─────────────────────────────────────────────────────────────

def check_identity():
    section("① Identity")
    for key in ("user.name", "user.email"):
        current = git_get(key)
        label   = C.hi(key)
        cur_str = C.ok(current) if current else C.err("(not set – REQUIRED)")
        print(f"\n  {label}: {cur_str}")
        if not current:
            try:
                val = input(f"  Enter value for {key}: ").strip()
            except (EOFError, KeyboardInterrupt):
                print()
                continue
            if val:
                if git_set(key, val):
                    print(f"  {C.ok(f'✓ Set: {key} = {val}')}")
                else:
                    print(f"  {C.err('✗ Failed.')}")


def check_credential(plat: str):
    section("② Credential Helper")
    key     = "credential.helper"
    current = git_get(key)
    rec     = recommended_credential_helper(plat)

    hints = {
        "macos":   "macOS Keychain – secure system store, no plaintext.",
        "windows": "Git Credential Manager – comes bundled with Git for Windows.",
        "wsl":     "libsecret links into GNOME Keyring; fallback is memory cache.",
        "linux":   "memory cache (15 min–1 h) is safer than plaintext store.",
    }
    prompt_update(key, current, rec, hints.get(plat, ""))

    if plat in ("linux", "wsl") and current == "store":
        print(f"  {C.warn('⚠ store saves credentials in plaintext at ~/.git-credentials')}")
        print(f"  {C.dim('  Fine if your screen locks quickly (macOS-style). Keep it if intentional.')}")


def check_line_endings(plat: str):
    section("③ Line Endings (core.autocrlf)")
    hints = (
        "input  → commit LF as-is, never convert on checkout  (Linux/macOS/WSL)\n"
        "  true   → commit converts CRLF→LF, checkout converts LF→CRLF          (Windows)\n"
        "  false  → no conversion at all (not recommended for cross-platform work)"
    )
    prompt_update(
        "core.autocrlf",
        git_get("core.autocrlf"),
        recommended_autocrlf(plat),
        hints,
    )


def check_filemode(plat: str):
    section("④ File Permission Tracking (core.fileMode)")
    current = git_get("core.fileMode")
    if plat == "wsl":
        prompt_update(
            "core.fileMode", current, "false",
            "WSL mounts Windows drives with inconsistent permissions – set false globally."
        )
    else:
        show_readonly(
            "core.fileMode", current,
            "true = track +x changes (default, fine on native Linux/macOS)."
        )


def check_encoding():
    section("⑤ Encoding & Chinese Filename Support")

    items = [
        ("i18n.commitEncoding", "utf-8",  "Encoding for commit messages."),
        ("i18n.logOutputEncoding", "utf-8", "Encoding when displaying log output."),
        ("core.quotePath", "false",        "Show non-ASCII filenames (e.g. Chinese) unescaped in status/log."),
    ]
    for key, rec, hint in items:
        prompt_update(key, git_get(key), rec, hint)


def check_misc():
    section("⑥ Quality-of-Life Settings")

    items = [
        ("init.defaultBranch",  "main",      "Default branch name for new repos."),
        ("pull.rebase",         "false",      "false=merge, true=rebase on pull. Pick your team's convention."),
        ("push.default",        "current",    "Push only the current branch by default."),
        ("diff.algorithm",      "histogram",  "More accurate diff output than the default 'myers'."),
        ("rerere.enabled",      "true",       "Remember conflict resolutions and replay them automatically."),
        ("core.longPaths",      "true",       "Needed on Windows for deep directory trees."),
        ("color.ui",            "auto",       "Colorized output when printing to a terminal."),
    ]
    for key, rec, hint in items:
        prompt_update(key, git_get(key), rec, hint)


def check_editor():
    section("⑦ Default Editor")
    current = git_get("core.editor")
    choices = {
        "1": ("code --wait",  "VS Code"),
        "2": ("vim",          "Vim"),
        "3": ("nano",         "Nano"),
        "4": ("hx",           "Helix"),
    }
    cur_str = C.ok(current) if current else C.warn("(not set – git uses $EDITOR or vi)")
    print(f"\n  {C.hi('core.editor')}: {cur_str}")
    print(f"  Options: " + "  ".join(f"{k}={C.b(v[1])}" for k, v in choices.items()))
    try:
        ans = input("  Choose [1-4], type custom, or Enter to skip: ").strip()
    except (EOFError, KeyboardInterrupt):
        print()
        return
    if not ans:
        return
    value = choices[ans][0] if ans in choices else ans
    if git_set("core.editor", value):
        print(f"  {C.ok(f'✓ Set core.editor = {value}')}")


def check_aliases():
    section("⑧ Useful Aliases")
    aliases = [
        ("alias.st",      "status -sb",                        "Short, branch-aware status."),
        ("alias.lg",      "log --oneline --graph --decorate",   "Pretty one-line log graph."),
        ("alias.unstage", "reset HEAD --",                      "Unstage a file easily."),
        ("alias.last",    "log -1 HEAD",                        "Show last commit."),
        ("alias.aliases", "config --get-regexp alias",          "List all aliases."),
    ]
    for key, rec, hint in aliases:
        prompt_update(key, git_get(key), rec, hint)


# ─────────────────────────────────────────────────────────────
# Project-level output
# ─────────────────────────────────────────────────────────────

GITATTRIBUTES = """\
# ──────────────────────────────────────────
# .gitattributes – project-level line ending
# and binary file policy.
# Place this file in the repo root.
# ──────────────────────────────────────────

# Default: let Git decide (text=auto normalises to LF in repo)
* text=auto

# Explicit text files – always store as LF
*.js      text eol=lf
*.ts      text eol=lf
*.jsx     text eol=lf
*.tsx     text eol=lf
*.mjs     text eol=lf
*.cjs     text eol=lf
*.json    text eol=lf
*.jsonc   text eol=lf
*.md      text eol=lf
*.mdx     text eol=lf
*.html    text eol=lf
*.css     text eol=lf
*.scss    text eol=lf
*.less    text eol=lf
*.yaml    text eol=lf
*.yml     text eol=lf
*.toml    text eol=lf
*.ini     text eol=lf
*.cfg     text eol=lf
*.env     text eol=lf
*.sh      text eol=lf
*.bash    text eol=lf
*.zsh     text eol=lf
*.py      text eol=lf
*.rb      text eol=lf
*.go      text eol=lf
*.rs      text eol=lf
*.cs      text eol=lf
*.sql     text eol=lf
*.xml     text eol=lf
*.svg     text eol=lf
*.txt     text eol=lf
*.lock    text eol=lf -diff
Makefile  text eol=lf

# Windows-only scripts – keep CRLF on checkout
*.bat     text eol=crlf
*.cmd     text eol=crlf
*.ps1     text eol=crlf

# Binary – no conversion, no diff
*.png     binary
*.jpg     binary
*.jpeg    binary
*.gif     binary
*.webp    binary
*.ico     binary
*.pdf     binary
*.zip     binary
*.gz      binary
*.tar     binary
*.7z      binary
*.exe     binary
*.dll     binary
*.so      binary
*.dylib   binary
*.wasm    binary
*.ttf     binary
*.otf     binary
*.woff    binary
*.woff2   binary
*.eot     binary
*.mp4     binary
*.mp3     binary
*.wav     binary
*.ogg     binary
"""

GITIGNORE_ADDITIONS = """\
# ── OS ──────────────────────────────────
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Thumbs.db
ehthumbs.db
Desktop.ini

# ── Editor ──────────────────────────────
.vscode/settings.json
.idea/
*.suo
*.user
*.swp
*~

# ── Secrets ─────────────────────────────
.env
.env.*
!.env.example
"""


def print_project_templates():
    section("⑨ Project-Level Templates")
    print(f"\n  {C.b('.gitattributes')}  {C.dim('(copy to your repo root)')}")
    print()
    for line in GITATTRIBUTES.splitlines():
        if line.startswith("#"):
            print("  " + C.dim(line))
        else:
            print("  " + line)

    print(f"\n  {C.b('Recommended additions for .gitignore')}")
    print()
    for line in GITIGNORE_ADDITIONS.splitlines():
        if line.startswith("#"):
            print("  " + C.dim(line))
        else:
            print("  " + line)


# ─────────────────────────────────────────────────────────────
# Summary
# ─────────────────────────────────────────────────────────────

def print_summary():
    section("✅ Current Global Config")
    keys = [
        "user.name", "user.email",
        "credential.helper",
        "core.autocrlf", "core.fileMode", "core.quotePath", "core.editor", "core.longPaths",
        "i18n.commitEncoding", "i18n.logOutputEncoding",
        "init.defaultBranch", "pull.rebase", "push.default",
        "diff.algorithm", "rerere.enabled", "color.ui",
        "alias.st", "alias.lg", "alias.unstage", "alias.last",
    ]
    for key in keys:
        val = git_get(key)
        val_str = C.ok(val) if val else C.dim("(not set)")
        print(f"  {C.hi(key):<45} {val_str}")


# ─────────────────────────────────────────────────────────────
# Entry point
# ─────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="Interactive cross-platform Git configuration guide."
    )
    parser.add_argument("--no-color",    action="store_true", help="Disable colored output.")
    parser.add_argument("--summary-only",action="store_true", help="Just print current config, no prompts.")
    parser.add_argument("--templates",   action="store_true", help="Print project-level templates and exit.")
    args = parser.parse_args()

    if _supports_color(args.no_color):
        C.enable()

    if not git_available():
        print(C.err("✗ git not found in PATH. Please install Git first."))
        sys.exit(1)

    plat = detect_platform()
    plat_label = {"macos": "macOS", "linux": "Linux", "wsl": "WSL (Linux)", "windows": "Windows"}.get(plat, plat)

    print()
    print(C.b(C.MAGENTA + "╔══════════════════════════════════════════════╗" + C.RESET))
    print(C.b(C.MAGENTA + "║         git-config-guide  v1.0.0             ║" + C.RESET))
    print(C.b(C.MAGENTA + "╚══════════════════════════════════════════════╝" + C.RESET))
    print(f"\n  Platform detected: {C.b(plat_label)}")
    print(f"  Git version      : {C.dim(subprocess.getoutput('git --version'))}")
    print(f"\n  {C.dim('Press Enter to skip any item. Type a custom value to override the recommendation.')}")

    if args.templates:
        print_project_templates()
        return

    if args.summary_only:
        print_summary()
        return

    check_identity()
    check_credential(plat)
    check_line_endings(plat)
    check_filemode(plat)
    check_encoding()
    check_misc()
    check_editor()
    check_aliases()
    print_project_templates()
    print_summary()

    print()
    print(C.ok("  Done. Run `git config --global --list` to verify all settings."))
    print()


if __name__ == "__main__":
    main()

File Information

  • Filename: git-config-guide.py
  • Category: python
  • Language: PYTHON

View on GitHub