Git for Production Deployments: Safe Checkout, Atomic Switches, and Rollback Tactics
This guide covers essential Git commands and practices for operations teams, including repository cloning, fetch vs pull, branch/tag management, detached HEAD deployments, atomic symlink-based releases, pre-deployment checks, conflict resolution, revert/reset/restore differences, cherry-pick boundaries, submodules/LFS, worktrees, git archive, bisect debugging, .gitignore limitations, force-push safety, repository corruption recovery, rollback procedures, and audit trails.
Introduction
Online applications often need rollback to a previous version, but the deployment machine may report local uncommitted changes; developers say a fix is merged to main, yet the server still points to an old commit; the same version behaves differently on two machines because one ran git pull directly, causing silent working-tree drift. These issues are not complex but can turn a routine release into an incident.
The goal of using Git in operations is not to memorize as many commands as possible, but to answer four questions: where does the code to be released come from, what is the exact state of the current working tree, what will change, and how to recover to a verified version after failure. When every step carries a commit hash, a diff, and verification evidence, Git becomes an auditable tool in the release pipeline rather than just a code downloader on the server.
This article assumes Git 2.x and a Linux deployment environment. Placeholders like <repo-url>, <commit-hash>, <branch-name>, and <deploy-dir> must be replaced with actual values before execution.
1. Establish Correct Concepts of Version and Working Tree
A Git repository typically contains three states: HEAD: the currently checked-out commit, usually pointing to a local branch, but may be in detached HEAD state.
Staging area (index): content prepared for the next commit.
Working tree: files currently visible on disk.
For deployments, the commit object matters, not the branch name. A branch is a moving reference; main today and tomorrow may point to different commits, while a full commit hash points to immutable content. A production change request should record at least the repository URL, full commit hash, build artifact checksum, and deployment timestamp.
Viewing Basic Repository Facts
git rev-parse --show-toplevel
git status --short --branch
git rev-parse HEAD
git branch --show-current
git remote -v git status --short --branchis suitable for quick state checks, but do not only look at the first line. M = modified, A = added, D = deleted, ?? = untracked. The two columns reflect staging area and working tree respectively; deployment scripts should treat any non-empty status as a risk unless those files are explicitly outside the repo.
Example output:
## main...origin/main [behind 2]
M config/application.yml
?? debug.logInterpretation: local main is two commits behind remote, config file modified, and an untracked log exists. Running pull, checkout, or cleanup now could overwrite the scene; automatic deployment should not proceed.
Full Commit Hash and Timestamp
git show -s --format='commit=%H
author=%an <%ae>
date=%aI
subject=%s' HEAD2. Initial Repository Clone: Clone, Remotes, and Identity Config
Standard Clone
git clone <code><repo-url></code> <code><local-dir></code>
cd <code><local-dir></code>
git remote -vShallow Clone for Limited History
git clone --branch <code><branch-name></code> --single-branch --depth 50 <code><repo-url></code> <code><local-dir></code>Shallow clones suit temporary build environments with no history analysis needs, but have limitations: early commits may not be directly checkoutable, git describe, change-range calculations, and some build tools may lack full history. To deepen later:
git fetch --unshallowVerify Remote on Existing Repo
git remote get-url origin
git remote show originChanging Remote URL (Config Change)
OLD_URL="$(git remote get-url origin)"
printf 'old origin: %s
' "${OLD_URL}"
git remote set-url origin <code><new-repo-url></code>
git remote -vRollback command:
git remote set-url origin "${OLD_URL}"Authentication for Deployment Machines
Deployment machines must not use personal accounts and long-lived passwords. Prefer read-only Deploy Keys, short-lived tokens, or controlled machine identities with restricted repo scope. Private key permissions should be 0600; host fingerprints should be pre-loaded into known_hosts; avoid StrictHostKeyChecking=no in scripts.
Git Commit Identity (Only When Creating Commits/Tags)
git config --local user.name "<code><automation-account-name></code>"
git config --local user.email "<code><automation-account-email></code>"
git config --local --list --show-originUsing --local avoids affecting other repos on the same machine. Pure fetch, checkout, and build usually don't need commit identity config.
3. Daily Information Gathering: status, log, show, diff
1. git status: Confirm No Local Changes
git status
git status --short
git status --porcelain=v1 --porcelain=v1provides a stable, script-parsable format. The following pre-deployment check only accepts a completely clean working tree:
if [[ -n "$(git status --porcelain=v1)" ]]; then
echo "Working tree has uncommitted or untracked content, aborting deployment" >&2
git status --short >&2
exit 1
fiNote: git status being clean only proves the Git-managed working tree has no differences; it does not prove build artifacts, external configs, or database versions are consistent.
2. git log: Confirm Commit Relationships
git log --oneline --decorate --graph -20
git log --first-parent --oneline <code><old-commit></code>..<code><new-commit></code>
git log --author='<code><author-keyword></code>' --since='<code><start-date></code>' --until='<code><end-date></code>' --first-parentshows the merge trajectory on the main branch, hiding internal details of merged branches; reviewing specific changes still requires git show.
3. git show: Inspect What a Commit Actually Changed
git show --stat <code><commit-hash></code>
git show --name-status <code><commit-hash></code>
git show <code><commit-hash></code> -- <code><file-path></code>Release reviews should focus on config templates, DB migrations, dependency lock files, startup scripts, and infrastructure manifests, not just business source code. git show --stat only shows file scope, not a substitute for content review.
4. git diff: Compare Working Tree, Staging Area, and Commits
# Working tree vs staging area
git diff
# Staging area vs HEAD
git diff --cached
# Between two definite commits
git diff --stat <code><old-commit></code> <code><new-commit></code>
git diff <code><old-commit></code> <code><new-commit></code> -- <code><file-path></code>
# Only list change types and filenames
git diff --name-status <code><old-commit></code> <code><new-commit></code>To determine if a change has entered a target branch, fetch remote refs first, then compare:
git fetch --prune origin
git branch --contains <code><commit-hash></code>
git branch --remotes --contains <code><commit-hash></code>4. fetch vs pull: Separate Execution in Deployment Scenarios
git fetchonly updates remote-tracking refs and objects, without automatically modifying the current working tree:
git fetch --prune origin --prunedeletes local remote-tracking refs that no longer exist on the remote; it does not delete local branches. After fetching, examine differences first:
git log --oneline HEAD..origin/<code><branch-name></code>
git diff --stat HEAD origin/<code><branch-name></code> git pullis essentially fetch followed by merge or rebase. While convenient in interactive development, it can introduce implicit merges in production deployments. If the team insists on using it, at least restrict to fast-forward only:
git pull --ff-only origin <code><branch-name></code>Once local and remote diverge, --ff-only fails, forcing the operator to investigate rather than auto-generating a merge commit. For repeatable deployments, a safer approach is fetch followed by checking out an approved tag or full commit hash.
5. Branches, Tags, and Commit Hashes: Choosing Production Versions
1. Branch Operations
git branch --all --verbose
git switch <code><branch-name></code>
git switch -c <code><new-branch-name></code> --track origin/<code><remote-branch-name></code>Older Git versions can use git checkout <code><branch-name>. git switch has clearer semantics, reducing the risk of mixing branch switching with file restoration.
Before deleting a local branch, confirm it is merged and not the current branch:
git branch --merged
git branch -d <code><branch-name></code> git branch -Dforce-deletes unmerged branches, possibly losing the only reference. Before executing, record git rev-parse <code><branch-name> and confirm the commit has been pushed or backed up.
2. Tag Operations
git fetch --tags --prune origin
git tag --list --sort=-version:refname | head -n 20
git show <code><tag-name></code>
git verify-tag <code><signed-tag-name></code>Lightweight tags are just refs; annotated tags store creator, time, and message; signed tags can verify publisher identity. Tags can also be force-updated, so critical releases should record the commit hash resolved from the tag:
git rev-parse '<code><tag-name></code>^{commit}'3. Detached HEAD Is Not an Error
Builds and deployments checking out a fixed commit often use detached HEAD:
git switch --detach <code><commit-hash></code>This means HEAD points directly to a commit, not a movable local branch. For immutable release directories, this is exactly the desired state; do not make temporary modifications and commit in this state, because new commits without a branch or tag reference are easily lost later.
6. Recommended Release Structure: release Directory + Atomic Symlink
Running git pull directly in the live serving directory may let processes read a mix of old and new files for several seconds. A better structure:
<code><app-root></code>/
├── current -> releases/<code><release-id></code>
├── releases/
│ ├── <code><old-release-id></code>/
│ └── <code><new-release-id></code>/
└── shared/
├── config/
└── data/Each deployment completes checkout, build, and verification in a new directory, then atomically switches the current symlink. State data, user uploads, and secrets must not live in the Git working tree; they belong in the backed-up shared directory or dedicated storage.
The following script demonstrates the core "prepare release directory" flow, excluding project-specific build and service reload commands:
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="<code><repo-url></code>"
TARGET_COMMIT="<code><full-commit-hash></code>"
APP_ROOT="<code><app-root-dir></code>"
RELEASE_ID="$(date -u +%Y%m%dT%H%M%SZ)-${TARGET_COMMIT:0:12}"
RELEASE_DIR="${APP_ROOT}/releases/${RELEASE_ID}"
if [[ ! "${TARGET_COMMIT}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "TARGET_COMMIT must be a 40-character full commit hash" >&2
exit 1
fi
install -d -m 0755 "${APP_ROOT}/releases"
# Clone into new directory to avoid modifying the live current directory.
git clone --no-checkout "${REPO_URL}" "${RELEASE_DIR}"
git -C "${RELEASE_DIR}" fetch --prune origin
# Verify commit exists, then checkout as detached HEAD.
git -C "${RELEASE_DIR}" cat-file -e "${TARGET_COMMIT}^{commit}"
git -C "${RELEASE_DIR}" switch --detach "${TARGET_COMMIT}"
ACTUAL_COMMIT="$(git -C "${RELEASE_DIR}" rev-parse HEAD)"
if [[ "${ACTUAL_COMMIT}" != "${TARGET_COMMIT,,}" ]]; then
echo "Checked-out commit does not match target" >&2
exit 1
fi
git -C "${RELEASE_DIR}" status --short
printf 'release_dir=%s
commit=%s
' "${RELEASE_DIR}" "${ACTUAL_COMMIT}"The script uses set -euo pipefail to abort early on unhandled errors, undefined variables, or pipeline failures. Before going live, still run dependency installation, compilation, unit tests, config rendering, and health checks, ensuring these steps fail without switching the symlink.
Switching the Symlink
Record the old target and use a temporary link in the same directory:
APP_ROOT="<code><app-root-dir></code>"
NEW_RELEASE="<code><new-release-absolute-path></code>"
OLD_RELEASE="$(readlink -f "${APP_ROOT}/current" 2>/dev/null || true)"
printf 'old_release=%s
new_release=%s
' "${OLD_RELEASE}" "${NEW_RELEASE}"
test -d "${NEW_RELEASE}"
ln -s "${NEW_RELEASE}" "${APP_ROOT}/.current.new"
mv -Tf "${APP_ROOT}/.current.new" "${APP_ROOT}/current"
readlink -f "${APP_ROOT}/current"Impact: the symlink switch makes subsequent requests read the new version, but whether already-running processes pick up new code depends on the application's loading mechanism. If reload or restart is required, confirm load-balancer drain, connection draining, instance redundancy, and startup time first. Do not assume the service has loaded the new version just because the link switched.
7. Pre-Deployment Checks: Block Risks Before Switching
Repository-level checks can be a standalone script:
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="<code><repo-dir></code>"
TARGET_COMMIT="<code><full-commit-hash></code>"
git -C "${REPO_DIR}" rev-parse --is-inside-work-tree >/dev/null
if [[ -n "$(git -C "${REPO_DIR}" status --porcelain=v1)" ]]; then
echo "Repository not clean, refusing to continue" >&2
git -C "${REPO_DIR}" status --short >&2
exit 1
fi
git -C "${REPO_DIR}" fetch --prune origin
git -C "${REPO_DIR}" cat-file -e "${TARGET_COMMIT}^{commit}"
CURRENT_COMMIT="$(git -C "${REPO_DIR}" rev-parse HEAD)"
echo "Current commit: ${CURRENT_COMMIT}"
echo "Target commit: ${TARGET_COMMIT}"
git -C "${REPO_DIR}" diff --check "${CURRENT_COMMIT}" "${TARGET_COMMIT}"
git -C "${REPO_DIR}" diff --name-status "${CURRENT_COMMIT}" "${TARGET_COMMIT}" git diff --checkcatches trailing whitespace and conflict markers, but it is not a language syntax checker. Config files must be validated with their respective tools, e.g., Nginx with nginx -t, systemd units with systemd-analyze verify <code><unit-file>, shell scripts at minimum with bash -n <code><script-path>, and optionally ShellCheck.
If the commit includes database migrations, forward and backward compatibility must be assessed separately. Code symlinks can roll back in seconds, but database schema and data writes usually cannot be automatically undone.
8. Conflicts and Mistakes: Preserve Evidence First, Then Decide Recovery
1. Local Changes Blocking Switch
First view diffs and save a patch:
git status --short
git diff
git diff --cached
git diff > "../worktree-$(date -u +%Y%m%dT%H%M%SZ).patch"If changes must be temporarily stashed, including untracked files:
git stash push --include-untracked -m "pre-deploy-<code><change-request-id></code>"
git stash list
git stash show --stat stash@{0}Stash remains in the current repo, not a reliable backup. Important scenes should be additionally copied to a controlled location. Before restoring, inspect:
git stash show -p stash@{0}
git stash apply stash@{0}Using apply instead of pop preserves the stash on conflict; confirm restore success before git stash drop stash@{0}.
2. Merge or Rebase Conflicts
git status
git diff --name-only --diff-filter=UIf merge should not continue:
git merge --abortIf rebase should not continue:
git rebase --abortDo not use git reset --hard as a generic "fix conflict" command. It discards uncommitted changes to tracked files and does not analyze whether those changes are production hotfixes.
3. Recovering Recently Lost Commit Refs
git reflog --date=iso
git show <code><commit-from-reflog></code>
git branch recovery/<code><date></code> <code><commit-hash></code> reflogmainly records local ref changes, has a retention period, and does not automatically exist on other machines. After finding the target, immediately create a branch or tag; do not rely on reflog long-term.
9. Choosing Between revert, reset, and restore
These three commands appear to "undo" but have different effects.
git revert: Create a Reverse Commit
For branches already pushed and used by others, revert preserves full history:
git switch <code><branch-name></code>
git pull --ff-only origin <code><branch-name></code>
git revert <code><commit-to-undo></code>
git show --stat HEADReverting a merge commit requires specifying the mainline parent:
git show --no-patch --pretty=raw <code><merge-commit-hash></code>
git revert -m 1 <code><merge-commit-hash></code> -m 1cannot be copied mechanically; it means using the first parent as mainline. Before executing, confirm which parent represents the target branch via commit relationships, otherwise you may revert the wrong content.
git restore: Restore File Contents
# Discard unstaged working-tree changes for a file
git restore -- <code><file-path></code>
# Unstage a file but keep working-tree content
git restore --staged -- <code><file-path></code>
# Restore a file from a specific commit to working tree
git restore --source=<code><commit-hash></code> -- <code><file-path></code>The first command directly overwrites unsaved changes. Run git diff -- <code><file-path> first, save a patch if needed.
git reset: Move Branch Ref and Optionally Change Staging/Working Tree
git reset --soft <code><commit-hash></code>
git reset --mixed <code><commit-hash></code>
git reset --hard <code><commit-hash></code> --softkeeps staging and working tree; --mixed resets staging but keeps working tree; --hard resets both. Moving history on shared branches often leads to force-push, unsuitable for routine production rollback. If absolutely necessary, at least record current commit, create a protection branch, and confirm no untracked files:
git status --short
git rev-parse HEAD
git branch backup/pre-reset-<code><change-request-id></code> HEAD10. Selective Merge of Fixes: cherry-pick Boundaries
To apply a reviewed fix commit to a release branch:
git switch <code><release-branch-name></code>
git pull --ff-only origin <code><release-branch-name></code>
git cherry-pick -x <code><fix-commit-hash></code> -xrecords the original commit hash in the new commit message for audit. On conflict, check status, resolve, then:
git status
git add <code><resolved-file-path></code>
git cherry-pick --continueTo abort:
git cherry-pick --abort cherry-pickonly transplants the patch from that commit; it does not automatically guarantee its dependent commits, configs, DB migrations, and interface versions are complete. Before merging, check:
git show --stat <code><fix-commit-hash></code>
git log --oneline --reverse <code><base-commit></code>..<code><fix-commit-hash></code>11. Submodules and Git LFS: Common "Code Present but Files Wrong"
When a repo contains submodules, the main repo only records submodule commit pointers. After cloning, initialize per repo convention:
git submodule status --recursive
git submodule update --init --recursive
git submodule foreach --recursive 'git status --short --branch'Do not run aimless git pull on submodules on the deployment machine; that would drift content away from the commit recorded by the main repo.
For repos using Git LFS, ordinary Git objects may contain only pointer files:
git lfs version
git lfs env
git lfs pull
git lfs ls-filesGit LFS is an extension requiring pre-installation. Before build, spot-check that large files are not still pointer text starting with version https://git-lfs.github.com/spec/v1.
12. Using worktree to Prepare Multiple Versions in Parallel
A single Git working directory can only check out one version at a time. If release scripts switch branches in the current directory, ongoing builds, manual investigations, and next-release preparations interfere. git worktree lets one repo's object database serve multiple independent working directories, suitable for parallel preparation of release, rollback, and hotfix versions on the same build machine.
First update objects in the main repo, then create a fixed-commit worktree:
REPO_DIR="<code><main-repo-dir></code>"
TARGET_COMMIT="<code><full-commit-hash></code>"
WORKTREE_DIR="<code><new-worktree-dir></code>"
git -C "${REPO_DIR}" fetch --prune origin
git -C "${REPO_DIR}" cat-file -e "${TARGET_COMMIT}^{commit}"
git -C "${REPO_DIR}" worktree add --detach "${WORKTREE_DIR}" "${TARGET_COMMIT}"
git -C "${WORKTREE_DIR}" status --short --branch
git -C "${WORKTREE_DIR}" rev-parse HEAD --detachavoids multiple worktrees checking out the same local branch. Each worktree has its own HEAD, staging area, and working tree, but shares objects and refs; thus deleting objects, running aggressive cleanup, or modifying public refs can still affect other worktrees.
List Worktree Registrations
git -C "${REPO_DIR}" worktree list --porcelainAfter build, confirm the directory is not the current production symlink target, no processes are using it, and artifacts are archived. For dry-run checks only:
readlink -f <code><app-root></code>/current
git -C "${WORKTREE_DIR}" status --short
git -C "${REPO_DIR}" worktree listRemoving a Worktree
git -C "${REPO_DIR}" worktree remove "${WORKTREE_DIR}"
git -C "${REPO_DIR}" worktree prune --dry-run worktree removedeletes the corresponding working directory, a high-risk cleanup. If the directory has uncommitted changes, Git usually refuses; do not add --force for convenience. Save patches or move files first, then delete per checklist. worktree prune --dry-run only shows stale registrations eligible for cleanup; confirm before running without --dry-run. Rollback is recreating the worktree at the original commit; uncommitted files are only recoverable if backed up.
13. From Source to Artifact: Avoid Shipping .git and Working-Tree Files
Production runtime directories usually don't need .git metadata. Copying the entire repo to the web root may leak .git/config, refs, or history objects due to misconfigured web servers. A safer approach: check out source in a controlled build environment, generate an artifact containing only runtime necessities, compute its checksum, and deploy.
For projects needing no compilation and having no submodules or LFS special handling, git archive can export a specific commit:
REPO_DIR="<code><repo-dir></code>"
TARGET_COMMIT="<code><full-commit-hash></code>"
OUTPUT_FILE="<code><artifact-dir></code>/source-${TARGET_COMMIT}.tar.gz"
git -C "${REPO_DIR}" cat-file -e "${TARGET_COMMIT}^{commit}"
git -C "${REPO_DIR}" archive \
--format=tar.gz \
--output="${OUTPUT_FILE}" \
"${TARGET_COMMIT}"
sha256sum "${OUTPUT_FILE}" > "${OUTPUT_FILE}.sha256" git archiveexports only files recorded in the commit, excluding untracked files, working-tree modifications, and the .git directory. This is both an advantage and a limitation: submodule contents are not auto-embedded, LFS object export depends on LFS filters and environment, and build-generated files won't appear. Must perform an unpack validation against the repo before use.
Unpack to Temporary Directory and Verify
#!/usr/bin/env bash
set -euo pipefail
ARCHIVE_FILE="<code><artifact-archive></code>"
CHECKSUM_FILE="${ARCHIVE_FILE}.sha256"
VERIFY_DIR="$(mktemp -d)"
trap 'rm -rf "${VERIFY_DIR}"' EXIT
sha256sum --check "${CHECKSUM_FILE}"
tar -tzf "${ARCHIVE_FILE}" > "${VERIFY_DIR}/archive-list.txt"
sed -n '1,50p' "${VERIFY_DIR}/archive-list.txt"
tar -xzf "${ARCHIVE_FILE}" -C "${VERIFY_DIR}"
test -f "${VERIFY_DIR}/<code><critical-file-path></code>"
echo "Artifact unpack check passed: ${VERIFY_DIR}" tarunpacking untrusted archives carries path-traversal and overwrite risks. Production pipelines should only accept artifacts from trusted build systems with matching checksums, unpacked in isolated temporary directories. The script deletes the temp dir on exit; to preserve the scene for investigation, remove the trap and manage the directory manually.
Build Artifact Metadata
Build artifacts should also record:
Full source commit hash and repo URL.
Build image digest or build-machine environment version.
Dependency lock files and download sources.
Compilation flags, feature toggles, and target architecture.
Artifact SHA-256.
SBOM, signatures, and vulnerability scan results (if org process enables).
Deployment machines must verify artifact signatures or checksums; do not modify source on the server and continue running. If emergency fix is needed, go back to the branch, create a reviewable commit, and run the same build pipeline to produce a new version.
14. Locating Regression Commits: Using bisect to Narrow Range
When a known <good-commit> works and <bad-commit> fails, with many commits in between, git bisect uses binary search to select commits for testing. It does not automatically know "good" vs "bad"; judgment must come from a repeatable test.
Manual Process
git bisect start
git bisect bad <code><bad-commit></code>
git bisect good <code><good-commit></code>
# Run test on the commit Git checks out, then mark:
git bisect good
git bisect bad
# After finish, restore the branch/commit from before start
git bisect resetIf an intermediate commit cannot build due to missing dependencies, use:
git bisect skipAutomated Test Script Exit Codes
Automation scripts must use exit codes: 0 = good, 1 - 127 (except 125) = bad, 125 = current commit untestable. Example:
#!/usr/bin/env bash
set -euo pipefail
./<code><project-build-command></code> || exit 125
./<code><regression-test-command></code>Run Auto Bisect
git bisect start <code><bad-commit></code> <code><good-commit></code>
git bisect run <code><test-script-path></code>
git bisect log
git bisect resetDo not run bisect tests that modify data directly on production servers. Use isolated environments, test databases, and repeatable inputs. Tests depending on flaky external services may misjudge intermittent failures as regression commits. After finding a candidate, examine the commit diff, re-verify on both good and bad ends, and prove the fix eliminates the symptom.
15. .gitignore Is Not a Config Isolation Solution
.gitignoreonly affects whether untracked files are shown or added by default; it cannot stop tracking already-tracked files. The following commands determine which rule ignores a file:
git check-ignore -v <code><file-path></code>
git ls-files --error-unmatch <code><file-path></code>If the second command succeeds, the file is tracked; even if later added to .gitignore, modifications still appear in git status. To stop tracking but keep the local file, do on a dev branch after review:
git rm --cached -- <code><file-path></code>
git status --short
git diff --cached -- <code><file-path></code>After committing this change, other environments updating may delete the tracked copy in their working tree; migrate configs and verify startup first. Do not execute on production repo without committing.
Some hide local config diffs with:
git update-index --skip-worktree <code><file-path></code>
git update-index --assume-unchanged <code><file-path></code>These index flags are not secrets management or deployment config solutions. They easily let operators forget local diffs and cause hard-to-understand behavior on version switches. Check existing flags:
git ls-files -v | grep -E '^[a-zS]'Unset skip-worktree:
git update-index --no-skip-worktree <code><file-path></code>Unset assume-unchanged:
git update-index --no-assume-unchanged <code><file-path></code>A better structure: commit secret-free config templates, place environment config in a config center, secrets system, or release platform, and render at build or startup. Release records should save config version or hash, but never write secret values into logs.
If secrets were ever committed to Git, deleting from the latest commit is insufficient; history may still contain them. Immediately rotate secrets, restrict repo access, assess history rewrite impact, then use team-approved cleanup. History rewriting changes many commit hashes and all clones; must coordinate all users, not done unilaterally by a deployment machine.
16. Remote Push, Force Updates, and Protected Branches
Deployment accounts should be read-only. Only tag-publishing or auto-version-write workflows need write permission, constrained by protected branches, signing, and CI checks.
Normal Push
git push origin <code><local-branch></code>:<code><remote-branch></code>
git push origin <code><tag-name></code>Confirm Commits to Be Sent Before Push
git fetch origin
git log --oneline origin/<code><remote-branch></code>..<code><local-branch></code>
git diff --stat origin/<code><remote-branch></code> <code><local-branch></code> git push --forcemay overwrite commits others have pushed; not a routine fix for "push rejected". If approved to rewrite a personal or temporary branch, use lease-protected force:
git fetch origin
git push --force-with-lease origin <code><local-branch></code>:<code><remote-branch></code> --force-with-leaserefuses if the remote ref differs from local expectation, but it is not a permission control; wrong local remote ref or bad params can still cause risk. Impact includes all downstream clones, builds, and release records based on old history. Before executing, save remote current commit, create protection tag or backup ref, notify collaborators; after, verify remote commit graph and CI. Rollback requires pushing the saved old commit back and re-coordinating all downstreams.
Critical branches should have server-side protection: no direct push, no force push, require merge requests, required status checks, minimum reviewers, and signing policy. Client-side aliases cannot replace server-side protection.
17. Repository Anomalies and Object Integrity Checks
On bad object, object file is empty, index file corrupt, or unreadable commits, stop building in that repo, save errors and disk state:
git status
git rev-parse HEAD
git fsck --full
df -h <code><repo-dir></code>
df -i <code><repo-dir></code>
dmesg --level=err,warn | tail -n 100Disk full, inode exhaustion, storage I/O errors, process crashes, and faulty sync tools can corrupt repos. git fsck dangling commits are not necessarily corruption; they may be temporarily unreferenced history objects. Focus on missing, corrupt, and unreadable objects.
Deployment machine repos are usually regenerable caches. Safest recovery: keep the faulty directory, re-clone from trusted remote to a new directory, then verify against target commit:
mv <code><faulty-repo-dir></code> <code><faulty-repo-kept-dir></code>
git clone <code><repo-url></code> <code><new-repo-dir></code>
git -C <code><new-repo-dir></code> fetch --tags --prune origin
git -C <code><new-repo-dir></code> cat-file -e '<code><target-commit></code>^{commit}'
git -C <code><new-repo-dir></code> fsck --fullMoving the directory affects build tasks referencing that path; stop or isolate related tasks first, record processes and disk space, and ensure capacity for two repos simultaneously. Rollback can switch path back to kept directory, but if it is truly corrupt, it's only for forensics, not for further deployment.
Do not copy .git/objects from another unknown working directory to "patch files". If local-only commits must be recovered, first confirm full commit and objects from reflog, backup, or other trusted clone, then create an explicit ref.
18. Rollback: Restore the Entire Release Unit, Not Just Source
Before rollback, clarify impact scope: instance count, in-flight requests, background jobs, message consumers, DB migrations, and cache formats. Save current version info, health status, and key logs; confirm old version can still read data written by new version.
Symlink-Based Rollback Example
#!/usr/bin/env bash
set -euo pipefail
APP_ROOT="<code><app-root-dir></code>"
ROLLBACK_RELEASE="<code><verified-old-release-absolute-path></code>"
test -d "${ROLLBACK_RELEASE}"
test -d "${ROLLBACK_RELEASE}/.git"
CURRENT_RELEASE="$(readlink -f "${APP_ROOT}/current")"
CURRENT_COMMIT="$(git -C "${CURRENT_RELEASE}" rev-parse HEAD)"
ROLLBACK_COMMIT="$(git -C "${ROLLBACK_RELEASE}" rev-parse HEAD)"
printf 'current=%s (%s)
rollback=%s (%s)
' \
"${CURRENT_RELEASE}" "${CURRENT_COMMIT}" \
"${ROLLBACK_RELEASE}" "${ROLLBACK_COMMIT}"
ln -s "${ROLLBACK_RELEASE}" "${APP_ROOT}/.current.rollback"
mv -Tf "${APP_ROOT}/.current.rollback" "${APP_ROOT}/current"
readlink -f "${APP_ROOT}/current"After execution, reload or rolling-restart per application, and verify:
curl --fail --silent --show-error <code><health-check-url></code>
readlink -f <code><app-root></code>/current
git -C "$(readlink -f <code><app-root></code>/current)" rev-parse HEADIf the app exposes /version, also verify the returned commit hash. HTTP 200 alone does not prove critical dependencies, read/write paths, and background tasks are healthy.
Rollback itself may fail. If old version is incompatible with new DB schema, stop increasing traffic, restore compatible schema per migration plan, or forward-fix instead of repeatedly switching code.
19. Post-Release Cleanup and Routine Maintenance
Deleting old release directories is high-risk; may remove the only quickly-rollbackable version. Conditions:
Current version stable and past observation window.
At least several verified versions retained.
Data, configs, and secrets not in target directory.
Target directory not the current symlink target.
Change request records deletion list, manual review or dry-run first.
Only list candidates, do not delete directly:
APP_ROOT="<code><app-root-dir></code>"
CURRENT="$(readlink -f "${APP_ROOT}/current")"
printf 'current=%s
' "${CURRENT}"
find "${APP_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p
' | sort -nAfter confirming deletion targets, delete one by one with absolute paths, and re-compare readlink -f before each rm -rf. Do not interpolate unverified variables into rm -rf. After deletion, verify symlink, process working directories, and health checks; if needed, restore from backup or rebuild old version from commit hash.
Repo object maintenance is usually automatic. Before manual runs, observe:
git count-objects -vH
git fsck --full git gcrepacks objects, may consume CPU, I/O, and temporary disk space; run on production repos during low-traffic, confirm free space and backups first. Deployment machines using fresh-clone immutable directories rarely need frequent manual maintenance.
20. Cement Git Commands into Deployment Evidence
An auditable deployment should at least leave:
git remote get-url origin
git rev-parse HEAD
git show -s --format='%H %aI %an %s' HEAD
git status --porcelain=v1
git diff --name-status <code><old-commit></code> <code><new-commit></code>Also record build tool version, dependency lock file hash, artifact hash, config version, DB migration number, and health check results. Source commit hash only proves input code version; it cannot alone prove the final running artifact is identical.
Conclusion
Core Git capabilities in ops scenarios boil down to three points: use status, log, show, and diff to see facts; use fetch plus fixed commit for deterministic deployment; use immutable release directories and explicit rollback paths to control change risk. reset --hard, force push, and in-place pull on live directories are not forbidden, but they alter or discard state and must not become default actions without evidence, backup, and approval.
Change "branch-name deployment" to "commit-hash deployment", change "overwrite update" to "new directory verified then switch", and write current commit, diff, verification, and rollback into the release record — only then does Git become a true part of a stable release system.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
