OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 001

From the first add to undoing a change.

Walk through a local Git workflow: staging, partial commits, diff inspection, restoration, history rewriting, and ignore rules.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSGit: from snapshots to collaboration and recovery ↗
About 8 min readStart reading

Git commands are easy to confuse when you have not first distinguished which copy of the content they modify. A saved file, an added file, and a committed file occupy three different places. Follow a local workflow from start to finish, and undoing or restoring changes no longer has to be guesswork.

The working tree, staging area, and current commit#

The working tree contains the files your editor is changing. The staging area, also called the index, organizes the next commit. HEAD usually points through the current branch to the latest commit. A commit stores a directory-tree snapshot and its metadata, and records its parent relationships. It does not record each editor operation from opening a file to closing it.

01 / WORKTREEFiles currently being edited
02 / INDEXThe version prepared for commit
03 / HEADThe current commit’s snapshot

For example, change a file from A to B, run add, then change it to C: the working tree contains C, the staging area still contains B, and the current commit still contains A. If you commit now, B is what gets committed. Running add once does not turn the file into a “live stream of pending changes” that continuously tracks subsequent edits.

In terms of object relationships, blobs represent file contents, trees organize paths, and commits point to trees and connect the history. A branch is a movable reference, not a complete copy of a directory. Everyday operations do not require editing these internal objects directly, but this model explains why moving a branch pointer, modifying working-tree files, and deleting historical objects are different actions.

Starting a new repository#

Before the first commit, configure the author identity that will appear in the commit record. The example below sets it for the current repository; add --global only if you actually want it to be the default for all repositories. Replace the example name and email with information you are willing to have appear in commit history.

git init
git config user.name "Example Author"
git config user.email "author@example.com"
git config --get user.name
git config --get user.email
git status

Initializing a repository does not automatically put every file in the directory under version control. New files initially appear as untracked; their contents enter history only after being staged and committed. A tracked file can also contain both staged and unstaged changes at the same time.

add can work on less than a whole file#

git add updates the staging area with the state of the selected content at the time it runs. It can record additions and modifications as well as deletions. The following forms have different scopes and need not be run in sequence: [1]

CommandScope
git add -- notes.mdUpdates staging only for this path; -- separates options from paths
git add .The current directory and its subdirectories; when run in a repository subdirectory, this does not cover the entire repository
git add -uWithout a path, updates modifications and deletions of tracked files across the whole working tree, without adding new files
git add -p -- main.cInteractively selects change hunks in this file

If a file contains both a bug fix and a refactor, committing the entire file ties those two purposes together. add -p lets you select individual hunks: y accepts the current hunk, n skips it, s attempts to split it, and q ends selection. Not every hunk can be split further, and quitting does not undo hunks already accepted.

The following workflow assumes the two kinds of changes can be selected separately. Select only the fix in the first pass, inspect it, and commit; select the refactor in the second pass. Before every commit, read the actual staged content rather than relying on how many times you just pressed y.

git add -p -- main.c
git diff --cached
git commit -m "fix: correct boundary check"

git add -p -- main.c
git diff --cached
git commit -m "refactor: simplify parser"

This is the value of the staging area: it lets you organize a commit around one explainable purpose, without having to include every change in the working directory at once.

Using diff to distinguish the three states#

CommandCompared statesQuestion it answers
git diffWorking tree ↔ staging areaWhich changes have not yet been staged?
git diff --cachedStaging area ↔ HEADWhat is the next ordinary commit about to include?
git diff HEADWorking tree ↔ HEADWhat are all the changes to tracked files relative to the current commit?

An ordinary diff does not show untracked file contents as tracked differences. When checking the scope of a commit, therefore, use git status and diff together: the former lists states, while the latter shows content. Before the first commit, when HEAD does not yet exist, do not blindly reuse every comparison that depends on HEAD.

When viewing existing history, choose the output to suit the question: a compact graph for branch and commit relationships, stat for the extent of file changes, and patch for detailed content. --all means showing history reachable from the various references; it does not promise to recover every commit that once existed but is now unreachable.

git log --oneline --graph --all
git log --stat -5
git log -p -1

commit and amend: creating a record or replacing the current one#

An ordinary git commit creates a new commit from the staging area and moves the current branch forward. A useful commit message explains why the change was made and how behavior changes as a result; “update” provides little help when troubleshooting later.

If you notice an omission just after committing, stage the additional change and use --amend. This creates a new commit that replaces the current tip commit, rather than patching the original object; the commit ID changes. --no-edit means only that the original message is reused, not that the original commit object is reused. [2]

git add -- notes.md
git diff --cached
git commit --amend --no-edit

The example above is appropriate for a tip commit you are still organizing on your own. If other people have already based work on it, rewriting history causes synchronization problems; follow your collaboration conventions and consider adding a corrective commit instead. Also check whether the staging area already contains other content: amend will include that too.

restore: identify the source and destination first#

“Undoing changes” can mean at least two things: keeping file edits while unstaging them, or actually discarding working-tree edits. Different default sources lead to different consequences. The three lines below are independent choices, not a cleanup sequence to run from start to finish. [3]

# 取消暂存:HEAD → 暂存区,保留工作区编辑
git restore --staged -- notes.md

# 丢弃未暂存编辑:暂存区 → 工作区
git restore -- notes.md

# 明确让两处都回到 HEAD:会丢弃这个路径的本地修改
git restore --source=HEAD --staged --worktree -- notes.md

Returning to the A, B, C example: running restore without options returns the working tree to B, not A, because the default source is the staging area. The default source becomes HEAD only when --staged is specified. This is why “restore means returning to the last commit” is not precise enough.

These examples that reference HEAD assume the repository already has a commit. Unstaging a file that is new relative to HEAD makes it untracked again, while leaving the working-tree file in place; unstaging does not mean deleting a file from disk. Before an operation that really overwrites content, use diff to confirm its scope.

reset: moving the tip and deciding which copies move with it#

git reset <mode> <commit> should be distinguished from the form that resets the staging area by path. This section discusses the former: on an ordinary branch, it moves the current branch to the target commit, while the mode determines whether the staging area and working tree are updated too.

ModeCurrent branch / HEADStaging areaWorking tree
--softMoves to the target commitPreservedPreserved
--mixed (default)Moves to the target commitReset to the targetPreserved
--hardMoves to the target commitReset to the targetResets tracked content, discarding local changes

For example, with HEAD~1 as the target, soft is useful for retaining the last commit’s changes as staged content; mixed leaves them in the working tree so you can choose what to stage again. Both require the current commit to actually have a parent. Hard is destructive: it discards local changes to the affected tracked files, and may also overwrite or delete untracked files or directories that obstruct paths it needs to write. It is not, however, a general-purpose command for “removing all untracked files.” [4]

Likewise, git reset --hard origin/main does not automatically fetch the latest content from the remote. It uses the remote-tracking reference already present locally and moves the current branch. Treating it as a casual “quick sync” makes it easy to modify the wrong thing.

After accidentally moving a commit pointer, first search the local records with reflog, then create a recovery branch at the commit you find so it has an explicit reference again. Replace the commit ID here with the actual value you have checked.

git reflog
git branch recovery/saved-tip <commit-sha>

Reflog is not an indefinite backup, nor can it magically restore working-tree edits that were never committed. Garbage-collection policies, elapsed time, and whether the objects still exist all affect recoverability. After noticing a mistake, stop further rewriting and preserve any commits you have found; that is more reliable than repeatedly trying hard resets. [5]

Ignoring files: distinguish shared rules from local ones#

.gitignore can be committed and is suitable for a team’s shared exclusions, such as build artifacts. .git/info/exclude stays in the current clone and suits personal helper files. Their main role is to control the discovery of untracked files; they do not suddenly stop Git from recording changes to files already tracked. [6]

# .gitignore 示例
*.log
/build/
/node_modules/
!important.log

A leading slash anchors a rule to the directory containing that ignore file; a trailing slash denotes a directory. ! introduces an exception, but if an entire parent directory is excluded, a single exception for a child file will not make Git descend into the ignored directory to find it.

If a file does not appear in status, ask the rules themselves why before blindly adding more wildcards:

git check-ignore -v -- docs/development-plan.local.md

When a rule matches, the command reports its source and line number. Ignore rules are not a privacy cleanup tool either: if sensitive content has already entered history, adding an ignore rule later does not remove it from old commits. In the everyday workflow, always check status and inspect the staged diff before recording a clearly scoped snapshot.

References & further reading

  1. Git: Staging Content ↗
  2. Git: Commit and Amend ↗
  3. Git: Restoring Paths ↗
  4. Git: Reset Modes ↗
  5. Git: Local Reference Logs ↗
  6. Git: Ignore Rules ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

Git: from snapshots to collaboration and recovery

  1. 01Git's underlying model: objects, snapshots, and references
  2. 02From the first add to undoing a change.
  3. 03Git branches: merging, conflicts, and saving work in progress
  4. 04Remote collaboration with Git: fetching, tracking, and two remotes
  5. 05Advanced Git: rebasing, cherry-picking, and bisecting
  6. 06A Git scenario handbook: correcting, undoing, and recovering
CONTINUE READING / NEXT ARTICLEThe other side of rwx.

Read a full ls listing to understand numeric permissions, directory access, deletion, special bits, and practical troubleshooting.