Computing Foundations › Git and GitHub › Day 30
Hands-on lab — Day 30: Git Fundamentals: Repositories, Staging, and Commits
- ← Back to the Day 30 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-030-git-fundamentals-repositories-staging-and-commits/
Commands
Setup
cd labs/sections/computing-foundations/day-030-git-fundamentals-repositories-staging-and-commits Run
bash examples/git_basics.sh
bash starter/git_basics.sh Test
bash tests/run_tests.sh File tree
examples/git_basics.sh expected-output/FIELDS.md expected-output/sample-run.txt metadata.yml README.md requirements/README.md security.md starter/git_basics.sh starter/git-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 030 lab — Init, Stage, Commit
Lesson
- Lesson title: Git Fundamentals: Repositories, Staging, and Commits
- Day number: 30 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-030-git-fundamentals-repositories-staging-and-commits
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-030-git-fundamentals-repositories-staging-and-commitswhen the site is running.
Purpose
Day 30's lesson builds the mental model of Git's three areas — working
directory, staging area, and repository — and what a commit really is. This
lab makes that model physical: you drive a throwaway Git repository by
hand and watch a single file move from untracked, to staged, to committed, then
see a .gitignore keep a secret out of history. Nothing here touches your real
projects or your global Git configuration, and it runs entirely offline.
Learning objectives
- Initialise a repository with
git initand recognise the.gitdirectory. - Read
git status --shortcodes (??,A,M) and name which of the three areas holds the current content at each step. - Move a file through the flow with
git add(stage) andgit commit(record), and confirm two commits accumulate in the history. - Use
git diffto see an unstaged change, andgit log --onelineto read history newest-first. - Use
.gitignoreto keep a file untracked, and prove it withgit check-ignore.
Prerequisites
- The Day 30 lesson (read it first — it explains every concept this lab exercises).
- The Day 29 idea of why version control exists.
- A terminal and Git installed (
git --versionprints a 2.x version). - No prior Git experience: every command is given and explained.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, Git 2.50.1).
- Linux — fully supported (any distribution with Git and a POSIX shell).
- Windows — use WSL or Git Bash and follow the same commands.
Hardware requirements
Any computer that can run Git. The lab creates a tiny temporary repository with a handful of small text files; it needs no meaningful RAM, disk, or GPU.
Required software
git(any 2.x release).bash(3.2+) and standard utilities:mktemp,printf,sed,grep,ls,rm— all preinstalled on macOS and Linux.
See requirements/README.md for install commands.
Free and open-source options
Git is free and open-source (GPLv2), and every utility this lab uses ships with your OS. No account, API key, purchase, or network connection is needed.
Installation
None beyond Git itself. From the repository root:
cd labs/sections/computing-foundations/day-030-git-fundamentals-repositories-staging-and-commits
git --version # confirm Git is installed
File structure
day-030-git-fundamentals-repositories-staging-and-commits/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── git_basics.sh ← YOUR working file (5 exercises)
│ └── git-worksheet.md ← record states, hashes, and what was ignored
├── examples/
│ └── git_basics.sh ← completed reference implementation
├── tests/
│ └── run_tests.sh ← automated checks (12)
├── expected-output/
│ ├── sample-run.txt ← a real captured run (macOS, Git 2.50.1)
│ └── FIELDS.md ← which lines are stable vs. which vary
├── requirements/
│ └── README.md ← dependency statement (just Git)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished result first
bash examples/git_basics.sh
## 2. Your task: complete the five exercises in the starter, then run it
bash starter/git_basics.sh
## 3. Check everything
bash tests/run_tests.sh
Then fill in starter/git-worksheet.md from what
you saw.
What the commands do
bash examples/git_basics.sh— the reference walkthrough. It creates a temporary repo under this directory, gives it a local identity, then runs the full flow:git init,git statuson an untracked file,git add,git commit, an edit,git diffon the unstaged change,git add+git commit, a.gitignorethat keepssecret.keyuntracked, andgit log --oneline. It deletes the temp repo on exit.bash starter/git_basics.sh— the same skeleton with fiveFILL_MElines. Each exercise comment names the exact command to type in its place. Edit the file in any text editor, then run it.bash tests/run_tests.sh— builds its own throwaway repo and asserts real Git behaviour: init works, a new file is untracked,git addstages it, commits accumulate to 2+, an edit shows as modified-but-unstaged,.gitignoreexcludes a file,HEADresolves, and no remote exists (proving it is fully local). It also runs the reference script and checks it leaves no directory behind.
Expected output
See expected-output/sample-run.txt — a real
captured run. The step banners, status codes, diff hunk, and the three-line
git log --oneline are identical every run; commit hashes differ every run
because a hash is computed from the snapshot content plus the commit time and
author. expected-output/FIELDS.md says exactly
which lines are stable and which vary.
A trimmed excerpt:
----- 2. git status — a brand-new file is UNTRACKED -----
?? notes.txt
----- 3. git add — move notes.txt into the STAGING AREA -----
A notes.txt
----- 8. git log --oneline — read the history (newest first) -----
945beab Add .gitignore to exclude secret.key
712412c Append a second line to notes.txt
fa3f66b Add notes.txt with initial notes
Validation steps
- Run
bash examples/git_basics.sh— it must exit without errors and print three commits undergit log --oneline. - Confirm
secret.keynever appears in anygit statusoutput. - Complete
starter/git_basics.sh(replace all fiveFILL_MElines) and run it — it must printgit log --onelinewith three commits. - Run the tests (next section) — all checks must pass.
- Confirm no
.gitdemo.*or.gittest.*directory is left behind.
Tests
bash tests/run_tests.sh
Expected final line: 12 checks, 0 failure(s). The command exits 0 on
success and non-zero on any failure, so it can run in CI. It uses no network.
Cleanup
Nothing to clean up in normal use: both scripts delete their temporary repository on exit. If a run was interrupted (e.g. Ctrl-C), remove any leftover with:
rm -rf .gitdemo.* .gittest.*
To reset your edits to the starter, restore it from version control:
git checkout -- starter/git_basics.sh.
Troubleshooting
See troubleshooting.md — covers "identity not set",
nothing to commit, working tree clean, a .gitignore that does not seem to
work, and Windows notes.
Security notes
See security.md. Short version: everything runs in a throwaway
temp directory with a local identity, makes no network calls, needs no
privileges, and demonstrates keeping secrets out of a repository with
.gitignore — a habit the course reinforces.
Extension exercises
- Run
git cat-file -p HEADin a temp repo to print the raw commit object and find itstree,parent,author, and message fields — the anatomy the lesson describes. - Stage part of a change with
git add -p(patch mode) and commit only some hunks, leaving the rest in the working directory. Confirm withgit status. - Add a second ignore pattern (e.g.
*.log) to.gitignore, create a matching file, and verify withgit check-ignore -vwhich rule matched.
Navigation
- Previous day: Day 29 — Why Version Control Exists
(
labs/sections/computing-foundations/day-029-why-version-control-exists/). - Next day: Day 31 — Branching and Merging
(
labs/sections/computing-foundations/day-031-branching-and-merging/).
Expected output
FIELDS.md
# What to expect in the output (all platforms)
`sample-run.txt` in this directory is a **real captured run** of
`examples/git_basics.sh` (macOS, Git 2.50.1, 2026-07-12). The same script
produces the same *shape* on any Linux with Git installed.
## Lines that are identical every run
- The eight `----- N. ... -----` step banners, in order.
- `?? notes.txt` — the untracked file in step 2.
- `A notes.txt` — the staged file in step 3 (note the two spaces after `A`).
- ` M notes.txt` — the modified-but-unstaged file in step 5 (leading space).
- The `git diff` hunk showing one context line and one added line
(`+a second line, added later`).
- In step 7, `git status --short` prints **nothing** for `secret.key`; the
`check-ignore` line reads `.gitignore:1:secret.key secret.key`.
- `git log --oneline` lists the three commit subjects, newest first.
- The summary reports `Commits in history: 3`.
## Lines that legitimately differ between runs
- **Commit hashes.** The short hashes (e.g. `b4b4836`) are computed from the
snapshot content **plus the commit timestamp and author**, so every run
produces different hashes. That is correct and expected — it is the whole
point of a content-addressed history. Two learners will never share a hash.
- **The `index` line inside the diff** (`index 841d9f2..47a4a5f`) references
blob hashes, which are stable for identical content — you should see the
same short blob ids because the file bytes are the same, but do not rely on
them.
- **The temp-directory path** in the final line (`.gitdemo.XXXXXX`) has a
random suffix and is deleted immediately after.
No field should ever show `secret.key` in a `git status`; if it does, the
`.gitignore` step did not work — see `troubleshooting.md`.
sample-run.txt
----- 1. git init — create an empty repository -----
Created a repository. The .git directory now exists:
./.git
----- 2. git status — a brand-new file is UNTRACKED -----
?? notes.txt
(?? means Git sees the file but is not tracking it yet)
----- 3. git add — move notes.txt into the STAGING AREA -----
A notes.txt
(A in the left column means the file is staged, ready to commit)
----- 4. git commit — record the staged snapshot in the repository -----
First commit created: b4b4836
----- 5. Edit the file, then git diff — see the UNSTAGED change -----
M notes.txt
(the M means notes.txt is modified in the working directory but not staged)
Working directory vs last commit:
diff --git a/notes.txt b/notes.txt
index 841d9f2..47a4a5f 100644
--- a/notes.txt
+++ b/notes.txt
@@ -1 +1,2 @@
notes for day 30
+a second line, added later
----- 6. git add + git commit — record the second snapshot -----
Second commit created: 346df9d
----- 7. .gitignore — keep an ignored file UNTRACKED -----
status --short (secret.key must NOT appear; .gitignore is now committed):
(empty status above = clean tree; secret.key is ignored, not staged)
Proof Git is deliberately ignoring it:
.gitignore:1:secret.key secret.key
----- 8. git log --oneline — read the history (newest first) -----
17ddc01 Add .gitignore to exclude secret.key
346df9d Append a second line to notes.txt
b4b4836 Add notes.txt with initial notes
----- Summary -----
First commit (short hash): b4b4836
Second commit (short hash): 346df9d
HEAD points at: 17ddc01 (the latest commit on branch main)
Commits in history: 3
Ignored and never committed: secret.key
Done. The throwaway repository at <repo>/labs/sections/computing-foundations/day-030-git-fundamentals-repositories-staging-and-commits/.gitdemo.IdCo02 will now be deleted.
Source files
examples/git_basics.sh (3551 bytes)
#!/usr/bin/env bash
# Day 030 lab — completed reference implementation.
#
# Demonstrates Git's core model end to end in a THROWAWAY repository:
# working directory -> (git add) -> staging area -> (git commit) -> repository
#
# It creates a temporary repo under this lab directory, gives it a LOCAL
# identity (never touching your global git config), walks the full
# init -> status -> add -> commit -> edit -> diff -> add -> commit ->
# .gitignore -> log flow, then deletes the temporary repo on exit.
#
# No network, no sudo, no changes outside the temp directory it creates.
set -euo pipefail
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
work="$(mktemp -d "${lab_dir}/.gitdemo.XXXXXX")"
# Always clean up the throwaway repo, however the script exits.
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
# Run every git command with a LOCAL identity for this repo only. We pass the
# identity via -c on each call so nothing is written to your ~/.gitconfig.
git_local() {
git -C "${work}" \
-c user.name="Day 30 Learner" \
-c user.email="learner@example.invalid" \
-c init.defaultBranch=main \
-c commit.gpgsign=false \
"$@"
}
rule() { printf '\n----- %s -----\n' "$1"; }
rule "1. git init — create an empty repository"
git_local init -q
echo "Created a repository. The .git directory now exists:"
ls -d "${work}/.git" | sed "s#${work}/#./#"
rule "2. git status — a brand-new file is UNTRACKED"
printf 'notes for day 30\n' > "${work}/notes.txt"
git_local status --short
echo "(?? means Git sees the file but is not tracking it yet)"
rule "3. git add — move notes.txt into the STAGING AREA"
git_local add notes.txt
git_local status --short
echo "(A in the left column means the file is staged, ready to commit)"
rule "4. git commit — record the staged snapshot in the repository"
git_local commit -q -m "Add notes.txt with initial notes"
first_hash="$(git_local rev-parse --short HEAD)"
echo "First commit created: ${first_hash}"
rule "5. Edit the file, then git diff — see the UNSTAGED change"
printf 'a second line, added later\n' >> "${work}/notes.txt"
git_local status --short
echo "(the M means notes.txt is modified in the working directory but not staged)"
echo "Working directory vs last commit:"
git_local --no-pager diff
rule "6. git add + git commit — record the second snapshot"
git_local add notes.txt
git_local commit -q -m "Append a second line to notes.txt"
second_hash="$(git_local rev-parse --short HEAD)"
echo "Second commit created: ${second_hash}"
rule "7. .gitignore — keep an ignored file UNTRACKED"
printf 'secret.key\n' > "${work}/.gitignore"
printf 'PRETEND-API-KEY-do-not-commit\n' > "${work}/secret.key"
git_local add .gitignore
git_local commit -q -m "Add .gitignore to exclude secret.key"
echo "status --short (secret.key must NOT appear; .gitignore is now committed):"
git_local status --short
echo "(empty status above = clean tree; secret.key is ignored, not staged)"
echo "Proof Git is deliberately ignoring it:"
git_local check-ignore -v secret.key | sed "s#${work}/#./#"
rule "8. git log --oneline — read the history (newest first)"
git_local log --oneline
rule "Summary"
echo "First commit (short hash): ${first_hash}"
echo "Second commit (short hash): ${second_hash}"
echo "HEAD points at: $(git_local rev-parse --short HEAD) (the latest commit on branch main)"
echo "Commits in history: $(git_local rev-list --count HEAD)"
echo "Ignored and never committed: secret.key"
echo
echo "Done. The throwaway repository at ${work} will now be deleted."
metadata.yml (622 bytes)
lesson_id: D030
day: 30
kind: command-line-inspection
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-030-git-fundamentals-repositories-staging-and-commits
run_commands:
- bash examples/git_basics.sh
- bash starter/git_basics.sh
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'rm -rf .gitdemo.* .gittest.* # only needed if a run was interrupted; scripts self-clean'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), Git 2.50.1, bash tests/run_tests.sh → 12 checks, 0 failure(s)'
requirements/README.md (1040 bytes)
# Dependencies — Day 030 lab
**One tool: Git.** This lab needs nothing else beyond a POSIX shell.
- `git` (any 2.x version; the reference run used 2.50.1). Check with
`git --version`.
- `bash` ≥ 3.2 and the standard utilities `mktemp`, `printf`, `sed`, `grep`,
`ls`, `rm` — all preinstalled on macOS and every mainstream Linux
distribution.
## Installing Git
- **macOS:** `git` ships with the Xcode Command Line Tools. If `git --version`
prompts to install them, accept — or install Git via Homebrew
(`brew install git`).
- **Debian/Ubuntu:** `sudo apt install git`
- **Fedora/RHEL:** `sudo dnf install git`
- **Windows:** install Git for Windows, or run this lab inside WSL and follow
the Linux path.
There is deliberately no `requirements.txt`/`package.json` here: the lab is
pure Git and shell. It makes **no network connections** and needs **no API
key**. It never writes to your global Git configuration — every command runs
with a local identity scoped to a throwaway repository the script deletes on
exit.
starter/git_basics.sh (3001 bytes)
#!/usr/bin/env bash
# Day 030 lab — YOUR working file.
#
# Goal: drive Git's core model by hand in a THROWAWAY repository and watch a
# file move through the three areas:
# working directory -> (git add) -> staging area -> (git commit) -> repository
#
# The scaffolding below sets up a temporary repo with a LOCAL identity (so
# nothing touches your global git config) and cleans it up on exit. Your job
# is the five numbered exercises: replace each `FILL_ME` line with the exact
# git command named in the comment above it. The finished reference is in
# examples/git_basics.sh — try it yourself first.
#
# Run it with: bash starter/git_basics.sh
set -euo pipefail
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
work="$(mktemp -d "${lab_dir}/.gitdemo.XXXXXX")"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
# Every git command runs with a LOCAL identity for THIS repo only.
git_local() {
git -C "${work}" \
-c user.name="Day 30 Learner" \
-c user.email="learner@example.invalid" \
-c init.defaultBranch=main \
-c commit.gpgsign=false \
"$@"
}
# --- Exercise 1: initialise an empty repository ---------------------------
# Replace FILL_ME with: git_local init -q
echo "Exercise 1: git init"
FILL_ME
[ -d "${work}/.git" ] && echo " .git created" || { echo " ERROR: no .git dir"; exit 1; }
# Create a file in the working directory (given).
printf 'notes for day 30\n' > "${work}/notes.txt"
# --- Exercise 2: stage notes.txt (move it to the staging area) ------------
# Replace FILL_ME with: git_local add notes.txt
echo "Exercise 2: git add notes.txt"
FILL_ME
echo " status after add:"; git_local status --short | sed 's/^/ /'
# --- Exercise 3: commit the staged snapshot -------------------------------
# Replace FILL_ME with: git_local commit -q -m "Add notes.txt with initial notes"
echo "Exercise 3: git commit"
FILL_ME
echo " first commit: $(git_local rev-parse --short HEAD)"
# Edit the file so there is an unstaged change to see (given).
printf 'a second line, added later\n' >> "${work}/notes.txt"
# --- Exercise 4: show the unstaged change with a diff ---------------------
# Replace FILL_ME with: git_local --no-pager diff
echo "Exercise 4: git diff (working directory vs last commit)"
FILL_ME
# Stage and commit the second snapshot (given, so you finish with 2 commits).
git_local add notes.txt
git_local commit -q -m "Append a second line to notes.txt"
echo " second commit: $(git_local rev-parse --short HEAD)"
# Add a .gitignore and an ignored file (given).
printf 'secret.key\n' > "${work}/.gitignore"
printf 'PRETEND-API-KEY-do-not-commit\n' > "${work}/secret.key"
git_local add .gitignore
git_local commit -q -m "Add .gitignore to exclude secret.key"
# --- Exercise 5: print the one-line history -------------------------------
# Replace FILL_ME with: git_local log --oneline
echo "Exercise 5: git log --oneline"
FILL_ME
echo
echo "If you see 3 commits above and secret.key never appeared in a status, you did it."
starter/git-worksheet.md (1980 bytes)
# Git worksheet — Day 030
Fill this in from a real run of `examples/git_basics.sh` (or your finished
`starter/git_basics.sh`). Watch the output scroll past and record what you
actually saw — the point is to connect each command to the state it produced.
## 1. The state of `notes.txt` at each stage
For each moment, write what `git status --short` showed (or would have shown)
for `notes.txt`, and say which of the three areas held the current version.
| Moment | `git status --short` code | Which area holds the newest content |
| --- | --- | --- |
| Right after creating the file, before `git add` | `______` | working directory only |
| After `git add notes.txt`, before commit | `______` | ______ |
| Right after the first `git commit` | (clean / no entry) | ______ |
| After appending a second line, before staging | `______` | ______ |
| After staging and the second `git commit` | (clean / no entry) | ______ |
Reminder of the codes: `??` = untracked, `A ` = staged new file,
` M` = modified but not staged, `M ` = modified and staged.
## 2. Your two commit hashes (short form)
Copy the short hashes the script printed (yours will differ from anyone
else's — that is expected; a hash is computed from the snapshot's content,
author, time, message, and parent).
- First commit ("Add notes.txt with initial notes"): `_______`
- Second commit ("Append a second line to notes.txt"): `_______`
- Which commit does `HEAD` point at after the run? `_______`
## 3. What `.gitignore` excluded
- The file name listed inside `.gitignore`: `______________`
- The file that stayed **untracked** because of it: `______________`
- Did that ignored file ever appear in `git status`? (yes / no): `______`
- One sentence in your own words on why keeping secrets out of a repository
matters:
`___________________________________________________________________`
## 4. One thing that surprised you
`_____________________________________________________________________`
tests/run_tests.sh (4259 bytes)
#!/usr/bin/env bash
# Tests for the Day 030 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Builds a throwaway repository the same way the lab does (temp dir under the
# lab directory, LOCAL identity only, no network) and verifies the real
# behaviour of Git's core model: init works, staging works, commits accumulate,
# .gitignore actually excludes a file, and cleanup leaves nothing behind.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# --- 0. The reference script runs cleanly and cleans up after itself -------
before_count="$(ls -1d "${lab_dir}"/.gitdemo.* 2>/dev/null | wc -l | tr -d ' ')"
if bash "${lab_dir}/examples/git_basics.sh" >/dev/null 2>&1; then
check "examples/git_basics.sh exits successfully" "yes"
else
check "examples/git_basics.sh exits successfully" "no"
fi
after_count="$(ls -1d "${lab_dir}"/.gitdemo.* 2>/dev/null | wc -l | tr -d ' ')"
if [ "${before_count}" = "${after_count}" ]; then
check "reference script leaves no .gitdemo.* directory behind" "yes"
else
check "reference script leaves no .gitdemo.* directory behind" "no"
fi
# --- Build our own throwaway repo to assert on Git's behaviour directly ----
work="$(mktemp -d "${lab_dir}/.gittest.XXXXXX")"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
git_local() {
git -C "${work}" \
-c user.name="Test Runner" \
-c user.email="test@example.invalid" \
-c init.defaultBranch=main \
-c commit.gpgsign=false \
"$@"
}
# 1. init creates a repository
git_local init -q >/dev/null 2>&1
[ -d "${work}/.git" ] && check "git init creates a .git repository" "yes" \
|| check "git init creates a .git repository" "no"
# 2. a new file is untracked
printf 'hello\n' > "${work}/file.txt"
if git_local status --short | grep -q '^?? file.txt$'; then
check "a new file shows as untracked (??)" "yes"
else
check "a new file shows as untracked (??)" "no"
fi
# 3. staging moves it to the index
git_local add file.txt
if git_local status --short | grep -q '^A file.txt$'; then
check "git add stages the file (A in status)" "yes"
else
check "git add stages the file (A in status)" "no"
fi
# 4. first commit lands
git_local commit -q -m "first commit" >/dev/null 2>&1
c1="$(git_local rev-list --count HEAD 2>/dev/null || echo 0)"
[ "${c1}" = "1" ] && check "first commit is recorded (1 commit)" "yes" \
|| check "first commit is recorded (1 commit)" "no"
# 5. edit + second commit -> 2 commits
printf 'world\n' >> "${work}/file.txt"
# the edit must be visible as an unstaged modification before we stage it
if git_local status --short | grep -q '^ M file.txt$'; then
check "an edit shows as modified-but-unstaged ( M)" "yes"
else
check "an edit shows as modified-but-unstaged ( M)" "no"
fi
git_local add file.txt
git_local commit -q -m "second commit" >/dev/null 2>&1
c2="$(git_local rev-list --count HEAD 2>/dev/null || echo 0)"
if [ "${c2}" -ge 2 ]; then
check "history reaches 2+ commits" "yes"
else
check "history reaches 2+ commits" "no"
fi
# 6. .gitignore actually excludes a file
printf 'ignored.log\n' > "${work}/.gitignore"
printf 'noise\n' > "${work}/ignored.log"
if git_local status --short | grep -q 'ignored.log'; then
check ".gitignore keeps the ignored file out of status" "no"
else
check ".gitignore keeps the ignored file out of status" "yes"
fi
if git_local check-ignore -q ignored.log; then
check "git confirms the file is ignored (check-ignore)" "yes"
else
check "git confirms the file is ignored (check-ignore)" "no"
fi
# 7. HEAD resolves to a real commit hash
if git_local rev-parse --short HEAD >/dev/null 2>&1; then
check "HEAD points at a real commit" "yes"
else
check "HEAD points at a real commit" "no"
fi
# 8. no network was used — assert the repo has no configured remote
if [ -z "$(git_local remote 2>/dev/null)" ]; then
check "repository has no remote (fully local, no network)" "yes"
else
check "repository has no remote (fully local, no network)" "no"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 030 lab
git: command not found
Git is not installed (or not on your PATH). Install it — see
requirements/README.md — and confirm with git --version.
*** Please tell me who you are / "identity not set"
When you run git commit in your own repositories, Git needs a name and email
for the commit's author fields. If you have never configured them, Git stops
and asks. This lab's scripts never hit that error because they pass a
local identity on every command:
git -C "${work}" \
-c user.name="Day 30 Learner" \
-c user.email="learner@example.invalid" \
...
The -c key=value flags set config for that single command only, so nothing
is written to your global ~/.gitconfig. In your own projects you would
instead set it once, globally:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
nothing to commit, working tree clean
This is not an error — it is Git telling you there is nothing staged.
git commit only records what is in the staging area. If you edited a file
but did not git add it, the staging area is empty and there is nothing to
commit. Run git status: if your change shows under "Changes not staged for
commit", git add it first, then commit. This is the single most common
beginner surprise, and the lesson's three-areas model explains exactly why.
secret.key shows up in git status anyway
The .gitignore step failed. Common causes:
- The file was already tracked before you added it to
.gitignore. Git ignores untracked files only; a file already committed keeps being tracked. Untrack it withgit rm --cached secret.key, then commit. - The pattern does not match. Confirm
.gitignorecontains exactlysecret.key(no trailing spaces). Ask Git why withgit check-ignore -v secret.key— it prints the matching rule, or nothing if no rule matches.
Permission denied when running a script
Run it through bash explicitly rather than executing it directly:
bash examples/git_basics.sh
If you prefer ./examples/git_basics.sh, first chmod +x the script.
A .gitdemo.* or .gittest.* directory is left behind
The scripts delete their throwaway repo on exit via a trap. A leftover only
happens if a run was killed (e.g. Ctrl-C at just the wrong moment). It is
safe to delete by hand: rm -rf .gitdemo.* .gittest.* from the lab directory.
Windows: bash is not recognized
Use WSL (wsl --install, then open your Linux distribution) and follow the
Linux path, or use Git Bash, which ships with Git for Windows.
Security notes
Security notes — Day 030 lab
-
Everything happens in a throwaway directory. Each script creates its own temporary repository with
mktemp -dinside this lab directory (.gitdemo.XXXXXX/.gittest.XXXXXX) and deletes it on exit via a shelltrap. Nothing is created outside that temp directory, and no existing repo of yours is touched. -
Local identity only. The scripts set
user.nameanduser.emailwith per-command-cflags, so they never write to your global~/.gitconfigand never change how commits are authored in your real projects. The email used,learner@example.invalid, is in the reserved.invaliddomain and can never resolve to a real address. -
No network, no privileges. Git here runs entirely offline: the repo has no remote, nothing is pushed or fetched, and no command needs
sudo. If a tutorial ever tells you tosudoa Git command you have not read, stop. -
Never commit secrets — and preview
.gitignorefor them. The lab makes this concrete: it writes a fakesecret.keyand uses.gitignoreto keep it untracked, so it is never captured in a commit. In real work, credentials (API keys, tokens, passwords,.envfiles) must be listed in.gitignorebefore the firstgit add, because once a secret is committed it lives in the repository history even after you delete the file — removing it later requires rewriting history. The habit to build: before you commit, rungit statusand read what is staged, and keep a.gitignorethat excludes every secret and credential file.git check-ignore -v <file>confirms a file is being ignored before you trust it. -
Read scripts before running them. Both lab scripts are short and commented. Reading a shell script before executing it is a habit worth keeping for anything you download.