Git stores a project's history as a chain of commits — snapshots of all files at the moment you save. The working loop: change files, mark the ones you want with git add, record them with git commit. New work goes into a separate branch and is merged back with git merge; if both branches changed the same line, Git stops at a conflict and you choose the final text yourself.
After this you will be able to
- Create a repository, set up
.gitignoreand make meaningful commits withgit addandgit commit - Create a branch, switch to it and merge it back, telling a fast-forward from a merge commit
- Resolve a merge conflict: find the markers, keep the final text and complete the merge
- Pick a safe way to undo:
restore,revertorreset --soft
Know beforehand
Verified with
Git 2.55 (Git for Windows, Git Bash)Git 2.45 (Alpine Linux 3.20)
Checked against docs: October 11, 2026
Contents
- Why version control
- Working directory, index, commit
- Your first repository: init, status, add, commit, log
- .gitignore and what must not be committed
- Branches: creating, switching and fast-forward
- A merge with a conflict: how to resolve it
- Undoing changes: restore, revert, reset — without losing work
- Practice: a repository with a deliberate conflict
Why version control
Without version control a project's history lives in folders like site-final, site-final-2, site-final-EDITS. It is unclear what changed, who did it and how to get yesterday's working version back. Git solves this: every save — a commit — gets an author, a date, a message and a unique identifier, and you can return to any commit.
Pro Git, the project's official book, stresses how Git differs from many older systems: it stores snapshots rather than a list of edits — on every commit Git effectively takes a picture of all your files. Unchanged files are not copied again; Git links to the version it already has. Nearly every operation is local, so you can work with the history offline.
- History: who changed each line, when and why.
- Safe experiments: a new feature is built in a branch and does not break the working version.
- Teamwork: several people's changes are combined automatically, and Git shows contested spots explicitly.
- Undo: a bad commit can be reverted without losing the rest of the work.
Working directory, index, commit
The main thing to remember is that a file in Git has three states. Per Pro Git: modified — you changed the file but have not committed it; staged — you marked the current version of the file to go into the next commit; committed — the data is safely stored in your local database. That gives three places changes move between.
Working directory
- What it holds
- The files you edit
- Command to see it
git status,git diff
Index
- What it holds
- File versions that will go into the next commit
- Command to see it
git diff --staged
Repository
- What it holds
- All commits, in the
.gitdirectory - Command to see it
git log
The index lets you build a commit deliberately: out of five changed files, stage the two that belong to one task. Before the first commit Git needs to know who you are — the name and email go into every commit. --global saves the settings for all of the user's repositories; the last line makes main the default name of the first branch (the git init documentation says that without this setting Git currently uses master, which will change to main in Git 3.0).
git config --global user.name "Aigerim Dev"
git config --global user.email "aigerim@example.com"
git config --global init.defaultBranch maingit config --global --list shows all three values.Your first repository: init, status, add, commit, log
Let us create a repository for a coffee shop landing page. git init creates a hidden .git directory in the current folder — that is the repository. The folder has two files: README.md and the index.html page:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
<h1>Coffee Point</h1>
<p>Open daily 8:00-22:00</p>
</body>
</html>$ git --version
git version 2.55.0.windows.1
$ mkdir landing && cd landing
$ git init -b main
Initialized empty Git repository in C:/Users/you/landing/.git/
$ git config user.name "Aigerim Dev"
$ git config user.email "aigerim@example.com"
$ printf '# Coffee Point landing\n' > README.md
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
index.html
nothing added to commit but untracked files present (use "git add" to track)
$ git add index.html README.md
$ git status --short
A README.md
A index.html
$ git commit -m "Add landing page skeleton"
[main (root-commit) 01210cb] Add landing page skeleton
2 files changed, 9 insertions(+)
create mode 100644 README.md
create mode 100644 index.html
$ git log --oneline
01210cb Add landing page skeleton--global.git status
A beginner's main command: which branch you are on, what changed, what is staged. Git calls new files untracked — it sees them but does not track them.
git add
Copies the current version of files into the index.
git status --shortshows it compactly:??is an untracked file,Ais added to the index,Mis modified. The left column is the index, the right one the working directory.git commit -m
Records the contents of the index. Git prints the branch, the short commit hash (
01210cb) and statistics.root-commitis the first commit, with no parent.git log --oneline
The history, one commit per line: hash and message. Without
--onelineit also shows author and date.
.gitignore and what must not be committed
Every project accumulates files that do not belong in history: installed dependencies (node_modules), builds, logs and, most importantly, local settings with secrets (.env). A .gitignore file at the repository root lists path patterns Git should ignore.
$ printf 'API_KEY=YOUR_API_KEY\n' > .env
$ mkdir node_modules && touch node_modules/cache.js
$ printf 'debug\n' > npm-debug.log
$ git status --short
?? .env
?? node_modules/
?? npm-debug.log
$ printf 'node_modules/\n.env\n*.log\n' > .gitignore
$ git status --short
?? .gitignore
$ git add .gitignore
$ git commit -m "Ignore dependencies, logs and local secrets"
[main 8e7365c] Ignore dependencies, logs and local secrets
1 file changed, 3 insertions(+)
create mode 100644 .gitignore
$ git check-ignore -v .env npm-debug.log
.gitignore:2:.env .env
.gitignore:3:*.log npm-debug.loggit check-ignore -v shows which rule on which line matched.The same .gitignore with comments — easier to maintain:
# dependencies — installed by npm install
node_modules/
# local settings and secrets
.env
# logs
*.loggit check-ignore -v finds the rules for node_modules/, .env and *.log.node_modules/
- What it ignores
- A directory with that name at any level (a trailing slash means directories only)
*.log
- What it ignores
- All files ending in .log;
*does not match/
/build
- What it ignores
- Only
buildat the root — a leading slash anchors the pattern to the .gitignore directory
!keep.log
- What it ignores
- Re-includes a file excluded by an earlier rule
# text
- What it ignores
- A comment
Branches: creating, switching and fast-forward
A branch in Git is a movable pointer to a commit. New work goes into its own branch: the main one (main) stays working, and an unfinished feature does not get in anyone's way. git switch -c name creates a branch and switches to it (two commands do the same: git branch name and git switch name). Per Pro Git, since version 2.23 you can use git switch instead of the older git checkout to switch and create branches.
$ git switch -c feature/footer
Switched to a new branch 'feature/footer'
$ git diff
diff --git a/index.html b/index.html
index 8e73a21..4683878 100644
--- a/index.html
+++ b/index.html
@@ -4,5 +4,6 @@
<body>
<h1>Coffee Point</h1>
<p>Open daily 8:00-22:00</p>
+ <footer>Almaty, Abay Ave 1</footer>
</body>
</html>
$ git commit -am "Add footer with address"
[feature/footer 47ae5bd] Add footer with address
1 file changed, 1 insertion(+)
$ git switch main
Switched to branch 'main'
$ git merge feature/footer
Updating 8e7365c..47ae5bd
Fast-forward
index.html | 1 +
1 file changed, 1 insertion(+)
$ git branch -d feature/footer
Deleted branch feature/footer (was 47ae5bd).git diff).git diff shows unstaged changes: lines with + were added, lines with - removed. git commit -am stages every already tracked modified file and commits right away — the flag does not add new files. After switching to main, the merge was a fast-forward: main had no new commits of its own, so Git simply moved the pointer forward. A merged branch is deleted with git branch -d — per the documentation, the branch must be fully merged, otherwise Git refuses to delete it.
A merge with a conflict: how to resolve it
If both branches have their own commits, Git performs a three-way merge: it compares the two latest snapshots with their common ancestor and creates a merge commit with two parents. When the branches changed the same line differently, there is no automatic choice — that is a conflict. Let us reproduce one: in feature/hours we change the opening hours, and in main we change the same line in a different way.
$ git branch feature/hours
$ git switch feature/hours
Switched to branch 'feature/hours'
$ sed -i 's/8:00-22:00/9:00-23:00/' index.html
$ git commit -am "Change opening hours to 9-23"
[feature/hours bd9d4cd] Change opening hours to 9-23
1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
$ sed -i 's/8:00-22:00/8:00-22:00, weekends till 23:00/' index.html
$ git commit -am "Mention weekend hours"
[main 437a99b] Mention weekend hours
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --all
* bd9d4cd Change opening hours to 9-23
| * 437a99b Mention weekend hours
|/
* 47ae5bd Add footer with address
* 8e7365c Ignore dependencies, logs and local secrets
* 01210cb Add landing page skeleton
$ git merge feature/hours
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
$ cat index.html
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
<h1>Coffee Point</h1>
<<<<<<< HEAD
<p>Open daily 8:00-22:00, weekends till 23:00</p>
=======
<p>Open daily 9:00-23:00</p>
>>>>>>> feature/hours
<footer>Almaty, Abay Ave 1</footer>
</body>
</html>Git stopped in the middle of the merge: the file is marked both modified and now contains markers. Between <<<<<<< HEAD and ======= is the current branch's version (main); between ======= and >>>>>>> feature/hours is the incoming branch's version. Resolving a conflict means leaving the final text and deleting all three markers. Here the right result combines both edits: the new hours and the weekend note.
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
<h1>Coffee Point</h1>
<p>Open daily 9:00-23:00, weekends till 23:00</p>
<footer>Almaty, Abay Ave 1</footer>
</body>
</html>$ git diff
diff --cc index.html
index d216469,5d082e3..0000000
--- a/index.html
+++ b/index.html
@@@ -3,7 -3,7 +3,7 @@@
<head><meta charset="utf-8"><title>Coffee Point</title></head>
<body>
<h1>Coffee Point</h1>
- <p>Open daily 8:00-22:00, weekends till 23:00</p>
- <p>Open daily 9:00-23:00</p>
++ <p>Open daily 9:00-23:00, weekends till 23:00</p>
<footer>Almaty, Abay Ave 1</footer>
</body>
</html>
$ git add index.html
$ git status --short
M index.html
$ git commit --no-edit
[main c954b42] Merge branch 'feature/hours'
$ git log --oneline --graph
* c954b42 Merge branch 'feature/hours'
|\
| * bd9d4cd Change opening hours to 9-23
* | 437a99b Mention weekend hours
|/
* 47ae5bd Add footer with address
* 8e7365c Ignore dependencies, logs and local secrets
* 01210cb Add landing page skeleton
$ git branch -d feature/hours
Deleted branch feature/hours (was bd9d4cd).git diff uses the combined format: two columns of signs, one per branch.Find conflicted files
git statuslists them under Unmerged paths.Edit each file
Keep the version you need or combine both, and delete the
<<<<<<<,=======,>>>>>>>markers. Many code editors highlight these blocks and help you pick a version.Mark it resolved
git add file— per Pro Git, staging the file is what marks the conflict as resolved.Finish the merge
git commit(with--no-editit keeps the defaultMerge branch …message). Changed your mind?git merge --abortreturns everything to the state before the merge.
Undoing changes: restore, revert, reset — without losing work
There are several undo commands, and the right one depends on where the mistake lives: in the working directory, in the index or already in a commit. Since Git 2.23 files have a dedicated command, git restore — the same one git status suggests.
$ printf 'oops\n' >> README.md
$ git status --short
M README.md
$ git restore README.md
$ git status --short
$ printf 'draft\n' > notes.txt
$ git add notes.txt
$ git status --short
A notes.txt
$ git restore --staged notes.txt
$ git status --short
?? notes.txt
$ rm notes.txt
$ sed -i 's# <footer># <p class="promo">-50% today</p>\n <footer>#' index.html
$ git commit -am "Add promo banner"
[main c8a8b36] Add promo banner
1 file changed, 1 insertion(+)
$ git revert --no-edit HEAD
[main 6bc2589] Revert "Add promo banner"
Date: Sun Oct 11 16:11:34 2026 -0600
1 file changed, 1 deletion(-)
$ git log --oneline -3
6bc2589 Revert "Add promo banner"
c8a8b36 Add promo banner
c954b42 Merge branch 'feature/hours'
$ grep -c promo index.html
0
$ printf 'typo\n' >> README.md
$ git commit -am "WIP"
[main 7ca022b] WIP
1 file changed, 1 insertion(+)
$ git reset --soft HEAD~1
$ git status --short
M README.md
$ git log --oneline -1
6bc2589 Revert "Add promo banner"
$ git restore --staged README.md && git restore README.md
$ git status
On branch main
nothing to commit, working tree cleanBroke a file, have not run add yet
- Command
git restore file- What happens
- The file returns to the version in the index; working-directory edits are lost
An extra file got into the index
- Command
git restore --staged file- What happens
- The file leaves the index; its contents on disk stay
A bad commit is already in shared history
- Command
git revert <commit>- What happens
- A new commit undoes the changes; history is not rewritten
The last commit has not been shared yet
- Command
git reset --soft HEAD~1- What happens
- The commit is removed, its changes stay staged in the index
Practice: a repository with a deliberate conflict
The script below creates a practice repository: a site settings file, a feature/hours branch and different edits to the same line in both branches. Save it as conflict-lab.sh and run bash conflict-lab.sh in bash, zsh or Git Bash. The -i.bak flag makes sed behave the same in GNU sed (Linux, Git Bash) and BSD sed (macOS).
#!/usr/bin/env bash
# conflict-lab.sh — creates a practice repo with two branches and a deliberate merge conflict
set -e
dir="${1:-git-conflict-lab}"
mkdir "$dir" && cd "$dir"
git init -q -b main
git config user.name "Git Learner"
git config user.email "learner@example.com"
printf 'title: Coffee Point\ncity: Almaty\nhours: 8:00-22:00\n' > site.yml
git add site.yml
git commit -q -m "Add site settings"
git switch -q -c feature/hours
sed -i.bak 's/8:00-22:00/9:00-23:00/' site.yml && rm site.yml.bak
git commit -q -am "Change hours on feature branch"
git switch -q main
sed -i.bak 's/8:00-22:00/8:00-21:00/' site.yml && rm site.yml.bak
git commit -q -am "Change hours on main"
echo "Ready: cd $dir && git merge feature/hours"Next step: a remote repository — git push, pull requests and code review on GitHub.
How this was verified4 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.