How to buildOctober 11, 20268 min read
Programming FundamentalsStep 10 of 15

Git from scratch: commits, branches, merging

In short

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.

TutorialLevel: BeginnerGit & GitHubPractice: ~45–90 min

After this you will be able to

  • Create a repository, set up .gitignore and make meaningful commits with git add and git 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, revert or reset --soft

Verified with

Git 2.55 (Git for Windows, Git Bash)Git 2.45 (Alpine Linux 3.20)

Checked against docs: October 11, 2026

Contents
  1. 01Why version control
  2. 02Working directory, index, commit
  3. 03Your first repository: init, status, add, commit, log
  4. 04.gitignore and what must not be committed
  5. 05Branches: creating, switching and fast-forward
  6. 06A merge with a conflict: how to resolve it
  7. 07Undoing changes: restore, revert, reset — without losing work
  8. 08Practice: 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 directorygit addIndex (staging area)git commitRepository (.git)
The path of a change: from a file on disk to a commit in history.

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 .git directory
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).

bash
git config --global user.name "Aigerim Dev"
git config --global user.email "aigerim@example.com"
git config --global init.defaultBranch main
One-time setup. Use your own name and email. Checked with Git 2.55: git 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:

index.html
<!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>
The starting page — create it in your editor.
terminal
$ 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
Git 2.55, Git Bash (real output; the path in the Initialized line is shortened, index.html was created in the editor). Here the name and email are set for this repository only — without --global.
  1. 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.

  2. git add

    Copies the current version of files into the index. git status --short shows it compactly: ?? is an untracked file, A is added to the index, M is modified. The left column is the index, the right one the working directory.

  3. git commit -m

    Records the contents of the index. Git prints the branch, the short commit hash (01210cb) and statistics. root-commit is the first commit, with no parent.

  4. git log --oneline

    The history, one commit per line: hash and message. Without --oneline it 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.

terminal
$ 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.log
Git 2.55, Git Bash (real output). git check-ignore -v shows which rule on which line matched.

The same .gitignore with comments — easier to maintain:

.gitignore
# dependencies — installed by npm install
node_modules/
# local settings and secrets
.env
# logs
*.log
Checked: git 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 build at 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.

terminal
$ 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 2.55, Git Bash (real output; the footer line was added to index.html in the editor before 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.

terminal
$ 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 2.55, Git Bash (real output). sed edits the line in place — you can do the same in your editor.

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.

index.html
<!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>
index.html after resolving the conflict — no markers left.
terminal
$ 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 2.55, Git Bash (real output). During a merge git diff uses the combined format: two columns of signs, one per branch.
  1. Find conflicted files

    git status lists them under Unmerged paths.

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

  3. Mark it resolved

    git add file — per Pro Git, staging the file is what marks the conflict as resolved.

  4. Finish the merge

    git commit (with --no-edit it keeps the default Merge branch … message). Changed your mind? git merge --abort returns 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.

terminal
$ 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 clean
Git 2.55, Git Bash (real output).

Broke 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).

conflict-lab.sh
#!/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"
Checked: Git 2.55 in Git Bash and Git 2.45 on Alpine Linux — identical result.

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.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.