Version-Control Foundations
Version control records changes, enables parallel work, provides a history of decisions, supports review, and allows recovery.
What this module covers
Why version control exists Local, centralized, and distributed version control
Repository, working tree, staging area, and commit Initialize, clone, status, add, commit, log, diff
Tracked, untracked, ignored, and modified files Commit messages and atomic commits .gitignore and repository hygiene Configure identity and default branch Inspecting changes before committing Basic recovery and safety habits
9.1 Why version control?
Version control records changes, enables parallel work, provides a history of decisions, supports review, and allows recovery. A commit should represent a coherent change with a useful explanation.
9.2 Distributed versus centralized systems
In centralized systems, a central server commonly owns the authoritative history. In distributed systems such as Git, each clone usually contains local repository history and can commit without contacting the server. Remote collaboration still requires synchronization and access control.
Other systems include Subversion (SVN) and Perforce. Their models and workflows differ; Git commands are not interchangeable with SVN or Perforce commands.
9.3 Initialize and configure Git
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible). HOME points to a scratch directory, so --global writes a throw-away configuration:
Use your own appropriate identity values. In managed environments, follow organizational identity and signing policy.
git --version
git config --global user.name "Example Developer"
git config --global user.email "[email protected]"
git config --global init.defaultBranch main
git init
git status$ git --version
git version 2.34.1
$ git config --global user.name "Example Developer"
$ git config --global user.email "[email protected]"
$ git config --global init.defaultBranch main
$ git init
Initialized empty Git repository in /tmp/bsvc/project/.git/
$ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)9.4 First commit
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):
git add stages the current content of selected paths. git diff --cached reviews the staged changes. git commit records the staged snapshot.
git add README.md
git diff --cached
git commit -m "docs: add project overview"
git log --oneline$ printf '# Sensor Demo\n\nReads a sensor and reports scaled values.\n' > README.md
$ git add README.md
$ git diff --cached
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dfd2cb6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Sensor Demo
+
+Reads a sensor and reports scaled values.
$ git commit -m "docs: add project overview"
[main (root-commit) 8fa0fca] docs: add project overview
1 file changed, 3 insertions(+)
create mode 100644 README.md
$ git log --oneline
8fa0fca docs: add project overview9.5 The three main states
Working tree: checked-out files on disk.
Index: proposed next commit snapshot.
Repository: commits and objects stored by Git.
The index is not merely a queue of filenames; it records the content and mode information intended for the next commit.
Try it: working tree, index and repository
Working tree --git add--> Index/Staging Area --git commit--> Repository
^ | |
|-------- edit/restore -------| |9.6 Essential commands
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible): the commands above in a working order, with path/to/file as src/parse.c:
Be careful: restoring a file can discard uncommitted edits. Inspect git status and git diff before destructive operations.
git status
git diff
git diff --staged
git add path/to/file
git restore path/to/file
git restore --staged path/to/file
git commit -m "fix: handle empty input"
git log --oneline --graph --decorate --all
git show HEAD$ printf 'int parse(const char *s)\n{\n if (s[0] == 0)\n return -1;\n return s[0] - 0x30;\n}\n' > src/parse.c
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/parse.c
no changes added to commit (use "git add" and/or "git commit -a")
$ git diff
diff --git a/src/parse.c b/src/parse.c
index db0d7e6..84c4345 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -1,4 +1,6 @@
int parse(const char *s)
{
+ if (s[0] == 0)
+ return -1;
return s[0] - 0x30;
}
$ git add src/parse.c
$ git diff --staged --stat
src/parse.c | 2 ++
1 file changed, 2 insertions(+)
$ git restore --staged src/parse.c
$ git status --short
M src/parse.c
$ git add src/parse.c
$ git commit -m "fix: handle empty input"
[main cd9af75] fix: handle empty input
1 file changed, 2 insertions(+)
$ git log --oneline --graph --decorate --all
* cd9af75 (HEAD -> main) fix: handle empty input
* b9a7414 feat: add parser
* 8fa0fca docs: add project overview
$ git show HEAD --stat
commit cd9af753998abc2161bcf1bdb9522c9c3f38f006
Author: Example Developer
…9.7 .gitignore
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible) with the block above saved as .gitignore:
Ignore patterns should match your project. Do not ignore source-controlled test fixtures or required generated files blindly. .gitignore does not untrack files that are already committed; remove those from the index with care, for example git rm --cached path.
# Build output
build/
dist/
out/
# Editor/OS noise
.DS_Store
Thumbs.db
.vscode/
# Secrets and local environment
.env
.env.*
!.env.example
# Object and executable outputs
*.o
*.obj
*.elf
*.hex
*.bin$ mkdir -p build && touch build/app.elf main.o .env .env.example Thumbs.db notes.md
$ git status --short --ignored
?? .env.example
?? .gitignore
?? notes.md
!! .env
!! Thumbs.db
!! build/
!! main.o
$ git check-ignore -v --non-matching .env .env.example main.o notes.md
.gitignore:12:.env .env
.gitignore:14:!.env.example .env.example
.gitignore:17:*.o main.o
:: notes.md
$ git add .gitignore .env.example notes.md && git commit -q -m 'chore: add ignore rules' && git status --short9.8 Good commits
A useful commit is:
Focused on one logical change.
Tested to the level appropriate for the change.
Reviewed before commit.
Named with a clear imperative summary.
Free from secrets, accidental binaries, unrelated formatting, and temporary debug output.
fix: reject invalid CAN payload length
Validate the payload length before copying bytes into the
transmit buffer to prevent an out-of-bounds write.Quiz
? In the real run, what did `git status --short` show after `git restore --staged src/parse.c`?
- `M src/parse.c` (staged) :: That was before unstaging.
+ ` M src/parse.c` (modified, not staged) :: Unstaging keeps the edit in the working tree.
- Nothing :: The edit was still there.
- `?? src/parse.c` :: The file was tracked.
? `.env.example` stayed visible while `.env` and `.env.*` were ignored. Which line made that possible?
+ `!.env.example` :: A `!` pattern re-includes a path; `git check-ignore -v` reported it.
- `.env` :: That ignores `.env`.
- `build/` :: Unrelated.
- `*.o` :: Unrelated.
? Which habits make commits useful? (select all that apply)
+ One logical change per commit :: Easier to review and revert.
+ Review the staged diff before committing :: `git diff --cached` shows exactly what will be recorded.
+ A clear imperative summary line :: For example `fix: handle empty input`.
- Including build outputs so others need not build :: Generated binaries belong in artifact storage.