Git Setup and Configuration

Quote

“Git proved I could be more than a one-hit wonder.”

Linus Torvalds, TED interview (2016)

Mental Model — How Git’s Layers Connect

Before running any commands, understand how the pieces fit together. The diagram below shows the relationship between the files you edit, the local Git machinery, the remote server, and the configuration and hook layers that govern behavior.


flowchart LR
  subgraph LOCAL["Local Machine"]
    LPAD[" "]
    WT["Working Tree<br/>files you edit"]
    IDX["Staging Area / Index<br/>git add"]
    OBJ[".git/ Object Store<br/>commits, trees, blobs"]
    CFG["Config Files<br/>system → global → local"]
    HK["Hooks Layer<br/>.git/hooks/"]
    LPAD ~~~ WT
  end
  subgraph REMOTE["Remote (GitHub)"]
    RPAD[" "]
    RR["Remote Repository<br/>origin"]
    AUTH["Auth Layer<br/>HTTPS PAT / SSH key"]
    RPAD ~~~ RR
    RPAD ~~~ AUTH
  end
  WT -->|"git add"| IDX
  IDX -->|"git commit"| OBJ
  OBJ -->|"git push"| RR
  RR -->|"git pull / fetch"| OBJ
  style LPAD fill:transparent,stroke:transparent,color:transparent
  style RPAD fill:transparent,stroke:transparent,color:transparent
  AUTH -.->|"authenticates"| RR
  HK -.->|"runs before commit/push"| IDX
  CFG -.->|"configures behavior"| OBJ

Reading the diagram:

  • Working tree — the files on disk you edit with your IDE or text editor.
  • Staging area (index)git add moves changes here. Only staged changes become part of the next commit.
  • .git/ object storegit commit creates an immutable snapshot and stores it here. This is the local history.
  • Config files — three scopes (system, global, local) control Git’s behavior. The most specific scope wins.
  • Hooks layer — scripts in .git/hooks/ run automatically at lifecycle events (before commit, before push, etc.).
  • Remote repository — the shared copy on GitHub. git push sends commits up; git pull / git fetch brings them down.
  • Auth layer — HTTPS with PAT or SSH key authenticates your identity to the remote.

Installing Git

Git must be installed before any other step. The installation method depends on your operating system.

Windows | Git | install on Windows

Install Git for Windows

On a new Windows machine or after a fresh OS install. It is typically triggered by git --version returns “command not found” or the version is below 2.39. Requires administrator privileges for the default installer. No restart needed. Install the Git CLI, Git Bash shell, and optional GUI tools.

Download the installer from git-scm.com/downloads/win and run it. Alternatively, use winget from a PowerShell terminal:

Install Git via winget (Windows Package Manager):

winget install --id Git.Git -e --source winget

Installer options to pay attention to

  • Default editor: choose VS Code or your preferred editor (the default is Vim).
  • PATH environment: select “Git from the command line and also from 3rd-party software” to make git available in PowerShell, CMD, and Git Bash.
  • Line ending conversions: the installer defaults to core.autocrlf=true (convert LF→CRLF on checkout, CRLF→LF on commit). This is correct for most Windows users on mixed-OS teams.
  • Credential helper: select “Git Credential Manager” (the default since Git 2.39+).

macOS | Git | install on macOS

Install Git on macOS

On a new Mac or after a major OS upgrade. It is typically triggered by git --version returns the Apple-bundled version (often outdated) or “command not found.”. No admin required for Homebrew install. Xcode Command Line Tools also provide a Git binary. Install a current Git version with full feature support.

macOS ships a Git binary as part of Xcode Command Line Tools, but it is often outdated. Install a current version via Homebrew:

Install Git via Homebrew:

brew install git

After installation, verify the Homebrew version is first on $PATH:

Verify the active Git binary location:

which git
/opt/homebrew/bin/git

If the output shows /usr/bin/git, the Xcode version is still taking precedence. Add Homebrew to your $PATH in ~/.zshrc.

Linux | Git | install on Linux

Install Git on Linux

On a new Linux machine, container, or VM. It is typically triggered by git --version returns “command not found.”. Requires sudo for package manager installation. Install the Git CLI.

Install Git on Debian/Ubuntu:

sudo apt-get update && sudo apt-get install -y git

Install Git on Fedora/RHEL:

sudo dnf install -y git

Install Git on Alpine (common in Docker images):

apk add --no-cache git

Windows | WSL | Git in WSL

Install Git in WSL

When using Windows Subsystem for Linux for development. It is typically triggered by git --version inside the WSL distribution returns “command not found” or an outdated version. WSL has its own filesystem and its own Git installation, separate from Git for Windows. Credentials, config, and hooks are independent. Install Git inside the Linux distribution running under WSL.

WSL distributions are standard Linux — use the apt or dnf commands above. Note that Git for Windows and WSL Git are separate installations with separate configurations. Setting user.email in Git for Windows does not affect WSL, and vice versa.

WSL and Windows Git are independent

  • ~/.gitconfig inside WSL is a different file from C:\Users\<you>\.gitconfig on the Windows side.
  • SSH keys in ~/.ssh/ inside WSL are not visible to Git for Windows, and vice versa.
  • Credential helpers configured in Git for Windows do not apply inside WSL.
  • If you work in both environments, configure both independently.

Share credentials between Windows and WSL

Inside WSL, configure Git to delegate credential storage to the Windows Credential Manager: git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"

Git | verify installation

Verify Git installation

After installing, verify the version from a terminal:

Check the installed Git version:

git --version
git version 2.53.0.windows.1

If the command is not found, the installation did not add Git to your PATH. On Windows, restart your terminal or run the installer again and ensure the PATH option is selected.


Identity Configuration

Git embeds an author name and email in every commit object. These values are mandatory — Git refuses to create a commit without them. The email address also determines whether GitHub attributes the commit to your profile on the contribution graph.

Git | config | set user identity

Set the global author name

Once on each new machine, before the first commit. It is typically triggered by first-time Git setup or git config --get user.name returns empty. --global writes to ~/.gitconfig. Applies to all repos for the current user. Does not require admin. Set the display name that appears in git log output and GitHub commit attribution.

Set the author name for all repositories on this machine:

git config --global user.name "alp78"

Set the global author email

Immediately after setting user.name. It is typically triggered by git config --get user.email returns empty or the wrong address. The email must match a verified email on your GitHub account for commits to be attributed to your profile. Set the email that appears in every commit and links your work to your GitHub identity.

Set the author email for all repositories:

git config --global user.email "alexper.recovery@gmail.com"

Email must match your GitHub account

If user.email does not match a verified email on your GitHub account, commits will appear as “unrecognized” — they will not count toward your contribution graph and will not link to your profile avatar.

Find your verified email on GitHub

Go to GitHub → Settings → Emails to see your verified addresses. Use that exact value. If you prefer to keep your email private, use the GitHub noreply address: <id>+<username>@users.noreply.github.com (visible on the same settings page).

Use the GitHub noreply email for privacy

When you want commits attributed to your GitHub profile without exposing your real email. It is typically triggered by privacy policy, personal preference, or corporate guidance. GitHub generates a unique noreply address for every account. Find it at GitHub → Settings → Emails → “Keep my email addresses private.”. Prevent your real email from appearing in public commit history while maintaining contribution attribution.

Set the noreply email as your global author email:

git config --global user.email "12345678+alp78@users.noreply.github.com"

Enable the email privacy setting on GitHub

On GitHub → Settings → Emails, check “Keep my email addresses private” and “Block command line pushes that expose my email.” The second option rejects pushes that use a non-noreply email, preventing accidental exposure.

Use a different identity for a specific repository

When you contribute to a repository that requires a different email (e.g., work vs. personal). It is typically triggered by the repo belongs to a different organization or requires a different identity. --local writes to .git/config inside the repository. Overrides --global for this repo only. Ensure commits in this repo use the correct identity without changing the global default.

Set a repo-specific email (local scope):

git config --local user.email "alex@stockindex.example.com"

Verify the effective email in this repo:

git config --get user.email
alex@stockindex.example.com

Verify both scopes are visible:

git config --list --show-scope | grep user.email
global	user.email=alexper.recovery@gmail.com
local	user.email=alex@stockindex.example.com

The local value takes precedence. Remove it to revert to the global default:

Remove the local override:

git config --local --unset user.email

Conditional includes for automatic identity switching

Instead of setting --local in every repo, use includeIf in ~/.gitconfig to automatically apply a different identity based on the repo’s filesystem path:

# ~/.gitconfig
[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work
 
# ~/.gitconfig-work
[user]
    email = alex@stockindex.example.com
    name = Alex Perrier

Any repository cloned under ~/work/ automatically uses the work identity. No per-repo --local configuration needed.


Authentication

Git communicates with remote repositories (GitHub) over HTTPS or SSH. GitHub no longer accepts account passwords for Git operations — you must use a Personal Access Token (PAT) or SSH key.

Git | Authentication | HTTPS with PAT

HTTPS is the default protocol when you clone with a https://github.com/... URL. Authentication requires a Personal Access Token (PAT) instead of your GitHub password.

Generate a Personal Access Token on GitHub

Before the first git push or git clone over HTTPS, or when an existing token expires. It is typically triggered by git prompts for a password or returns Authentication failed. Browser-based operation on GitHub.com. Tokens are scoped and have configurable expiry. Create a credential that Git can use to authenticate with GitHub over HTTPS.

Generate a PAT (classic)

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic).
  2. Click Generate new token (classic).
  3. Set a descriptive note (e.g., laptop-2026).
  4. Set an expiration (90 days is a reasonable balance between security and convenience).
  5. Select scopes: at minimum repo (full repository access). Add workflow if you use GitHub Actions.
  6. Click Generate token and copy it immediately — GitHub will not show it again.

PATs are secrets

  • Never commit a PAT to a repository, paste it in Slack, or store it in an unencrypted file.
  • If a PAT is compromised, revoke it immediately on GitHub → Settings → Developer settings → Tokens.
  • Set the shortest practical expiry. 90-day tokens are standard in most organizations.

Store the PAT securely using a credential helper

Configure Git to cache credentials in the OS secure keychain (see the Credential Helpers section below). After the first successful git push, the PAT is stored and you will not be prompted again until it expires.

Validate HTTPS authentication

After configuring the PAT and credential helper, to confirm everything works. It is typically triggered by initial setup or after a PAT rotation. Requires a valid PAT and network access to github.com. Confirm that Git can authenticate with GitHub over HTTPS.

Test HTTPS authentication using the GitHub CLI:

gh auth status
github.com
  ✓ Logged in to github.com account alp78 (keyring)
  - Active account: true
  - Git operations protocol: https
  - Token: gho_************************************
  - Token scopes: 'delete_repo', 'gist', 'read:org', 'repo', 'workflow'

The gh auth status command confirms the active account, protocol, and token scopes. If using gh as the credential helper (recommended), this is the single source of truth for HTTPS authentication status.

Git | Authentication | SSH keys

SSH authentication uses a cryptographic key pair. The private key stays on your machine; the public key is uploaded to GitHub. Once configured, Git operations over SSH (git@github.com:... URLs) authenticate silently.

Install GitHub CLI

If you want GitHub CLI to handle GitHub authentication and upload the SSH key from the terminal, install gh first. This step is typically triggered by a new machine build or by a shell that already has git and ssh but not GitHub CLI. The PowerShell variant below uses Scoop because it keeps the setup entirely in the terminal; GitHub CLI’s official Windows recommendation is WinGet, so treat Scoop as a community-supported alternative.

Install gh from PowerShell with Scoop:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
scoop install gh

Install gh on Ubuntu or Debian Linux from the official GitHub CLI apt repository:

(type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& out=$(mktemp) && wget -nv -O"$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat "$out" | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& sudo mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& sudo apt update \
&& sudo apt install gh -y

Generate an SSH key pair

Once per machine, or when rotating keys. It is typically triggered by no SSH key exists yet, or ssh -T git@github.com returns “Permission denied.”. Runs locally. The private key is stored in ~/.ssh/ on Linux and $env:USERPROFILE\.ssh\ on PowerShell. Requires no network access. Create a cryptographic identity for SSH authentication.

Generate an Ed25519 SSH key pair from PowerShell:

ssh-keygen -t ed25519 -C "YOUR_EMAIL@DOMAIN.COM"

Generate an Ed25519 SSH key pair on Linux:

ssh-keygen -t ed25519 -C "YOUR_EMAIL@DOMAIN.COM"

If you want to inspect the public key before uploading it, print id_ed25519.pub with Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub on PowerShell or cat ~/.ssh/id_ed25519.pub on Linux.

Authenticate GitHub CLI and upload the SSH key

After generating the key pair. It is typically triggered by you want GitHub CLI to manage Git authentication and register the SSH public key without using the browser settings page manually. The gh auth login --git-protocol ssh step sets SSH as the Git transport for github.com, and gh auth refresh expands the token scopes so gh ssh-key add can manage account SSH keys.

Authenticate from PowerShell and upload the SSH public key to GitHub:

gh auth login --git-protocol ssh
gh auth refresh -h github.com -s admin:public_key
gh ssh-key add $env:USERPROFILE\.ssh\id_ed25519.pub --title "Alexis Peringer Laptop" --type authentication

Authenticate from Linux and upload the SSH public key to GitHub:

gh auth login --git-protocol ssh
gh auth refresh -h github.com -s admin:public_key
gh ssh-key add ~/.ssh/id_ed25519.pub --title "Alexis Ubuntu Laptop" --type authentication

gh auth login can also offer the upload interactively

If you choose ssh as the Git transport during gh auth login, GitHub CLI can detect an existing SSH key and offer to upload it immediately. The explicit gh ssh-key add command is still useful when you want a deterministic, copyable workflow.

Start the SSH agent and add your key

If the private key has a passphrase, loading it into an SSH agent avoids repeated prompts. It is typically triggered by ssh-add reporting that no authentication agent is running, or by repeated passphrase prompts during Git operations. On PowerShell, use the Windows OpenSSH agent service. On Linux, start a per-session agent and load the key into it.

Start the Windows OpenSSH agent service from PowerShell and load the key:

Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519

Start an SSH agent on Linux and load the key:

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

Persist the SSH agent across sessions

  • Linux: add eval "$(ssh-agent -s)" and ssh-add ~/.ssh/id_ed25519 to ~/.bashrc or ~/.zshrc if you want an agent started automatically in interactive shells.
  • Windows PowerShell: Set-Service -StartupType Automatic makes the OpenSSH Authentication Agent persist across reboots, so future sessions only need ssh-add after a restart or key rotation.

Test SSH connectivity to GitHub

After adding the public key to GitHub and starting the agent. It is typically triggered by first-time SSH setup or troubleshooting authentication failures. Requires network access to github.com on port 22. Some corporate networks block port 22. Verify that SSH authentication works end-to-end.

Test SSH authentication with GitHub from either PowerShell or Linux:

ssh -T git@github.com
Hi alp78! You've successfully authenticated, but GitHub does not provide shell access.

A success message confirms the key is recognized. If you see “Permission denied (publickey),” the key is not loaded in the agent or not added to GitHub.

Corporate networks may block SSH (port 22)

If ssh -T git@github.com hangs or times out, your network may be blocking outbound SSH traffic. This is common on corporate networks with restrictive firewalls.

Use SSH over HTTPS port 443 as a fallback

Add this to ~/.ssh/config to tunnel SSH through port 443:

Host github.com
    Hostname ssh.github.com
    Port 443
    User git

Then test again: ssh -T git@github.com. This works on almost all networks that allow HTTPS traffic.

Git | Authentication | comparison

MethodSecurityConvenienceBest For
HTTPS + PATToken scoped and time-limited. Stored in OS keychain via credential helper.Works through all firewalls. No agent setup.Default recommendation. Corporate environments. CI/CD.
SSH keyStrong cryptographic auth. Passphrase adds second factor.Passwordless once agent is running. Requires port 22 (or 443 workaround).Engineers who prefer key-based auth. Environments where PATs are impractical.
gh CLI as credential helperDelegates to gh auth login. Token managed by gh.Zero manual token management. gh auth refresh handles renewal.Teams using the GitHub CLI. Simplest HTTPS setup.
credential.helper storeInsecure. Plaintext file at ~/.git-credentials.Zero dependencies.Never recommended. Only for isolated, ephemeral environments.

Git | Authentication | commit signing

Many production teams require signed commits or signed tags to prove that commits genuinely come from the claimed author and have not been tampered with. GitHub shows a green “Verified” badge next to signed commits. Git supports two signing backends: GPG (the traditional method) and SSH (simpler, available since Git 2.34).

Once per machine, as part of identity setup. It is typically triggered by team policy requires signed commits, or you want the “Verified” badge on GitHub. Uses your existing SSH key — no GPG toolchain needed. Requires Git 2.34+. Cryptographically sign every commit with your SSH key.

Configure SSH-based commit signing:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

The three settings:

  • gpg.format ssh — tells Git to use SSH instead of GPG for signing.
  • user.signingkey — points to the public key file. Git uses the corresponding private key (loaded in the SSH agent) to create the signature.
  • commit.gpgsign true — signs every commit automatically. Without this, you must pass -S on each git commit.

Upload the signing key to GitHub

The public key must be added to GitHub as a signing key (not just an authentication key):

  1. Go to GitHub → Settings → SSH and GPG keys → New SSH key.
  2. Set Key type to Signing Key.
  3. Paste the contents of ~/.ssh/id_ed25519.pub.

You can use the same key for both authentication and signing, but it must be registered separately for each purpose.

Sign commits with GPG

When your team or organization requires GPG signing specifically (common in regulated industries). It is typically triggered by team policy mandates GPG-signed commits, or you need to sign tags with a GPG identity. Requires the GPG toolchain (gpg or gpg2) installed on the machine. More complex setup than SSH signing. Sign commits with a GPG key for organizations that require GPG-based verification.

Configure GPG-based commit signing:

gpg --list-secret-keys --keyid-format=long

Find your key ID from the output (the 16-character hex string after sec ed25519/), then:

git config --global user.signingkey <KEY-ID>
git config --global commit.gpgsign true

GPG on Windows — configure the GPG program path

Git for Windows may not find the GPG binary automatically. Set it explicitly:

git config --global gpg.program "C:/Program Files (x86)/GnuPG/bin/gpg.exe"

Adjust the path to match your GPG installation.

Sign tags

Tags can be signed independently of commits. Annotated tags with -s use the configured signing key:

Create a signed annotated tag:

git tag -s v1.0.0 -m "Release v1.0.0"

Verify a signed tag:

git tag -v v1.0.0

SSH signing vs GPG signing

  • SSH signing (Git 2.34+) is simpler — reuses your existing SSH key, no GPG toolchain needed, easier to configure. Recommended for most teams.
  • GPG signing is the traditional method, required by some regulated organizations and open-source projects. More complex setup (key generation, keyring management, expiry handling).
  • Both produce the “Verified” badge on GitHub. Choose based on team policy.
FlagSyntaxDescription
gpg.formatgit config --global gpg.format sshSet signing backend to SSH (alternative: openpgp)
user.signingkeygit config --global user.signingkey <key>Path to SSH public key, or GPG key ID
commit.gpgsigngit config --global commit.gpgsign trueAutomatically sign all commits
tag.gpgsigngit config --global tag.gpgsign trueAutomatically sign all annotated tags
-Sgit commit -SSign a single commit (without global auto-sign)
-sgit tag -s <tag>Create a signed annotated tag
-vgit tag -v <tag>Verify a signed tag’s signature

Git | Authentication | multiple accounts on one machine

Engineers often work with multiple GitHub accounts on the same machine — a personal account and one or more corporate accounts. The ~/.ssh/config file routes SSH traffic to the correct key based on a per-host alias, and conditional includes in ~/.gitconfig apply the correct identity automatically based on the repository’s filesystem path.

Configure SSH routing for multiple accounts

When you have multiple GitHub accounts (personal + work) and use SSH authentication for both. It is typically triggered by git push on a work repo authenticates as your personal account, or vice versa. SSH uses the first matching key by default. Without explicit routing, the wrong key is offered for the wrong account. Ensure each repository authenticates with the correct GitHub account.

Create ~/.ssh/config with per-host aliases:

# Personal GitHub account
Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal
    IdentitiesOnly yes
 
# Work GitHub account
Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes

The IdentitiesOnly yes directive prevents the SSH agent from offering other keys — only the specified key is used for each host alias.

Clone repos using the host alias instead of github.com:

# Personal repo
git clone git@github-personal:alp78/my-side-project.git
 
# Work repo
git clone git@github-work:stockindex-corp/esg-pipeline.git

For existing repos, update the remote URL to use the alias:

git remote set-url origin git@github-work:stockindex-corp/esg-pipeline.git

Create ~/.gitconfig with conditional identity switching:

[user]
    name = alp78
    email = personal@example.com
 
[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

Create ~/.gitconfig-work:

[user]
    name = Alex Perrier
    email = alex@stockindex.example.com

Any repository cloned under ~/work/ automatically uses the work identity. All other repositories use the personal identity. No per-repo --local configuration needed.

gitdir: paths must end with a trailing slash

The includeIf "gitdir:~/work/" pattern must end with / to match all repositories under that directory. Without the trailing slash, only a repository named exactly work would match.

Verify the active identity per repo

In any repository, run git config --get user.email to confirm the correct identity is resolved. Run git config --list --show-scope to see whether the value comes from global or an includeIf include.


Credential Helpers

By default, Git prompts for credentials on every remote operation. A credential helper stores credentials securely so you authenticate once and Git reuses the stored credentials silently.

Git | credential.helper | OS-native credential managers

Once per machine, as part of initial setup. It is typically triggered by git prompts for a password on every push or pull. --global writes to ~/.gitconfig. Credential helpers are OS-specific. Store authentication credentials in the OS secure keychain so Git never prompts again.

Windows — use Git Credential Manager (included with Git for Windows 2.39+):

git config --global credential.helper manager

macOS — use the system Keychain:

git config --global credential.helper osxkeychain

Linux — use git-credential-store with libsecret (GNOME Keyring) or KWallet:

git config --global credential.helper /usr/lib/git-core/git-credential-libsecret

Use gh CLI as the credential helper (recommended for GitHub)

If you have the GitHub CLI (gh) installed, it can act as the credential helper. This is the simplest setup — gh auth login handles token generation, storage, and renewal:

gh auth setup-git

This writes the following to ~/.gitconfig:

[credential "https://github.com"]
    helper =
    helper = !'C:\\Program Files\\GitHub CLI\\gh.exe' auth git-credential

After this, all HTTPS Git operations to GitHub authenticate through gh automatically.

Only in ephemeral, single-user environments (CI containers, disposable VMs). It is typically triggered by no OS keychain is available and you cannot install one. Writes credentials in plaintext to ~/.git-credentials. Anyone with read access to your home directory can read them. Eliminate password prompts in environments where security is managed at a different layer.

Store credentials in plaintext (insecure):

git config --global credential.helper store

credential.helper store saves passwords in plaintext

The file ~/.git-credentials is readable by any process running as your user. On shared machines, other users with admin access can also read it. Never use this on a shared, production, or corporate-managed machine.

Use the OS credential manager instead

On Windows: credential.helper manager. On macOS: credential.helper osxkeychain. On Linux: credential.helper libsecret or use gh auth setup-git. All store credentials encrypted in the OS keychain.


Configuration Scope and Precedence

Git reads configuration from four scopes, in order of increasing precedence. A value set in a more specific scope overrides the same key in a broader scope.

Git | config | scope hierarchy


flowchart TB
  SYS["System<br/>/etc/gitconfig or<br/>C:/Program Files/Git/etc/gitconfig<br/>Lowest precedence"]
  GLO["Global<br/>~/.gitconfig<br/>User-level defaults"]
  LOC["Local<br/>.git/config<br/>Repo-specific overrides"]
  WRK["Worktree<br/>.git/config.worktree<br/>Highest precedence"]
  SYS --> GLO --> LOC --> WRK
  style SYS fill:#1a1b26,stroke:#565f89,color:#c0caf5
  style GLO fill:#292e42,stroke:#565f89,color:#c0caf5
  style LOC fill:#292e42,stroke:#7aa2f7,color:#c0caf5
  style WRK fill:#292e42,stroke:#bb9af7,color:#c0caf5
ScopeFlagFile LocationApplies ToUse When
System--systemC:/Program Files/Git/etc/gitconfig (Windows) or /etc/gitconfig (Linux/macOS)All users, all repos on the machineCorporate IT sets machine-wide defaults (line endings, proxy). Rarely edited manually.
Global--global~/.gitconfig or $XDG_CONFIG_HOME/git/configAll repos for the current OS userPersonal identity, editor, aliases, credential helper, pull strategy.
Local--local (default).git/config inside the repoThe current repository onlyWork email different from personal, repo-specific merge strategy, custom hooks path.
Worktree--worktree.git/config.worktreeThe current worktree only (requires extensions.worktreeConfig=true)Rarely needed — advanced multi-worktree setups.

Precedence rule: worktree > local > global > system. The most specific scope wins.

Git | config | inspect configuration values

View all resolved configuration values

List all configuration keys with their resolved values:

git config --list
diff.astextplain.textconv=astextplain
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
http.sslbackend=schannel
core.autocrlf=true
core.fscache=true
core.symlinks=false
pull.rebase=false
init.defaultbranch=master
user.email=alexper.recovery@gmail.com
user.name=alp78
core.editor=nano

View all values with their scope

List all values showing which scope each comes from:

git config --list --show-scope
system	diff.astextplain.textconv=astextplain
system	filter.lfs.clean=git-lfs clean -- %f
system	filter.lfs.smudge=git-lfs smudge -- %f
system	filter.lfs.process=git-lfs filter-process
system	filter.lfs.required=true
system	http.sslbackend=schannel
system	core.autocrlf=true
system	core.fscache=true
system	core.symlinks=false
system	pull.rebase=false
system	init.defaultbranch=master
global	user.email=alexper.recovery@gmail.com
global	user.name=alp78
global	core.editor=nano
local	core.repositoryformatversion=0
local	core.filemode=false
local	core.bare=false
local	core.logallrefupdates=true
local	core.symlinks=false
local	core.ignorecase=true
local	remote.origin.url=https://github.com/alp78/git-lab.git
local	remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
local	branch.main.remote=origin
local	branch.main.merge=refs/heads/main

View all values with their source file

List all values showing the file each comes from:

git config --list --show-origin
file:C:/Program Files/Git/etc/gitconfig    diff.astextplain.textconv=astextplain
file:C:/Program Files/Git/etc/gitconfig    core.autocrlf=true
file:C:/Users/aperi/.gitconfig             user.email=alexper.recovery@gmail.com
file:C:/Users/aperi/.gitconfig             user.name=alp78
file:C:/Users/aperi/.gitconfig             core.editor=nano
file:.git/config                           remote.origin.url=https://github.com/alp78/git-lab.git
file:.git/config                           branch.main.remote=origin

Query a single value

Get the resolved value of a specific key:

git config --get user.email
alexper.recovery@gmail.com

Unset a configuration value

To remove a value from a specific scope without affecting other scopes. It is typically triggered by a local override is no longer needed, or a misconfigured value must be removed. --unset removes the key from the targeted scope only. Other scopes are unaffected. Clean up configuration without side effects.

Remove a local config override:

git config --local --unset user.email
FlagSyntaxDescription
--listgit config --listPrint all resolved key-value pairs across all scopes
--show-origingit config --list --show-originShow the config file path where each value is defined
--show-scopegit config --list --show-scopeShow the scope (system/global/local/worktree) for each value
--get <key>git config --get user.emailPrint the resolved value of a single key
--get-all <key>git config --get-all credential.helperPrint all values for a multivalued key
--unset <key>git config --unset <key>Remove a key from the targeted scope
--unset-all <key>git config --unset-all <key>Remove all values for a multivalued key
--globalgit config --global <key> <value>Write to ~/.gitconfig (all repos for the current user)
--localgit config --local <key> <value>Write to .git/config (current repo only; default scope)
--systemgit config --system <key> <value>Write to the system-wide config (requires admin)
--editgit config --global --editOpen the config file for the given scope in the configured editor

These settings form a safe, professional baseline for data engineers. Apply them once on a new machine after setting identity and credential helper.

KeyRecommended ValueScopeRationaleCaveats
user.nameYour full name or handleGlobalIdentifies you in every commit.Use --local to override per-repo if needed.
user.emailYour GitHub-verified emailGlobalLinks commits to your GitHub profile.Use noreply address for public repos.
init.defaultBranchmainGlobalAligns with GitHub’s default. Avoids mastermain confusion.Older tutorials may still reference master.
core.editor"code --wait" or nanoGlobalControls editor for commit messages, interactive rebase, merge conflict markers.--wait is required for VS Code so Git waits for you to close the editor tab.
pull.rebasefalseGlobalDefault pull strategy is merge. Prevents accidental history rewriting for beginners.Teams that prefer linear history should set true and train on rebase workflows.
fetch.prunetrueGlobalAutomatically removes remote-tracking references to branches that no longer exist on the remote.No downside. Keeps git branch -r clean.
push.defaultcurrentGlobalPushes the current branch to a same-named remote branch. Safer than matching (which pushes all branches).simple (the default since Git 2.0) is also acceptable; current is slightly more convenient.
rebase.autoStashtrueGlobalAutomatically stashes uncommitted changes before rebase and pops them after.Prevents “cannot rebase: you have unstaged changes” errors.
core.autocrlftrue (Windows) / input (macOS/Linux)GlobalNormalizes line endings. See the Line Endings section below.Prefer .gitattributes for team-wide enforcement.
core.safecrlfwarnGlobalWarns if a line-ending conversion is irreversible.Set to true to block irreversible conversions entirely.
core.filemodefalse (Windows)LocalIgnores executable-bit changes on Windows (where the filesystem does not track them).Only relevant on Windows. Linux/macOS should leave it at true.
credential.helpermanager (Windows) / osxkeychain (macOS)GlobalStores credentials in the OS secure keychain.See the Credential Helpers section.
gpg.formatsshGlobalUse SSH keys for commit signing (simpler than GPG).Requires Git 2.34+. Skip if team does not require signing.
user.signingkey~/.ssh/id_ed25519.pubGlobalPublic key used for SSH commit signing.Must be uploaded to GitHub as a signing key.
commit.gpgsigntrueGlobalAutomatically sign all commits.Skip if team does not require signing. Adds ~50ms per commit.

Apply the baseline configuration

Set all recommended global defaults in one session:

git config --global init.defaultBranch main
git config --global core.editor "code --wait"
git config --global pull.rebase false
git config --global fetch.prune true
git config --global push.default current
git config --global rebase.autoStash true
git config --global core.safecrlf warn

Git | config | useful aliases

Create command shortcuts

Git aliases let you define short names for long or frequently used commands. They are stored under [alias] in ~/.gitconfig and invoked as git <alias>.

Define common aliases:

git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br "branch -vv"
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.unstage "reset HEAD --"

git st runs git status. git lg shows a compact graph of all branches. git undo moves the last commit back to staged without discarding changes. git br shows branches with their tracking status.


Line Endings and File Normalization

Line-ending differences between operating systems are the #1 source of noisy, meaningless diffs on mixed-OS teams. This section explains the problem and provides the canonical solution.

Git | Line Endings | LF vs CRLF

What are line endings?

Every text file uses an invisible character sequence to mark the end of each line:

  • LF (\n, hex 0A) — used by Linux and macOS.
  • CRLF (\r\n, hex 0D 0A) — used by Windows.

When a Windows developer commits files with CRLF endings and a macOS developer opens them, or vice versa, Git sees every line as changed even if the visible content is identical. This produces diffs that touch hundreds of lines with no meaningful change.

How core.autocrlf works

core.autocrlf controls automatic line-ending conversion during checkout and commit:

ValueOn checkout (repo → working tree)On commit (working tree → repo)Best for
trueConvert LF → CRLFConvert CRLF → LFWindows developers on mixed-OS teams. Files on disk have CRLF; repo stores LF.
inputNo conversionConvert CRLF → LFmacOS/Linux developers on mixed-OS teams. Files on disk stay as-is; repo stores LF.
falseNo conversionNo conversionOnly if the entire team uses the same OS and you manage endings manually.

core.autocrlf alone is not enough for teams

core.autocrlf is a per-machine setting. If one developer sets it to true and another to false, the repo gets a mix of LF and CRLF files. There is no team-wide enforcement.

Use .gitattributes as the canonical team-wide policy

.gitattributes is checked into the repository and applies to every collaborator regardless of their local config. It is the authoritative solution for mixed-OS teams.

Git | Line Endings | .gitattributes

Create a .gitattributes file for line-ending normalization

Once per repository, committed to version control. It is typically triggered by setting up a new repo or fixing line-ending churn in an existing one. .gitattributes lives in the repo root. It overrides core.autocrlf for the patterns it covers. Enforce consistent line endings in the repository regardless of each developer’s OS or local config.

Create a .gitattributes file with standard normalization rules:

# Set default behavior: normalize to LF in the repo, convert to OS-native on checkout
* text=auto
 
# Force LF for files that must always use LF (scripts, CI configs)
*.sh text eol=lf
*.py text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
*.sql text eol=lf
*.tf text eol=lf
*.hcl text eol=lf
Makefile text eol=lf
Dockerfile text eol=lf
 
# Force CRLF for Windows-specific files
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
 
# Binary files — do not normalize or diff
*.png binary
*.jpg binary
*.ico binary
*.zip binary
*.gz binary
*.parquet binary
*.avro binary
*.pkl binary

Normalize an existing repository after adding .gitattributes

After adding or modifying .gitattributes in a repo that already has mixed line endings. It is typically triggered by git diff shows line-ending changes on files you did not edit. This re-normalizes all tracked files. Produces a one-time diff that corrects all endings. Bring all existing files into compliance with the new .gitattributes policy.

Re-normalize all files in the repository:

git add --renormalize .
git commit -m "chore: normalize line endings per .gitattributes"

Diagnosing line-ending noise in diffs

If git diff shows every line changed in a file, check for line-ending mismatch:

git diff --check

This flags lines with whitespace errors including mixed line endings. After adding .gitattributes and re-normalizing, the noise should disappear.


Cross-Platform File Behavior

Beyond line endings, several filesystem differences across operating systems affect Git behavior. Understanding these prevents subtle bugs and noisy diffs on mixed-OS teams.

Git | Cross-Platform | filesystem differences

IssueWindowsmacOSLinuxGit Config / Fix
Line endingsCRLF (\r\n)LF (\n)LF (\n).gitattributes with * text=auto
Case sensitivityCase-insensitive (Readme.md = readme.md)Case-insensitive by default (HFS+)Case-sensitivecore.ignorecase=true on Windows/macOS (default). Avoid relying on case differences in filenames.
File permissions / executable bitNot tracked (NTFS does not store Unix permissions)TrackedTrackedcore.filemode=false on Windows (set automatically by Git for Windows).
Path length260-character limit by defaultNo practical limitNo practical limitEnable long paths: git config --system core.longpaths true (requires admin).
SymlinksNot supported by default (require Developer Mode)SupportedSupportedcore.symlinks=false on Windows (default). Enable with Developer Mode.

Case-sensitivity trap on macOS and Windows

If a Linux developer creates both Config.py and config.py in the same directory, macOS and Windows cannot distinguish them — one file silently overwrites the other on checkout. Git will track both, but the working tree can only contain one.

Prevent case conflicts

Establish a team convention: all filenames are lowercase with hyphens or underscores. Enforce it with a pre-commit hook or CI check. Avoid renaming files by case only (e.g., README.mdReadme.md) — this requires a two-step rename through a temporary name on case-insensitive filesystems.

Windows 260-character path limit

Deep directory nesting (common in node_modules/, .terraform/, Python virtualenvs) can exceed the 260-character Windows path limit, causing Filename too long errors on clone or checkout.

Enable long paths on Windows

git config --system core.longpaths true

This requires an admin terminal. Also enable the Windows group policy: Computer Configuration → Administrative Templates → System → Filesystem → Enable Win32 long paths.


Creating and Cloning Repositories

Use git init to start a new repository from scratch, or git clone to download an existing one from a remote. These are the two entry points into any Git workflow.

Git | init | initialize a new repository

git init turns any directory into a Git repository by creating the hidden .git/ subdirectory. Use this when starting a brand-new project locally. If the project already exists on a remote (GitHub, GitLab), use git clone instead.

Create a new repository

Starting a brand-new project that has no remote yet. It is typically triggered by ls -la .git returns “No such file or directory.”. Does not require network access. Creates .git/ in the current directory with the default branch name from init.defaultBranch. Initialize Git tracking in an existing directory.

Initialize a new Git repository:

git init
Initialized empty Git repository in C:/Users/aperi/AppData/Local/Temp/git-init-demo/.git/

What git init creates

The .git/ subdirectory contains:

  • HEAD — pointer to the current branch (initially refs/heads/main or refs/heads/master)
  • config — local repository configuration
  • objects/ — the object database (commits, trees, blobs)
  • refs/ — branch and tag pointers
  • hooks/ — sample hook scripts (not active until renamed)
  • info/ — auxiliary information (e.g., exclude patterns)

Contents of .git/ after initialization:

ls .git/
HEAD
config
description
hooks
info
objects
refs
FlagSyntaxDescription
-b <name> / --initial-branch <name>git init -b mainSet the name of the first branch (overrides init.defaultBranch)
--baregit init --bareCreate a repository with no working tree — used for server/remote repos
--template <dir>git init --template /pathPopulate .git/ from a custom template directory (custom hooks, config)
--shared[=<perms>]git init --shared=groupSet group-write permissions for shared server repositories

Git | clone | download a remote repository

git clone downloads a repository from a remote URL to your local machine, including all branches, tags, and the full commit history. The remote is automatically registered as origin.

Clone a repository

When joining an existing project or setting up a new machine. It is typically triggered by the repo exists on GitHub and you need a local copy. Requires network access and authentication (PAT or SSH key). Creates a new directory named after the repository. Get a complete, working copy of a remote repository with full history.

Clone a repository over HTTPS:

git clone https://github.com/alp78/git-lab.git
Cloning into 'git-lab'...

Verify the clone immediately after

After cloning, verify the remote URL, current branch, and tracking relationship:

Check remotes:

git remote -v
origin	https://github.com/alp78/git-lab.git (fetch)
origin	https://github.com/alp78/git-lab.git (push)

Check the current branch and tracking:

git branch -a
* main
  remotes/origin/main

Check working tree status:

git status
On branch main
Your branch is up to date with 'origin/main'.
 
nothing to commit, working tree clean

View commit history:

git log --oneline
7f5dfe6 chore: add pre-commit configuration
71f876e feat: add stock pipeline skeleton with tests and gitignore
0850a8c initial commit: add README

Clone into a specific directory

Clone into a custom directory name:

git clone https://github.com/alp78/git-lab.git my-project

Clone a specific branch

Clone and check out a specific branch (full history):

git clone --branch develop https://github.com/org/repo.git

Clone a single branch only (reduce download size):

git clone --branch develop --single-branch https://github.com/org/repo.git

Shallow clone — latest commit only

In CI/CD pipelines, automated builds, or when you only need the latest code and not the history. It is typically triggered by clone time or disk space is a concern, and full history is not required. --depth 1 fetches only the most recent commit. Some Git operations (bisect, blame across history) will not work without unshallowing. Minimize clone time and disk usage.

Create a shallow clone with depth 1:

git clone --depth 1 https://github.com/alp78/git-lab.git /tmp/git-lab-shallow
Cloning into '/tmp/git-lab-shallow'...

Shallow clone limitations

  • Cannot git push to the remote without first unshallowing.
  • Cannot run git bisect, git log --all, or other commands that require full history traversal.
  • git blame only shows the most recent commit for every line.

Unshallow when full history is needed

git fetch --unshallow

Converts a shallow clone into a full clone. After this, all history commands work normally.

Shallow clones in CI/CD

GitHub Actions uses actions/checkout with fetch-depth: 1 by default — a shallow clone. This is correct for most build and test jobs. Set fetch-depth: 0 only when you need full history (e.g., generating changelogs, running git describe).

FlagSyntaxDescription
--depth <n>git clone --depth 1 <url>Shallow clone: fetch only the last N commits
-b / --branch <name>git clone -b develop <url>Check out the specified branch after cloning
--single-branchgit clone --single-branch -b main <url>Fetch only the specified branch; omit all other remote refs
--baregit clone --bare <url>Clone without a working tree (for server/mirror repos)
--mirrorgit clone --mirror <url>Clone all refs including remote tracking; implies --bare
--recurse-submodulesgit clone --recurse-submodules <url>Automatically initialize and clone all submodules
--shallow-submodulesgit clone --shallow-submodules <url>Shallow-clone each submodule to depth 1
--filter=blob:nonegit clone --filter=blob:none <url>Partial clone: download commit/tree objects only, fetch blobs on demand (Git 2.19+)

Git | clone | partial clone and sparse checkout for large repos

Large monorepos (thousands of files, deep directory trees, gigabytes of history) make a standard git clone slow and disk-heavy. Git provides two complementary features for working efficiently in these repositories:

  • Partial clone (--filter=blob:none) downloads only commit and tree objects during clone. File contents (blobs) are fetched on demand as you check them out. This drastically reduces initial clone time.
  • Sparse checkout limits which directories appear in your working tree. Files outside the sparse set are not checked out, saving disk space and reducing noise. Combined with partial clone, files outside the sparse set are never even downloaded.

Together they enable a “clone the structure, check out only what you need” workflow — essential for data engineering teams working in monorepos that contain infrastructure code, multiple pipelines, shared libraries, and documentation side by side.

Partial clone with sparse checkout

When joining a large monorepo or setting up a new machine for a repo where you only need a subset of directories. It is typically triggered by standard git clone takes too long, uses too much disk, or downloads irrelevant code. Requires Git 2.25+ for sparse checkout, Git 2.19+ for partial clone. The remote must support partial clone (GitHub, GitLab, and Bitbucket all do). Full history is available — only blob downloads are deferred. Get a working checkout of a large repo in seconds, with only the directories you need on disk.

Step 1 — partial clone with sparse mode:

git clone --filter=blob:none --sparse https://github.com/alp78/git-lab.git /tmp/git-lab-sparse
Cloning into 'C:/Users/aperi/AppData/Local/Temp/git-lab-sparse'...

This downloads commit and tree metadata but no file contents. The working tree contains only root-level files.

Step 2 — select the directories you need:

cd /tmp/git-lab-sparse
git sparse-checkout set src tests

Step 3 — verify the sparse set:

git sparse-checkout list
src
tests

Only the src/ and tests/ directories (and root-level files) are checked out. All other directories exist in the Git history but are not materialized on disk. Git downloads blob contents for checked-out files on demand.

Add more directories later:

git sparse-checkout add docs infra

Return to full checkout:

git sparse-checkout disable

Sparse checkout changes what's visible, not what's tracked

Files outside the sparse set are not deleted from Git history — they are hidden from your working tree. git log still shows commits that touched those files. git status only reports on files in the sparse set. If you add a directory to the sparse set later, Git downloads and checks out its contents.

Combine with depth for maximum speed

For CI or quick exploration, combine partial clone, sparse checkout, and shallow depth:

git clone --filter=blob:none --sparse --depth 1 https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set pipelines/esg

This gives you only the latest commit, only the tree structure, and only the files in pipelines/esg/ — a 30-second setup for a 10 GB monorepo.

Sparse checkout for data engineering monorepos

In monorepos with multiple pipelines (pipelines/pricing/, pipelines/esg/, pipelines/risk/, infra/, libs/), each engineer can check out only their pipeline and shared libraries:

git sparse-checkout set pipelines/esg libs/common

This avoids downloading test fixtures, models, and generated artifacts from other teams’ pipelines. When you need to cross-reference another pipeline, add it: git sparse-checkout add pipelines/pricing.

FlagSyntaxDescription
--sparsegit clone --sparse <url>Enable sparse checkout mode during clone (only root files checked out)
setgit sparse-checkout set <dir> [<dir>...]Define the directories to include in the working tree
addgit sparse-checkout add <dir>Add a directory to the existing sparse set
listgit sparse-checkout listShow the current sparse checkout directories
disablegit sparse-checkout disableReturn to full working tree (all files checked out)
initgit sparse-checkout init --coneInitialize sparse checkout in cone mode (directory-based, faster)
reapplygit sparse-checkout reapplyRe-apply sparse patterns after config changes

Pre-Commit Hooks — Automated Quality Gates

Git hooks are scripts stored in .git/hooks/ that execute automatically at lifecycle events — before a commit, before a push, after a merge, etc. The pre-commit framework makes hook management declarative and shareable across the team via a versioned .pre-commit-config.yaml file.

Git | Hooks | what hooks are and how they work

Understanding Git hooks

A Git hook is an executable script in .git/hooks/ that Git runs at a specific point in its workflow. If the script exits with a non-zero status, the operation is aborted.

Key hooks:

HookTriggerCommon Use
pre-commitBefore a commit is createdLint, format, secrets scanning
commit-msgAfter the commit message is enteredEnforce message conventions (e.g., Conventional Commits)
pre-pushBefore git push sends data to the remoteRun tests, prevent force-push to main
post-mergeAfter a successful git mergeInstall dependencies, rebuild assets
pre-rebaseBefore git rebase beginsWarn if rebasing a shared branch

Hooks are local only — they live in .git/hooks/, which is not tracked by Git. This means hooks are not automatically shared when someone clones the repo. The pre-commit framework solves this problem.

Hooks can be bypassed with --no-verify

Any developer can skip pre-commit and commit-msg hooks by adding --no-verify to their commit command. Hooks are a convenience and first line of defense, not an enforcement mechanism.

Enforce checks in CI as the mandatory second line

Run the same checks in your CI pipeline (GitHub Actions, GitLab CI). Local hooks catch issues early and fast; CI catches everything that slips through.

Git | Hooks | pre-commit framework

The pre-commit framework manages hooks declaratively through a .pre-commit-config.yaml file in the repo root. It downloads, caches, and runs hook implementations from external repositories.

Install the pre-commit framework

Once per machine (or once per virtual environment). It is typically triggered by pre-commit --version returns “command not found.”. Requires Python and pip. Installs into the active Python environment. Make the pre-commit CLI available for configuring and running hooks.

Install pre-commit via pip:

pip install pre-commit

Verify the installation:

pre-commit --version
pre-commit 4.5.1

Create the hook configuration file

Once per repository, committed to version control. It is typically triggered by setting up a new repo or adding hooks to an existing one. .pre-commit-config.yaml lives in the repo root. Each entry under repos points to a hook repository, a pinned revision, and hook IDs. Define which checks run before every commit.

Example .pre-commit-config.yaml for a data engineering repository:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.22.1
    hooks:
      - id: gitleaks
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.11.5
    hooks:
      - id: ruff
      - id: ruff-format

Recommended hooks for data engineering teams

  • trailing-whitespace / end-of-file-fixer — clean whitespace. Prevents noisy diffs.
  • check-yaml — validates YAML syntax (Airflow DAGs, dbt schema.yml, GitHub Actions workflows).
  • check-added-large-files — blocks accidentally committed data files, models, or binaries.
  • gitleaks — scans for hardcoded secrets (API keys, passwords, GCP service account keys).
  • ruff — Python linting (replaces flake8, isort, pyflakes). ruff-format — Python formatting (replaces black).
  • sqlfluff — SQL linting and formatting (add via repo: https://github.com/sqlfluff/sqlfluff).
  • terraform_fmt / terraform_validate — Terraform formatting and validation (add via repo: https://github.com/antonbabenko/pre-commit-terraform).

Install hooks in the local repository

Once per clone, after cloning a repo that has .pre-commit-config.yaml. It is typically triggered by .git/hooks/pre-commit does not exist or is a sample file. Writes the hook script into .git/hooks/. After this, hooks run automatically before every git commit. Activate the pre-commit hooks for this repository.

Register hooks in the local repository:

pre-commit install
pre-commit installed at .git/hooks/pre-commit

Run all hooks against the entire codebase

On first setup (to validate the entire codebase), after adding new hooks, or in CI pipelines. It is typically triggered by initial clone, new hook added, or CI pipeline step. Runs every configured hook against every file in the repository, not just staged changes. Verify the entire codebase passes all checks.

Run all hooks on all files:

pre-commit run --all-files
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
Detect hardcoded secrets.................................................Passed
ruff.....................................................................Passed
ruff-format..............................................................Passed

Update hooks to the latest versions

Periodically (monthly or quarterly) to pick up bug fixes and new rules. It is typically triggered by scheduled maintenance or when a hook version is known to have a bug. Updates the rev field in .pre-commit-config.yaml to the latest tag for each repo. Commit the changes afterward. Keep hook implementations current without manual version tracking.

Auto-update all hook versions:

pre-commit autoupdate
[https://github.com/pre-commit/pre-commit-hooks] updating v5.0.0 -> v6.0.0
[https://github.com/gitleaks/gitleaks] updating v8.22.1 -> v8.30.0
[https://github.com/astral-sh/ruff-pre-commit] updating v0.11.5 -> v0.15.10

After autoupdate, commit the modified .pre-commit-config.yaml to share the updated versions with the team.

Run a single hook by name

Run only the gitleaks hook on all files:

pre-commit run gitleaks --all-files

Debug a failing hook

When a hook fails and the output is unclear. It is typically triggered by pre-commit run exits non-zero with insufficient detail. --verbose shows full hook output even for passing checks. Diagnose exactly what the hook is checking and why it failed.

Run hooks with verbose output:

pre-commit run --all-files --verbose

Show the diff of auto-fixed files when a formatting hook fails:

pre-commit run --all-files --show-diff-on-failure

When --no-verify is acceptable

When is it safe to skip hooks?

  • Acceptable: merge commits where hooks already passed on both branches, emergency hotfixes followed by a CI run, documentation-only commits where code hooks are irrelevant.
  • Not acceptable: “the hook is annoying,” “I’ll fix it later,” or “I’m in a hurry.” If a hook fails, fix the issue — do not bypass the gate.
FlagSyntaxDescription
installpre-commit installWrite hook scripts into .git/hooks/
runpre-commit runRun hooks against staged files only
--all-filespre-commit run --all-filesRun hooks against all repository files
--files <path>pre-commit run --files src/foo.pyRun hooks against specific files only
--hook-stage <stage>pre-commit run --hook-stage pushTarget a specific stage (commit, push, merge-commit)
--verbosepre-commit run --verboseShow full hook output even for passing checks
--show-diff-on-failurepre-commit run --show-diff-on-failureDisplay the diff of auto-fixed files when a hook fails
autoupdatepre-commit autoupdateUpdate all hooks to their latest tagged version
cleanpre-commit cleanRemove cached hook environments
uninstallpre-commit uninstallRemove the hook script from .git/hooks/

Data Engineering Team Setup Guidance

Data engineering repositories have specific setup concerns beyond standard software projects. This section provides concrete recommendations for common repo types.

Git | Data Engineering | repository setup patterns

Python repositories

  • .gitignore: exclude __pycache__/, *.pyc, .venv/, *.egg-info/, dist/, build/, .pytest_cache/, .mypy_cache/, .ruff_cache/.
  • Hooks: ruff (lint + format), gitleaks (secrets), check-added-large-files (prevent data files).
  • Line endings: .gitattributes with *.py text eol=lf.
  • Lock files: commit requirements.txt or uv.lock. Do not commit *.egg files.
  • Virtual environments: never commit .venv/ — add to .gitignore.

SQL repositories

  • Hooks: sqlfluff (lint + format), check-yaml (for dbt schema.yml).
  • Line endings: *.sql text eol=lf. SQL files with CRLF cause issues in some database clients and CI pipelines.
  • Naming convention: lowercase filenames with underscores. Avoid spaces — they break many CLI tools.

dbt repositories

  • .gitignore: exclude target/, dbt_packages/, logs/, dbt_modules/.
  • Hooks: sqlfluff with dbt dialect, check-yaml, trailing-whitespace.
  • Config: commit dbt_project.yml, profiles.yml (without credentials), and packages.yml.

Terraform repositories

  • .gitignore: exclude .terraform/, *.tfstate, *.tfstate.backup, *.tfplan, .terraform.lock.hcl (debatable — many teams commit the lock file for reproducibility).
  • Hooks: terraform_fmt, terraform_validate, gitleaks.
  • Secrets: never commit *.tfvars files containing credentials. Use environment variables or a secrets manager.

Airflow / orchestration repositories

  • .gitignore: exclude logs/, airflow.db, airflow.cfg (if generated), __pycache__/.
  • Hooks: ruff, check-yaml, gitleaks.
  • DAG structure: one DAG per file, stored in dags/. Avoid deeply nested directories — Airflow’s DAG discovery can have path-length issues on Windows.

Notebooks

  • Git and notebooks: Jupyter notebooks (.ipynb) are JSON files with embedded outputs (images, data). They produce unreadable diffs and large file sizes.
  • Recommendation: use nbstripout as a pre-commit hook to strip outputs before committing. This keeps diffs clean and file sizes small.
  • Alternative: use Jupytext to pair notebooks with .py files and commit only the .py versions.
  • .gitignore: exclude .ipynb_checkpoints/.

Large files / Git LFS

Git LFS (Large File Storage) replaces large files in your repository with lightweight pointer files, while storing the actual file contents on a separate LFS server. This keeps clone times fast and repository sizes manageable. For data engineering teams handling Parquet files, serialized ML models, test fixtures, or large CSV datasets, LFS is an installation-day concern — not something to discover after the first 200 MB commit is rejected.

Adopt Git LFS before the repository starts absorbing assets that Git handles poorly: files above roughly 50 MB, binary artifacts such as models and images, large data snapshots, and any file type whose revisions are expensive to diff but expensive to lose.

Cost: Git LFS requires a paid plan on GitHub for storage and bandwidth beyond the free tier (1 GB storage, 1 GB/month bandwidth per account).

Install and verify Git LFS

On every new machine, before cloning any repository that uses LFS-tracked files. It is typically triggered by git lfs version returns “command not found,” or cloned LFS files contain pointer text instead of actual data. Git LFS is a separate binary that hooks into Git. On Windows, it is included with Git for Windows 2.39+. On macOS/Linux, it must be installed separately. Ensure LFS is available and initialized before touching any repository with large tracked files.

Check if Git LFS is already installed:

git lfs version
git-lfs/3.7.1 (GitHub; windows amd64; go 1.25.1; git b84b3384)

If the command is not found, install Git LFS:

Install Git LFS on macOS:

brew install git-lfs

Install Git LFS on Debian/Ubuntu:

sudo apt-get install git-lfs

Initialize Git LFS (required once per machine):

git lfs install
Updated Git hooks.
Git LFS initialized.

This registers the LFS clean/smudge filters in your global ~/.gitconfig and installs the necessary Git hooks. Without this step, LFS-tracked files will appear as small pointer files instead of their actual content.

Track file patterns with LFS

When adding a new binary or large file type to a repository. It is typically triggered by a new file type needs LFS tracking (e.g., adding Parquet fixtures to a test suite). git lfs track adds patterns to .gitattributes. The .gitattributes file must be committed to share LFS tracking rules with all collaborators. Tell Git which file patterns should be stored in LFS instead of the regular object database.

Track common data engineering file types:

git lfs track "*.parquet" "*.pkl" "*.h5"
Tracking "*.parquet"
Tracking "*.pkl"
Tracking "*.h5"

Verify tracked patterns:

git lfs track
Listing tracked patterns
    *.parquet (.gitattributes)
    *.pkl (.gitattributes)
    *.h5 (.gitattributes)
Listing excluded patterns

Always commit .gitattributes after adding LFS patterns:

git add .gitattributes
git commit -m "chore: track parquet, pkl, h5 files with Git LFS"
Verify LFS is working in a cloned repo

After cloning a repository that uses LFS, to confirm actual file contents were downloaded — not just pointer files. It is typically triggered by files look wrong (small text files where large binaries are expected), or git lfs pull was not triggered. Read-only check. If LFS was not initialized before the clone, files will contain pointer text. Confirm LFS files are fully downloaded and ready to use.

Check LFS environment and endpoint:

git lfs env

If LFS files are pointer stubs, force-download the actual content:

git lfs pull

LFS files appear as pointer text if LFS is not initialized

If you clone a repo before running git lfs install, LFS-tracked files contain pointer text like version https://git-lfs.github.com/spec/v1 instead of actual data. This silently breaks pipelines that expect real Parquet/CSV/model files.

Fix LFS pointer files after the fact

Run git lfs install followed by git lfs pull to download the actual file contents. For CI, ensure LFS is installed in the container image and use lfs: true in actions/checkout.

Secrets scanning

  • Critical: never commit API keys, passwords, service account JSON files, .env files, or PATs.
  • Pre-commit: use gitleaks as the first hook in every repository.
  • GitHub-side: enable GitHub Secret Scanning (Settings → Code security and analysis → Secret scanning) for an additional layer.
  • If a secret is accidentally committed: rotate the secret immediately. Removing it from a future commit does not remove it from history. Use git filter-repo or BFG Repo-Cleaner to purge it from all history.

Corporate and Enterprise Edge Cases

Enterprise environments introduce additional complexity beyond standard Git setup. This section addresses common corporate infrastructure issues.

Git | Enterprise | proxy and certificate issues

Corporate proxy configuration

When Git operations hang or time out behind a corporate proxy. It is typically triggered by git clone or git push fails with connection timeout or SSL errors. Corporate proxies intercept HTTPS traffic. Git needs to know the proxy address. Route Git HTTPS traffic through the corporate proxy.

Configure Git to use a corporate proxy:

git config --global http.proxy http://proxy.corp.example.com:8080
git config --global https.proxy http://proxy.corp.example.com:8080

Remove proxy configuration (e.g., when working from home):

git config --global --unset http.proxy
git config --global --unset https.proxy

Custom CA certificate (TLS interception)

When Git returns SSL certificate errors behind a corporate firewall that performs TLS inspection. It is typically triggered by SSL certificate problem: unable to get local issuer certificate or similar errors. Corporate firewalls often re-sign HTTPS traffic with an internal CA. Git does not trust this CA by default. Tell Git to trust the corporate CA certificate.

Point Git to the corporate CA bundle:

git config --global http.sslCAInfo /path/to/corporate-ca-bundle.crt

Never disable SSL verification globally

git config --global http.sslVerify false is a common but dangerous workaround. It disables certificate validation for all Git operations, making you vulnerable to man-in-the-middle attacks.

Add the corporate CA to the trust store instead

Ask your IT department for the corporate CA certificate. Add it to Git’s CA bundle with http.sslCAInfo, or add it to the OS trust store so all applications trust it.

SSO-enforced organizations

GitHub organizations can enforce SAML SSO. When SSO is enabled, PATs must be authorized for the organization after creation:

  1. Generate the PAT normally.
  2. Go to GitHub → Settings → Developer settings → Personal access tokens.
  3. Click the token → Configure SSOAuthorize for the organization.

Without this step, Git operations against the organization’s repos return 403 Forbidden.

Managed laptops with system-level Git config

Corporate machines may have a system-level gitconfig (in the Git installation directory) set by IT. This can silently set core.autocrlf, proxy settings, or credential helpers.

Check for system-level overrides:

git config --list --show-scope | grep "^system"

If unexpected values appear, consult IT before overriding — they may be there for a reason (proxy, CA, compliance).

Devcontainers and ephemeral environments

In devcontainers, Codespaces, or ephemeral CI environments:

  • Identity: set user.name and user.email in the container’s Dockerfile or .devcontainer.json.
  • Credentials: use GITHUB_TOKEN (automatically available in GitHub Actions and Codespaces) or mount the host’s credential helper.
  • Hooks: run pre-commit install in the container’s postCreateCommand.

New-Machine Onboarding Sequence

This section consolidates everything above into a step-by-step flow for setting up Git on a brand-new machine. Follow it in order.

Git | Onboarding | step-by-step setup

New-machine Git setup — complete checklist

  1. Install Git — use the instructions in the Installing Git section for your OS.
  2. Verify versiongit --version (expect 2.39+ for modern features).
  3. Set identitygit config --global user.name and git config --global user.email.
  4. Choose auth method — HTTPS with PAT (recommended) or SSH key.
  5. Configure credential helpercredential.helper manager (Windows), osxkeychain (macOS), or gh auth setup-git.
  6. Configure commit signing (if required) — SSH signing (gpg.format ssh, user.signingkey, commit.gpgsign true) or GPG signing.
  7. Configure multi-account SSH (if applicable) — create ~/.ssh/config with per-host aliases and includeIf in ~/.gitconfig.
  8. Apply recommended defaultsinit.defaultBranch main, core.editor, pull.rebase false, fetch.prune true, etc.
  9. Set aliasesst, co, lg, undo, etc.
  10. Install Git LFSgit lfs install (verify with git lfs version).
  11. Validate configgit config --list --show-scope to verify all values.
  12. Clone a test repogit clone https://github.com/alp78/git-lab.git.
  13. Verify clonegit remote -v, git branch -a, git status, git log --oneline.
  14. Install pre-commitpip install pre-commit then pre-commit install in the cloned repo.
  15. Run hook checkspre-commit run --all-files.
  16. Create a test commit — edit a file, git add, git commit, verify hooks run (and signing if configured).
  17. Push the test commitgit push to verify authentication works end-to-end.

Troubleshooting Cookbook

Common onboarding and setup problems with step-by-step diagnosis and fixes.

Git | Troubleshooting | common setup issues

git command not found

Cause: Git is not installed or not on the system PATH. Fix: install Git (see Installing Git section). On Windows, restart the terminal after installation. Verify with git --version.

Wrong or missing user.email

Cause: user.email not set, or set to the wrong address. Diagnosis: git config --get user.email Fix: git config --global user.email "correct@email.com"

Contributions not showing on GitHub

Cause: the commit email does not match any verified email on your GitHub account. Diagnosis: git log --format="%ae" -1 — check what email the last commit used. Fix: go to GitHub → Settings → Emails → verify the email. Then fix future commits: git config --global user.email "verified@email.com". To fix existing commits, use git rebase -i with exec git commit --amend --author="..." (destructive — rewrites history).

Authentication failed over HTTPS

Cause: PAT expired, revoked, or never created. GitHub no longer accepts passwords. Diagnosis: gh auth status — check if the token is valid. Fix: generate a new PAT on GitHub → Settings → Developer settings → Tokens. If using gh: gh auth login.

Permission denied (publickey) over SSH

Cause: SSH key not loaded in the agent, not added to GitHub, or wrong key file. Diagnosis:

ssh -vT git@github.com

Check which key files are being offered. Check ssh-add -l for loaded keys. Fix: ssh-add ~/.ssh/id_ed25519, then verify ssh -T git@github.com.

Host key verification failed

Cause: GitHub’s SSH host key changed or was never trusted. Fix: add GitHub’s host keys manually:

ssh-keyscan github.com >> ~/.ssh/known_hosts

Remote repository not found

Cause: wrong URL, no access to the repo, or PAT lacks the repo scope. Diagnosis: git remote -v — verify the URL. Try opening it in a browser. Fix: correct the URL with git remote set-url origin <correct-url>. Verify PAT scope includes repo.

pre-commit: command not found

Cause: pre-commit is not installed in the active Python environment. Fix: pip install pre-commit in the correct environment. Verify with pre-commit --version.

Editor opens vi unexpectedly

Cause: core.editor not set. Git falls back to vi on Linux/macOS. Fix: git config --global core.editor "code --wait" (or nano, vim, etc.).

CRLF/LF warnings or noisy diffs

Cause: line-ending mismatch between OS and repo. Fix: add .gitattributes with * text=auto and run git add --renormalize . (see Line Endings section).

detected dubious ownership in repository

Cause: the repository directory is owned by a different OS user. Git refuses to operate as a security measure (CVE-2022-24765). Fix:

git config --global --add safe.directory /path/to/repo

Understand before you add safe.directory

This error exists to protect you from running Git in a directory an attacker controls. Only add safe.directory for directories you trust. Common legitimate trigger: repos on network drives, USB drives, or WSL-mounted Windows paths.

Hooks not executable / permission denied on .git/hooks/*

Cause: on Linux/macOS, hook scripts must be executable (chmod +x). Fix: chmod +x .git/hooks/pre-commit. When using the pre-commit framework, pre-commit install handles this automatically.


Final Setup Validation Checklist

Run this checklist after completing the onboarding sequence to verify everything works.

Git | Validation | post-setup checks

#CheckCommandExpected Result
1Git versiongit --version2.39+
2Author namegit config --get user.nameYour name or handle
3Author emailgit config --get user.emailYour GitHub-verified email
4Default branchgit config --get init.defaultBranchmain
5Editorgit config --get core.editorYour preferred editor
6Credential helpergit config --get credential.helpermanager, osxkeychain, or gh helper
7Commit signinggit config --get commit.gpgsigntrue (if team requires signing)
8Auth test (HTTPS)gh auth status✓ Logged in to github.com
9Auth test (SSH)ssh -T git@github.comHi <user>! You've successfully authenticated
10Multi-account routingssh -T git@github-work (if configured)Correct account authenticated
11Git LFS installedgit lfs versionVersion string (e.g., git-lfs/3.7.1)
12Git LFS initializedgit lfs envEndpoint and filter config present
13Clone worksgit clone <url> + git remote -vRemote URL matches, branch tracks origin/main
14Hooks installedls .git/hooks/pre-commitFile exists (not .sample)
15Hooks passpre-commit run --all-filesAll checks pass
16Push worksgit push (after a test commit)No authentication errors
17Line-ending policycat .gitattributes* text=auto present
18Secrets scanningpre-commit run gitleaks --all-filesPassed (no secrets detected)

Git Setup and Configuration References