How common Git commands work and what they affect
Git is a widely used version control system, and its commands cover everything from saving a single change to rewriting the history of an entire repository. Each command acts on a specific part of the repository, and choosing one without knowing what it changes can rewrite shared history or discard work that was never saved anywhere else.
Four places matter when reading the sections below. The working directory holds the files on disk. The staging area (also called the index) holds the changes selected for the next commit. The local history is the chain of commits in the repository. The remote repository, usually called origin, is the copy hosted on a server. Every command in this article touches one or more of those places, and knowing which ones makes the outcome predictable.
Add vs Commit
git add copies the current content of files into the staging area, which is the list of changes selected for the next commit. git commit takes what is in the staging area and records it as a new commit in the history. A commit never reads the working directory directly, only the staging area, and that detail becomes visible when a file is edited after being staged:
echo "Release notes" >> notes.md
git add notes.md # stages the current content of the file
echo "Draft section" >> notes.md # edited after the add, this line is not staged
git status -s # AM notes.md (A: staged as new, M: modified again afterwards)
git commit -m "Add release notes" # records only "Release notes", the draft line stays uncommitted
The staging area also allows splitting a working directory with several unrelated edits into separate commits:
git add -p # asks for each changed block if it should be staged
git commit -m "Fix token expiration"
git add src/login.php
git commit -m "Add login form validation"
git restore --staged src/login.php # removes a file from the staging area, the file itself is kept
git commit -a -m "Update docs" # stages all modified tracked files and commits, untracked files are excluded
Until git commit runs, nothing that was staged is part of the history, so a staged change only prepares the next commit and can still be modified or removed. Once a commit exists, it is identified by a hash and can be pushed, reverted or recovered from the reflog. Running git status between both commands shows exactly what the next commit is going to contain.
Commit vs Stash
Both commands save changes, but in different places. git commit records the changes as a permanent entry in the history of the current branch. git stash puts them in a separate stack that does not belong to any branch, and restores a clean working directory. The usual scenario for a stash is unfinished work on a feature branch when an urgent fix is needed somewhere else:
git stash push -m "login form draft" # saves staged and unstaged changes of tracked files
git switch main # the working directory is clean, switching is possible
# ... fix the problem, commit and push ...
git switch feature
git stash list # stash@{0}: On feature: login form draft
git stash pop # reapplies the changes and removes the entry from the stack
Several stashes can exist at the same time. Each git stash push adds a new entry on top of the stack, and the entries are numbered from the newest (stash@{0}) to the oldest. Any entry can be inspected and restored by its name:
git stash push -m "login form draft"
git stash push -m "api timeout experiment"
git stash list
# stash@{0}: On feature: api timeout experiment
# stash@{1}: On feature: login form draft
git stash show -p stash@{1} # shows the changes stored in that entry without applying them
git stash apply stash@{1} # restores that entry and keeps it in the stack
git stash pop stash@{1} # restores that entry and removes it from the stack
git stash branch fix-login stash@{1} # creates a branch from the commit where the entry was made and applies it
git stash drop stash@{0} # deletes one entry without applying it
git stash clear # deletes every entry
When no name is given, git stash pop and git stash apply use stash@{0}, the newest entry.
A stash is local to the repository where it was created and is never sent to the remote, so it works as a temporary shelf and not as a backup. Entries are not tied to the branch where they were created, and applying one on a different branch can produce conflicts.
Status vs Diff
git status reports the state of the repository: the current branch, the files that are staged, the files that are modified but not staged, and the untracked files. It lists file names and their state. git diff shows the content of the changes, line by line. Neither of them modifies anything.
git status
# On branch feature
# Changes to be committed:
# modified: src/auth.php
# Changes not staged for commit:
# modified: src/login.php
# Untracked files:
# notes.txt
The output tells which files changed and in which area they are, and git diff shows what changed inside them:
git diff # unstaged changes: working directory against the staging area
git diff --staged # staged changes: staging area against the last commit
git diff HEAD # all changes to tracked files since the last commit
git diff main feature # changes between the tips of two branches
@@ -12,7 +12,7 @@ function login($request)
- $token = null;
+ $token = $this->createToken($user);
Both commands are read-only, so they can be run as often as needed. git status answers where things stand before a commit, a switch of branch or a pull, and git diff is used to review the exact lines that a commit is going to contain. A file that appears under staged changes in git status is not shown by a plain git diff, which only reads the unstaged changes, and this detail explains many cases where the diff looks empty while the status reports modified files.
Merge vs Rebase
Both commands bring the work of one branch into another. The difference is the shape of the history afterwards. Take a feature branch that was created from main, while main kept receiving commits:
A---B---C feature
/
D---E---F main
git merge keeps both lines of work and ties them together with a new commit that has two parents:
git switch main
git merge feature # creates merge commit M, existing commits keep their hashes
A---B---C
/ \\
D---E---F-------M main
git rebase takes the commits of feature and replays them on top of the current tip of main. The replayed commits are new commits with new hashes, marked here as A', B' and C':
git switch feature
git rebase main # replays A, B and C on top of F
git switch main
git merge feature # main can now fast-forward, no merge commit is needed
D---E---F---A'---B'---C' main, feature
A merge produces a branching history in which every existing commit keeps its hash, while a rebase produces a linear history in which the replayed commits are new ones with new hashes.
The two commands also handle conflicts in a different way. A merge combines both branches in one step, so conflicting changes are resolved a single time and the result is stored in the merge commit. A rebase replays the commits one by one, so the same area can conflict in several of them, and each stop needs its own resolution followed by git rebase --continue. The command git rebase --abort cancels the rebase and returns the branch to the state it had before it started.
A rebase replaces commits instead of adding new ones next to them. After rebasing feature, the local branch contains A', B' and C', and the original A, B and C no longer belong to it. If feature was already pushed, the remote still holds A, B and C, so a regular git push is rejected because both histories do not match. The push has to be forced with git push --force-with-lease origin feature, which replaces the commits on the remote and refuses to do it when it finds commits there that were never fetched. Anyone who already pulled A, B and C keeps commits that no longer exist on the branch, and has to reconcile their copy.
Fetch vs Pull
git fetch downloads new commits, branches and tags from the remote and updates the remote-tracking branches, such as origin/main. Local branches and the working directory are not touched. git pull runs a fetch and then integrates the downloaded commits into the current branch, using a merge by default or a rebase when requested.
git fetch origin # updates origin/main, local main stays where it was
git diff main origin/main # content of the differences between both branches
git merge origin/main # integrates the remote commits when the time comes
The origin/main branch is a remote-tracking branch. It is a read-only reference stored in the local repository that records where main was on the remote the last time Git contacted it. When the table below says that a fetch updates origin/main, it means that Git downloads the missing commits and moves this reference to the latest remote commit, while the local main branch stays where it was until the commits are integrated.
There are two ways to integrate them. When the local branch has no commits of its own, the remote commits sit directly ahead of it and Git can perform a fast-forward, which only moves the branch pointer forward and creates no merge commit:
Before: A---B---C---D---E
| |
main origin/main
After: A---B---C---D---E
|
main, origin/main
When the local branch has a commit that the remote does not have, the histories have diverged and a fast-forward is not possible. In that case Git needs a merge commit or a rebase to combine both lines of work.
The fetch and the integration can also be performed in a single command with a pull:
git pull origin main # fetch followed by a merge into the current branch
git pull --rebase origin main # fetch followed by a rebase of the local commits
git pull --ff-only origin main # fetch followed by a fast-forward, stops if one is not possible
Because a fetch never modifies local branches or files, it can be run at any moment, even in the middle of unfinished work, and the results can be inspected before deciding what to do with them. A pull changes the current branch right away. Uncommitted edits that overlap with incoming changes can block it, and a merge or rebase can stop on conflicts that need to be resolved. When local and remote commits have diverged, recent Git versions ask for the strategy to be chosen, either per command with --rebase, --no-rebase or --ff-only, or once with a setting such as git config --global pull.rebase true.
Commit vs Cherry-pick
git cherry-pick applies the changes of selected commits to the current branch as new commits. git merge brings in every commit of another branch that the current one does not have. Take a release branch that needs one fix, which exists as commit B on feature, next to commits A and C that are not ready:
A---B---C feature
/
D---E---F release
git switch release
git cherry-pick 4e7a1c9 # 4e7a1c9 is the hash of B, creates B' on release with a new hash
A---B---C feature
/
D---E---F---B' release
A merge of feature into release would have brought A, B and C, keeping their original hashes. A few options adjust how a cherry-pick behaves:
git cherry-pick -x 4e7a1c9 # appends "(cherry picked from commit 4e7a1c9...)" to the message
git cherry-pick A..C # picks the commits after A up to C, A itself is excluded
git cherry-pick -n 4e7a1c9 # applies the changes without creating the commit
git cherry-pick --continue # resumes after resolving a conflict (--abort cancels it)
Since B' is a copy and not the original commit, Git does not know that both are related. When feature is merged into release later, B is brought in again. Identical changes usually apply without trouble, but if the surrounding code was modified in between, conflicts can appear, and the history shows two commits with the same message. The -x option leaves a trace of the original hash in the message, which helps to follow the origin of a copied fix.
Reset vs Revert
Both commands undo commits, but they act on the history in different ways:
git revertadds a new commit that cancels the changes of an earlier onegit resetmoves the branch pointer back, so later commits stop being part of the branch
Take this history, where commit C introduced a bug:
A---B---C---D main
git revert keeps C and D in place and records the undo as a new commit:
git revert 3f2a9c1 # 3f2a9c1 is the hash of C, creates C' with the opposite changes
A---B---C---D---C' main
git reset moves main back to B, and C and D are no longer reachable from the branch:
git reset --hard 9b1c4d2 # 9b1c4d2 is the hash of B
A---B main
Reset accepts three modes, which decide how far the undo reaches:
Mode | Branch pointer | Staging area | Working directory |
|---|---|---|---|
| Moved | Unchanged | Unchanged |
| Moved | Reset to the target commit | Unchanged |
| Moved | Reset to the target commit | Reset, changes to tracked files are discarded |
git reset --soft HEAD~1 # last commit is undone, its changes stay staged
git reset HEAD~1 # last commit is undone, its changes stay unstaged the files
git reset --hard HEAD~1 # last commit is undone and its changes are removed from files
The implication of git revert is that history only grows. Nothing is rewritten, so the result can be pushed with a regular git push, which makes it the command used for commits that other people already have.
Reverting a merge commit needs one extra piece of information. A merge commit has two parents: the first parent is the branch where the merge was made, and the second parent is the branch that was merged in. Git cannot guess which side should be kept, so the parent is chosen with -m, followed by its number:
A---B feature
/ \\
D---E---F---M main
git revert -m 1 7c3e5b8 # 7c3e5b8 is the hash of M, -m 1 selects F (the main line) as the parent to keep
In this example M has two parents: F as the first parent and B as the second. With -m 1, the revert commit brings main back to the state of F, which removes the changes that came from A and B. Using -m 2 would treat the feature side as the line to keep.
git reset rewrites history. On a pushed branch the remote rejects the result until it is forced, and teammates who already pulled the removed commits still have them. The --hard mode discards uncommitted changes to tracked files, and those cannot be recovered since they were never committed. Commits removed by a reset stay recoverable for a limited time, because git reflog lists where the branch pointed before and git reset --hard <hash> can move it back. Untracked files are left untouched by all three modes.
Conclusion
Git usually offers several routes to the same goal, and each route leaves something different behind. Some keep the history exactly as it happened, some reshape it into a straight line, and some remove work for good. The right one depends on the project, the team and the moment, and the constant in every case is knowing what a command changes before running it.
0 Comments