Posts in category “Git”

Git Bash and MSYS2 disagree on Git SSL? Use one Git for Windows

Git Bash and MSYS2 can load different Git executables while sharing the same ~/.gitconfig. http.sslbackend=schannel works with Git for Windows, but an MSYS2-native Git may support only OpenSSL and fail with Unsupported SSL backend.

Remove the MSYS2 Git package and put Git for Windows first in MSYS2's PATH:

pacman -R --noconfirm git
printf '%s\n' 'export PATH="/c/Program Files/Git/cmd:$PATH"' >> ~/.bashrc
source ~/.bashrc
hash -r

Use one shared configuration:

git config --global http.sslbackend schannel
git config --global credential.helper manager

Verify both shells resolve the same executable:

type -a git
git --version
git ls-remote origin HEAD

This keeps certificate verification in the Windows certificate store and avoids shell-specific OpenSSL CA or stale credential-store differences. Do not disable TLS verification with http.sslVerify=false.

Reviewing a branch: `git diff` wants three dots, `git log` wants two

Same task — "show me what this branch changed" — but the two tools take opposite dot conventions. Get it backwards and your review fills with commits the author never touched.

The diff: use three dots

git diff origin/main...HEAD

Three-dot diff is git diff $(git merge-base origin/main HEAD) HEAD — it diffs against the branch point, not the tip of main. That's exactly what you want: the author's delta and nothing else.

The nice property: it's immune to origin/main moving forward. New commits landing on main after the branch point aren't ancestors of HEAD, so they don't shift the merge-base. The diff stays clean.

The trap is a stale base, not a newer one. If your local main is older than the branch point, the merge-base slides back to an older ancestor and the diff swallows unrelated upstream commits — making the author look like they changed far more than they did. So fetch first:

git fetch origin
git diff origin/main...HEAD

Want to see the branch point itself? git merge-base origin/main HEAD.

The log: use two dots

git log origin/main..HEAD

Two-dot log = commits reachable from HEAD but not from origin/main = the branch's own commits, exactly.

Don't reach for three dots here out of habit — git log A...B is the symmetric difference, so it also lists the commits main picked up that HEAD doesn't have. That's the noise you were trying to avoid.

So: diff three-dot, log two-dot. Different tools, opposite defaults, same job.

Git for Windows nagging "Unlink failed. Should I try again? (y/n)"? One env var kills it

On a corporate Windows box, git pull/git fetch keeps stopping to ask:

Unlink of file '.git/objects/pack/pack-305a05....idx' failed. Should I try again? (y/n)

The cause is a security agent — SentinelOne, ZScaler, Defender — holding an open handle on the old .idx files while Git tries to repack. Git for Windows wraps unlink/rename failures in a retry prompt, and you end up babysitting every pull, mashing n.

yes n | git pull works but you have to remember to prefix it every time. The permanent fix is one line in ~/.bashrc (the Git Bash one):

export GIT_ASK_YESNO=false

Git runs the value of GIT_ASK_YESNO as a command to decide whether to retry — a non-zero exit is treated as "n". false always exits non-zero, so every prompt is silently answered "no". It's cleaner than </dev/null redirection (works regardless of whether stdin is a tty) and doesn't touch the other interactive bits — commit-message editor, credential prompts — which go through different machinery.

Answering "n" just means Git leaves the locked old pack file on disk; the new pack is already live, so the repo is fine. Once the security agent lets go, a git gc sweeps up the leftovers.

If the prompts are frequent, this cuts down how often they fire — sometimes the lock is Git's own multi-threaded pack-objects, not the AV:

git config --global pack.threads 1

This is the silence-it companion to the heavier "it's an open handle, not a permission" diagnosis — same root cause (a process holding a handle), but here you just want the nagging to stop, not to hunt the locker down.

Need a per-repo toggle for your own tool? Stash it in `git config`

When you build tooling around git repos, you eventually hit "where do I store a per-repo setting?" Committing a .toolrc pollutes the repo and needs a PR; a sidecar file in ~/.config can't easily be per-repo. The clean answer: put it straight into git config under your own namespace.

# per-repo (lands in <repo>/.git/config — local, never committed)
git config --local mux.adhocWorktree false

# per-machine, all repos (lands in ~/.gitconfig)
git config --global mux.adhocWorktree false

# read it back, normalized to true/false
git config --bool --get mux.adhocWorktree   # -> false ; exit 1 if unset

git lets you invent any <section>.<key> it doesn't recognize (mux.* here) and just stores it. That hands you three things for free that you'd otherwise have to build:

  • A per-repo store that never leaks.git/config is local to the checkout and never committed, so it won't pollute someone else's repo.
  • Scope layering with native precedence--local (repo) transparently overrides --global (machine). One git config --get resolves the hierarchy; you don't write any "check repo, then fall back to machine" logic.
  • Type parsing--bool normalizes false/0/no/offfalse, and a non-zero exit cleanly signals "not configured" so you can fall through to your built-in default.

So a full precedence chain for a feature flag becomes: CLI flag → env var → git config (local→global) → built-in default — and the middle two scopes are entirely git's job.

One gotcha with worktrees: git config --local writes to the shared common .git/config, not the linked worktree, because worktrees share config. If your setting is meant to key off the main checkout (it usually is), that's exactly right — but don't expect two worktrees of the same repo to hold different --local values.

The decision that surfaced this: an agent launcher was hardwiring a git worktree per session (hundreds of MB on big repos, painful on a VM). The whole "make it opt-out, but where does the per-repo marker live?" problem evaporated the moment we realized git config already is a per-repo + per-machine settings store with precedence baked in.

Got personal files to ignore? Use .git/info/exclude, not the shared .gitignore

When you want Git to ignore something that's yours — a local scratch dir, editor cruft, a personal plans/ folder, machine-local config — don't add it to the tracked .gitignore. That file is shared: a rule like plans/ or wiki-drafts/ forces your personal directory conventions onto everyone, and they don't have those dirs.

Put it in .git/info/exclude instead. Same syntax as .gitignore, but it's per-clone and never committed — so it's invisible to teammates and travels with nothing:

# .git/info/exclude
.claude/settings.local.json
.claude/plans/
.claude/wiki-drafts/

The rule of thumb: **.gitignore is for what the project should ignore (build output, node_modules/, *.user); .git/info/exclude is for what you happen to ignore on this machine.** If a teammate cloning fresh wouldn't also produce the file, it doesn't belong in the shared list.

One gotcha with worktrees: info/exclude lives in the common git dir, so it's shared across all linked worktrees of the same clone — but still not across separate clones. Find it with git rev-parse --git-common-dir if you're not sure where it is.

And if you ever need to start tracking something a blanket exclude was hiding (e.g. you had .claude excluded but now want .claude/skills/ committed), narrow the exclude line to the specific paths you still want ignored — don't delete it and shove replacement rules into the shared .gitignore.