Git Commits, Branches, Remotes, and Safe Undo
Introduction
Git commits, branches, and remotes let a team record and share changes deliberately. This lesson aligns with Meta’s "Version Control" course and uses Git to record the history of a project, work on ideas in isolation with branches, and share work through remotes such as GitHub. Git is the standard version-control system in front-end development. Every React project you build in the next chapters will live in a Git repository, and almost every team will ask you to use it from your first day.
Version control answers questions that every developer eventually asks: "What changed since yesterday?", "Who changed this line and why?", "Can I go back to the version that worked?", and "How can two people edit the same project without overwriting each other?" This lesson teaches the day-to-day commands and, more importantly, the mental model that makes them predictable.
The practice scenario is original: you will put the Harbor Workshops project under version control, make a series of commits, create a feature branch for a seat-warning feature, and connect the repository to a remote.
Learning objectives
- Explain what a repository, commit, and branch are, and what makes Git distributed.
- Describe the three areas of Git: working directory, staging area, and repository.
- Configure Git and create a repository with
git initorgit clone. - Use
git status,git add,git commit,git diff, andgit logfluently. - Write useful commit messages and use
.gitignorecorrectly. - Create, switch, merge, and delete branches.
- Undo mistakes safely with
git restore,git revert, andgit reset, and choose the right one. - Use
git remote,git push,git fetch, andgit pull, and explain the difference between them.
1. The mental model
Git records snapshots of your project. Each snapshot is a commit: it stores the state of all tracked files at a moment, a message, an author, a timestamp, and parent references (none for a root commit, usually one, or multiple for a merge), plus committer metadata. Following the parent pointers gives you the history.
Git is distributed: a normal full clone keeps repository history locally, supporting commits, logs, and branches offline. Shallow clones limit history, and partial clones may fetch missing objects on demand, so not every clone contains every object. A remote such as GitHub is just another copy that the team agrees to treat as the shared one.
The three areas
Files move through three areas, and nearly every beginner mistake comes from forgetting which area a change is in.
| Area | What it is | Commands that move changes |
|---|---|---|
| Working directory | The files you edit on disk | You edit here |
| Staging area (index) | A draft of the next commit | git add copies selected content into the index; git restore --staged normally resets it from HEAD |
| Repository | The saved history of commits | git commit records the staged draft |
The staging area lets you build a commit deliberately. If you fixed a bug and also reformatted a file, you can stage and commit them separately so each commit has one purpose.
An untracked file has no index entry yet. A tracked file can have both staged changes and further unstaged edits at once: git add records its content at that moment, not future edits. Read status as two comparisons, HEAD versus index and index versus working tree. Git does not record an empty directory by itself.
Git is not GitHub
Git is the version-control tool installed on your machine. GitHub (like GitLab and Bitbucket) is a hosting service for Git repositories with collaboration features such as pull requests and issues. Git works locally or with other hosts. GitHub adds a website and collaboration services; a contributor can also use its web interface without running a local Git command.
Continue with shell navigation and project paths for related practice.
2. First-time setup
Configure commit attribution; it is distinct from authentication. Choose an email appropriate for the repository, such as your GitHub-provided no-reply address if desired.
git config --global user.name "Amira Haddad"
git config --global user.email "amira@example.com"
git config --global init.defaultBranch main
git config --get user.name
git config --get user.emailinit.defaultBranch main makes new repositories start with a branch named main, the current convention on GitHub. Without an explicit choice, the initial name depends on Git’s version and configuration; do not assume every machine has the same default. The name is only a convention; the history is identical.
Do not use the global config for credentials. Authentication to GitHub uses a Personal Access Token over HTTPS, or an SSH key, or a credential helper.
3. Creating a repository and the basic cycle
cd ~/projects/harbor-workshops
git init
# Initialized empty Git repository in .../harbor-workshops/.git/
git statusIn this ordinary non-bare repository, git init creates a .git directory for local metadata and objects. Never edit it by hand, and never delete it unless you intend to destroy the history.
git status is the command you will use most. Read it before and after every other command.
git status
# On branch main
# No commits yet
# Untracked files (excerpt):
# (use "git add <file>..." to include in what will be committed)
# README.md
# src/Stage and commit
git add README.md # stage one file
git add src/ # stage a folder
git add . # stage everything in this folder (check status first!)
git status
# Changes to be committed (excerpt):
# new file: README.md
# new file: src/App.jsx
git commit -m "Add project README and App skeleton"
# [main (root-commit) 3f2a1c9] Add project README and App skeletongit add . is convenient but dangerous when you have not checked what it will include: it will stage anything not ignored, including accidental large files or secrets. Run git status first, or use git add -p to review changes piece by piece.
Inspect changes
git diff # working directory vs staging area (unstaged changes)
git diff --staged # staging area vs last commit (what will be committed)
git log # history reachable from the current branch
git log --oneline # one line per commit
git log --oneline --graph --all # branch structure as a text graph
git show HEAD # details of the current commit
git blame src/App.jsx # who last changed each lineThese example hashes are illustrative; use the hashes in your own log. A commit is identified by a hash (for example 3f2a1c9). Use a unique abbreviated hash from your own repository. Angle-bracket names such as <hash> elsewhere in this lesson are placeholders to replace before running a command.
git diff does not show the contents of new untracked files. Use git status to notice them, then inspect or deliberately stage them before reviewing git diff --staged.
Good commit messages
A commit message is a note to your future self and your team.
- Start with a short summary (about 50 characters), in the imperative mood: "Add seat warning", "Fix double booking".
- Describe what and why, not a transcript of keystrokes. Put detail after a blank line when needed.
- Make each commit one logical change that leaves the project working.
| Weak message | Better message |
|---|---|
update |
Fix seat count when two bookings arrive together |
changes |
Add SeatWarning component for low capacity |
final version real |
Extract date formatting into formatDate helper |
Commit often, in small steps. Small commits are easy to review, easy to revert, and make git blame and git bisect useful.
.gitignore
A .gitignore file lists patterns that Git should not track:
node_modules
dist
.env*
!.env.example
*.log
.DS_StoreIgnoring only prevents untracked files from being added. If a file such as .env was already committed, adding it to .gitignore does not remove it from history. You must stop tracking it with git rm --cached .env and, if it contained a secret, rotate the secret, because it remains in the history.
4. Branches
A branch is a movable pointer to a commit. Creating one is nearly free because Git only creates a new pointer. HEAD is the pointer that says which branch (or commit) you are currently on.
git branch # list local branches (* marks the current one)
git switch -c seat-warning # create once, then switch to it
# Alternative creation sequence, instead of the line above:
# git branch seat-warning
# git switch seat-warning
# Older alternative: git checkout -b seat-warninggit switch and git restore were introduced to split the overloaded git checkout into clearer commands. Both styles work in supported Git versions, and you will meet checkout in tutorials. Before switching, inspect status: non-conflicting uncommitted edits can follow you to another branch; Git refuses a switch that would overwrite them.
Now commit on the branch just created, without creating it a second time:
# edit src/components/WorkshopCard.jsx
git add src/components/WorkshopCard.jsx
git commit -m "Show warning when fewer than 3 seats remain"
git log --oneline --graph --all
# * 8d41b7e (HEAD -> seat-warning) Show warning when fewer than 3 seats remain
# * 3f2a1c9 (main) Add project README and App skeletonMerging
When the feature is ready, bring it into main.
git switch main
git merge seat-warningFor a normal two-branch merge, the main outcomes are:
- Fast-forward: the current tip is an ancestor of the incoming tip, so Git can advance the branch without a new commit. Options such as
--no-ffcan request a merge commit instead. - Merge commit: diverged histories are combined in a commit with two parents once conflicts, if any, are resolved.
- Already up to date: the incoming commits are already ancestors of the current tip, so no update is needed.
git branch -d seat-warning # delete after reviewing integration; not the checked-out branchThe -d ancestry check uses the branch’s upstream, or HEAD if it has no upstream; it does not prove that the feature was reviewed on main. Squash/rebase integration can change ancestry, so investigate a refusal rather than automatically forcing deletion.
Incompatible overlapping edits, or a delete-versus-edit, can cause a conflict. Identical edits on the same lines can merge automatically. Resolving conflicts is the subject of the next lesson.
Typical branch workflow
- Make sure
mainis up to date. - Create a short-lived branch named for the work (
seat-warning,fix/date-parse). - Commit in small steps.
- Merge back, usually through a pull request.
- Delete the branch.
Short branches reduce conflicts, because less time passes for the code to diverge.
Continue with pull requests, review, and merge conflicts for related practice.
5. Undoing things safely
Choosing the right undo depends on where the mistake is and whether it has been shared.
| Situation | Command | Effect |
|---|---|---|
| Discard unstaged edits in a file | git restore src/App.jsx |
Normally restores the working file from the index; the index may differ from HEAD. Edits are lost. |
| Unstage a file but keep edits | git restore --staged src/App.jsx |
Removes it from the next commit |
| Fix the last commit message or add a forgotten file (not pushed) | git commit --amend |
Replaces the last commit |
| Undo a commit already shared | git revert <hash> |
Adds a new commit that reverses the old one; history is preserved |
| Move the branch back, local only | git reset --soft <hash> (other modes below) |
Rewrites where the branch points |
git reset has three modes:
--soft: moves HEAD while leaving the index and working tree unchanged; differences against the new HEAD are what becomes staged.--mixed(default): moves HEAD and resets the index to it, while retaining working-tree content.--hard: moves HEAD and resets the index and tracked working-tree files to the target. It discards tracked edits and can overwrite or remove obstructing untracked paths; it is not a harmless way to clean up.
The golden rule: do not rewrite history that others already have. revert is safe for shared branches because it only adds. reset and amend change history and are appropriate for commits that exist only on your machine. Even a lost commit is often recoverable with git reflog, which keeps local reference movements subject to expiry and object cleanup, but do not rely on it as a plan.
git stash normally saves tracked staged and unstaged changes; -u also includes untracked files but not ignored files. Review what is included, and restore on the intended branch. In this example only, seat-warning is the original branch:
git stash push -u -m "WIP before switching tasks"
git switch main
# Work on the other task, then return before restoring:
git switch seat-warning
git stash apply 'stash@{0}' # keep the entry while checking the result
git status
# Only after verifying the restored work:
# git stash drop 'stash@{0}'Applying a stash can conflict; pop removes the entry only after successful application. A stash is local temporary storage, not a substitute for a durable backup. apply --index can attempt to restore the earlier staged state as well.
6. Remotes: sharing work
A remote is another copy of the repository, identified by a URL and a short name. By convention the main one is called origin.
git clone https://github.com/example/harbor-workshops.git
# normal clone of a non-empty repository: creates a folder, sets origin and checks out its default branch
# or connect an existing local project to a new empty remote:
git remote add origin https://github.com/example/harbor-workshops.git
git remote -v
git push -u origin main # publish main and remember the upstreamReplace the example repository URL with a repository you can access. -u records the upstream. Plain pull uses that tracking configuration; plain push also depends on push configuration, although the common matching-name setup lets both commands work without extra arguments.
fetch, pull, and push
| Command | What it does | Changes your files? |
|---|---|---|
git fetch |
Downloads new commits and updates remote-tracking branches such as origin/main |
No |
git pull |
Fetches, then applies the selected integration policy: fast-forward, merge, rebase, or squash | Can update files, or refuse divergence under fast-forward-only policy |
git push |
Uploads your local commits to the remote branch | Changes the remote |
git fetch is the safe way to look first: after it, git log main..origin/main shows what is new, and git diff main origin/main shows how it differs. git pull --ff-only makes the intended update explicit and refuses diverged branches. Current Git documentation defaults to fast-forward-only when no reconciliation method is configured; older versions and local configuration can differ. Inspect divergence and choose an approved merge or rebase policy explicitly.
A non-fast-forward rejection can occur when the remote branch tip is not an ancestor of the tip you are pushing:
! [rejected] main -> main (fetch first)Fetch and inspect first; for this shared-branch example, use git fetch origin followed by git merge origin/main, resolve any conflict and validate, then push. A plain pull may refuse divergence, so select the policy deliberately. Force-pushing can remove teammates’ commits from the branch’s visible history. Permission, protection-rule, or hook rejections have different causes; read the actual error.
Branches on the remote
git push -u origin seat-warning # publish your branch
git branch -r # list remote-tracking branches
# In a different clone with no local seat-warning branch:
# git fetch origin
# git switch --track origin/seat-warning7. Worked example: ship a seat-warning feature
Task. Treat this as a fresh practice scenario: main contains the Harbor Workshops skeleton, its origin remote and main’s tracking of origin/main are configured, and no local seat-warning branch exists. The earlier command blocks illustrate concepts rather than one script to execute twice. Add a low-seats warning on a branch, fix a mistake in your last commit, and publish the branch.
Step 1: Start from an up-to-date main.
git switch main
git pull --ff-onlyStep 2: Create the branch.
git switch -c seat-warningStep 3: Make the change and review it.
Update WorkshopCard.jsx with a visible warning for one or two remaining seats and a separate sold-out state. Assume the caller supplies validated capacity and booked counts:
export default function WorkshopCard({ title, capacity, booked }) {
const seatsLeft = capacity - booked;
const lowSeats = seatsLeft > 0 && seatsLeft < 3;
return (
<article>
<h2>{title}</h2>
<p>{seatsLeft === 0 ? "Sold out" : `${seatsLeft} seats left`}</p>
{lowSeats && <p>Only {seatsLeft} seats left</p>}
</article>
);
}git status
# modified: src/components/WorkshopCard.jsx
git diffgit diff shows the exact lines. If your own changes contain temporary debug logging, remove it before staging; the component above does not need any logging.
Step 4: Finish the review, stage, and commit.
# remove any temporary debug logging before staging
git add src/components/WorkshopCard.jsx
git commit -m "Show warning when fewer than 3 seats remain"Step 5: You forgot to update the README, and the commit is not pushed.
# edit README.md
git add README.md
git commit --amend --no-edit--amend is acceptable here because the commit has not been shared. It normally produces a new commit hash, so do not reuse a hash copied from an earlier example. Result: one revised commit instead of two.
Step 6: Publish.
git push -u origin seat-warningStep 7: Later the feature is integrated, and the latest ordinary, non-merge commit on main is found to be buggy. Use the following direct-main workflow only where repository policy permits it; otherwise propose the revert through a pull request. First inspect git log to confirm that assumption. If the feature was squashed or rebased, its integrated hash can differ. For this specific latest-commit case, use HEAD; a merge commit needs a separately chosen mainline and a different revert procedure.
git switch main
git pull --ff-only
git log --oneline -5
# After confirming HEAD is the ordinary commit to undo:
git revert HEAD
git push origin mainWhy these choices and not others. Working on a branch kept main stable while the feature was unfinished. amend was safe only because no one else had the commit; amending after publication can require rewriting a remote branch, so a follow-up commit is often preferable. revert on main adds a visible, reversible commit; reset --hard followed by push --force could remove commits from the shared branch and leave teammates needing to reconcile divergent history.
8. Pitfalls and study tips
- Pitfall: forgetting to stage.
git commitrecords only staged changes. If the commit looks empty, rungit status. The shortcutgit commit -am "message"stages changes to tracked files, but it never adds new untracked files. - Pitfall:
git add .too early. It can capture.env, build output, or large binaries. Checkgit statusfirst and keep.gitignorecurrent. - Pitfall: committing secrets. Deleting the file in a later commit does not remove it from history. Rotate the key.
- Pitfall: working on the wrong branch. Look at
git status("On branch ...") before editing and before committing. - Pitfall:
reset --hardwith uncommitted work. It cannot be undone for files that were never committed. - Pitfall: confusing
fetchandpull. An ordinarygit fetch originupdates local tracking data without integrating into your working tree. - Pitfall: force-pushing shared branches.
- Pitfall: huge commits that mix unrelated changes, which are hard to review or revert.
Study tip. For a goal-based practice question, identify the required command: "stage only one file" (git add file), "see staged changes" (git diff --staged), "undo a pushed commit safely" (git revert), "download without changing files" (git fetch), "create and switch" (git switch -c). Identify whether the change is unstaged, staged, committed locally, or pushed, and then choose the least destructive tool that works.
Study tip. If a push is rejected with "fetch first", the answer is to integrate the remote changes, not to force the push.
Mini case study: the leaked key and the broken main
Situation. Karim commits a .env file containing a private DATABASE_PASSWORD and pushes to main. A teammate notices the next day. Karim's first idea is to delete .env, commit "remove env", and carry on. Separately, a commit pushed to main last hour introduced a layout bug and Karim proposes git reset --hard HEAD~1 followed by git push --force.
Questions.
- Is deleting
.envin a new commit enough? - What should Karim do to prevent the file from being tracked again?
- What is the safer way to remove the buggy commit from
main?
Solution.
- No. The secret still exists in earlier commits in history, and anyone who cloned has it. The key must be revoked and replaced (rotated). Removing it from history is a separate, optional step that requires special tools and cannot erase copies others already have.
- Run
git rm --cached .env, add.envto.gitignore, and commit. Provide a.env.examplewith placeholder values so teammates know what variables are needed. - Use
git revert <hash>and a normalgit push. This adds a commit that undoes the change without rewriting shared history. The reset-and-force approach could remove the commit from the shared branch’s ancestry and discard later integration work.
Key terms
- Repository: Git objects and references, usually with a working tree;
.gitcan be a directory or a pointer file. - Commit: a snapshot with a message, author, and parent pointer.
- Staging area (index): the draft of the next commit.
- Branch: a movable pointer to a commit.
- HEAD: the pointer to the commit or branch you are on.
- Merge: combining the histories of two branches.
- Fast-forward: a merge that only moves a pointer.
- Remote / origin: another copy of the repository, by convention named
origin. - Upstream: the remote branch a local branch tracks.
- Revert: a new commit that undoes an earlier one.
Summary checklist
- I can explain the working directory, staging area, and repository.
- I configure
user.name,user.email, and the default branch. - I use
git status,git diff, andgit log --onelinebefore and after changes. - I write small commits with imperative, meaningful messages.
- I keep
.env,node_modules, and build output out of the repository. - I create and switch branches with
git switch -cand merge them. - I choose
restore,amend,revert, orresetbased on whether the change is shared. - I know
fetchdownloads andpullalso integrates, and thatpush -usets the upstream. - I never force-push a shared branch.
Official sources
- Meta Front-End Developer Professional Certificate (opens in a new tab)
- Course 3: Version Control (opens in a new tab)
- Pro Git book (opens in a new tab)
- Git reference documentation (opens in a new tab)
- GitHub Docs: About Git (opens in a new tab)
- Git: Staging content (opens in a new tab)
- Git: Clone options (opens in a new tab)
- Git: Restore (opens in a new tab)
- Git: Reset (opens in a new tab)
- Git: Stash (opens in a new tab)
- Git: Pull policies (opens in a new tab)
- Git: Push conditions (opens in a new tab)
