Git Workflows and Configuration

Last update: 2025-11-26 10:56:03 +0800 +0800

Git records content and history, while configuration determines how a local installation presents, authenticates, and transforms that content. Separating repository policy from per-developer convenience avoids surprising commits.

Inspect and Scope Configuration #

Git reads configuration from several scopes. Later scopes override earlier values for a scalar key:

ScopeTypical LocationIntended Use
System/etc/gitconfigInstallation-wide defaults
Global~/.gitconfig or ~/.config/git/configDeveloper preferences
Local.git/configOne repository
Worktree.git/config.worktree when enabledOne linked worktree
Commandgit -c name=value ...One invocation

git config --list --show-origin --show-scope shows the source of every effective value. Query a key before changing it, because a value can be inherited from a system or global configuration:

sh
git config --show-origin --get-all user.email
git config --local pull.rebase false
git -c fetch.prune=true fetch

Configuration syntax resembles an INI file. Git accepts many values when a file is written, then validates them when a command consumes the value. Therefore, a successful git config core.eol invalid does not prove that the value is usable. Prefer documented enum values and inspect a change with git config --show-origin --get core.eol.

Put project policy in tracked files such as .gitattributes and .gitignore, not in an untracked local configuration. A clone then receives the policy without setup instructions.

Recover from Common Repository States #

Git keeps unreachable objects for a retention period, so recovery often starts with locating an object rather than accepting data loss. Make a safety branch before a destructive operation:

sh
git branch backup/before-recovery

Recover a Dropped Stash That Contains Untracked Files #

git stash pop applies a stash and then drops its reference when the apply succeeds. A former stash can remain reachable only through an unreachable commit until pruning. Search without reflogs:

sh
git fsck --no-reflogs --unreachable |
  awk '/unreachable commit/ { print $3 }'

Inspect each candidate before applying it:

sh
git stash show --patch --include-untracked <object-id>
git stash apply <object-id>

git stash apply accepts a stash-shaped commit object. Copy recovered files to a safe location or commit them before further cleanup, because git gc can prune unreachable objects after the expiry policy permits it.

Synchronize Deleted Remote Branches #

git fetch adds and advances remote-tracking references but does not remove a local refs/remotes/origin/topic merely because the remote deleted topic. Prune obsolete tracking references explicitly:

sh
git fetch --prune origin
git remote prune origin
git config --global fetch.prune true

Pruning never deletes a local branch. Inspect branches whose upstream was removed with:

sh
git branch -vv

Abort a Conflicted git stash pop #

A conflicted stash pop leaves conflict markers and does not drop the stash. git merge --abort does not apply because stash pop is not a merge operation with merge state. Either resolve the files and commit the result, or discard the partially applied worktree state:

sh
git reset --hard HEAD
git clean -fd    # only after checking which untracked paths may be removed

Use git clean -nd first. reset --hard does not remove untracked files, while git clean -fd can do so.

Retrieve a File from Another Revision #

Read a historical path without changing the index:

sh
git show <revision>:path/from/repository/root

Restore a path into the worktree and index only when that is intended:

sh
git restore --source=<revision> --staged --worktree path/from/repository/root

Use Submodules as Pinned Dependencies #

A submodule entry in a tree has mode 160000 and stores a commit ID, not a copy of the nested repository. .gitmodules records the path and suggested URL; the superproject’s .git/config records local activation and URL overrides.

Add a submodule and commit both the metadata and gitlink:

sh
git submodule add https://example.com/library.git vendor/library
git commit -m "Add library submodule"

Clone a project and initialize the exact commits recorded by the superproject:

sh
git clone --recurse-submodules https://example.com/project.git
# Or, after a plain clone:
git submodule update --init --recursive

To advance a dependency, first check out and commit the intended revision inside the submodule, then commit the changed gitlink in the superproject:

sh
git -C vendor/library fetch --tags origin
git -C vendor/library switch --detach v2.4.0
git add vendor/library
git commit -m "Update library to v2.4.0"

git submodule update --remote follows a configured branch and changes the gitlink; it does not by itself record a reproducible update. Review and commit the new gitlink afterward. Removing a submodule requires removing its .gitmodules entry and gitlink, then deinitializing local metadata:

sh
git submodule deinit -f vendor/library
git rm -f vendor/library
rm -rf .git/modules/vendor/library

Treat a submodule URL as executable supply-chain configuration. Pinning a commit makes a build reproducible only while the referenced object remains available from a trusted remote or a preserved mirror.

Add Repository State to a Bash Prompt #

Git’s contrib/completion/git-prompt.sh defines __git_ps1. Source the copy installed with Git, rather than copying a version from another installation:

sh
if [[ -r /usr/share/git/completion/git-prompt.sh ]]; then
  . /usr/share/git/completion/git-prompt.sh
  GIT_PS1_SHOWDIRTYSTATE=1
  GIT_PS1_SHOWSTASHSTATE=1
  GIT_PS1_SHOWUNTRACKEDFILES=1
  GIT_PS1_SHOWUPSTREAM=auto

  path_color="\[$(tput setaf 2)\]"
  git_color="\[$(tput setaf 207)\]"
  reset_color="\[$(tput sgr0)\]"
  PS1="${path_color}[\u] \w${git_color}"'$(__git_ps1 " <%s>")'"${path_color} \$ ${reset_color}"
  unset path_color git_color reset_color
fi

Common installation paths include /usr/share/git/completion/git-prompt.sh, /usr/local/etc/bash_completion.d/git-prompt.sh, and /opt/homebrew/etc/bash_completion.d/git-prompt.sh.

The \[ and \] markers tell Bash that terminal-control bytes have zero display width. Omitting them breaks cursor placement and wrapping. *, +, %, and $ in the prompt describe unstaged changes, staged changes, untracked files, and stashed changes. Prompt-time status checks can be costly in a large repository or a slow filesystem, so disable state variables when latency matters.

Sign Commits and Tags with OpenPGP #

An OpenPGP signature proves that a holder of a private key signed a specific commit or annotated tag. It neither audits code nor encrypts repository content. Generate or select a signing key:

sh
gpg --full-generate-key
gpg --list-secret-keys --keyid-format=long
gpg --fingerprint <long-key-id>

Configure a fingerprint or long key ID, then enable signing where policy requires it:

sh
git config --global user.signingkey <fingerprint>
git config --global gpg.format openpgp
git config --global commit.gpgsign true
git config --global tag.gpgSign true

Test the key and inspect a signed commit:

sh
printf 'test\n' | gpg --clearsign
git commit --allow-empty -m "Verify commit signing"
git show --show-signature --no-patch HEAD
git verify-commit HEAD
git verify-tag v1.0.0

On macOS, configure an installed pinentry-mac in ~/.gnupg/gpg-agent.conf and restart the agent with gpgconf --kill gpg-agent. A terminal session can also require export GPG_TTY="$(tty)". Upload only an ASCII-armored public key to a hosting service. A verified-hosting badge also requires an account and commit email association that the hosting service recognizes.

Choose a Pull-Request Merge Strategy #

Each strategy changes permanent ancestry, not merely the display of history.

StrategyResult on mainUse WhenLocal Git command equivalent
Merge commitPreserves feature commits and adds a two-parent commitBranch topology and individual commits are usefulgit merge --no-ff feature
Squash and mergeAdds one new commit containing the complete changeA pull request is one logical change or contains fixupsgit merge --squash feature && git commit
Rebase and mergeReplays each feature commit linearly with new IDsEach commit is meaningful and a linear history is requiredgit rebase main feature && git switch main && git merge --ff-only feature
Fast-forward (not supported by Github PR)Moves main to the feature tip without a merge commitA strictly linear history is desired; GitHub’s PR UI does not offer this optiongit merge --ff-only feature

A merge commit preserves the original commits:

text
A---B---C---------M  main
         \       /
          D---E---F  feature

Squashing gives main one new commit after C; D, E, and F are not ancestors of that result. Delete the feature branch after a squash merge, because reusing it produces confusing ancestry.

Rebase and merge produces D', E', and F' after C. The changed parent of each replayed commit changes its ID and invalidates a signature on the original commit. Clean unmerged branches with interactive rebase before integration, and use --force-with-lease rather than --force when a reviewed branch must be updated.

Comments

Read or comment on GitHub (sign-in required to comment)