Operations 56 min read

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.

Raymond Ops
Raymond Ops
Raymond Ops
Git for Production Deployments: Safe Checkout, Atomic Switches, and Rollback Tactics

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 --branch

is 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.log

Interpretation: 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' HEAD

2. 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 -v

Shallow 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 --unshallow

Verify Remote on Existing Repo

git remote get-url origin
git remote show origin

Changing 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 -v

Rollback 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-origin

Using --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=v1

provides 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
fi

Note: 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-parent

shows 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 fetch

only updates remote-tracking refs and objects, without automatically modifying the current working tree:

git fetch --prune origin
--prune

deletes 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 pull

is 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 -D

force-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 --check

catches 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=U

If merge should not continue:

git merge --abort

If rebase should not continue:

git rebase --abort

Do 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>
reflog

mainly 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 HEAD

Reverting 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 1

cannot 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>
--soft

keeps 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> HEAD

10. 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>
-x

records 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 --continue

To abort:

git cherry-pick --abort
cherry-pick

only 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-files

Git 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
--detach

avoids 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 --porcelain

After 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 list

Removing a Worktree

git -C "${REPO_DIR}" worktree remove "${WORKTREE_DIR}"
git -C "${REPO_DIR}" worktree prune --dry-run
worktree remove

deletes 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 archive

exports 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}"
tar

unpacking 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 reset

If an intermediate commit cannot build due to missing dependencies, use:

git bisect skip

Automated 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 reset

Do 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

.gitignore

only 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 --force

may 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-lease

refuses 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 100

Disk 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 --full

Moving 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 HEAD

If 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 -n

After 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 gc

repacks 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CI/CDOperationsdeploymentgitversion-controlrollbackbash-scriptingatomic-deployment
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.