Computing FoundationsGit and GitHub › Day 30

Hands-on lab — Day 30: 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

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 init and recognise the .git directory.
  • Read git status --short codes (??, 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) and git commit (record), and confirm two commits accumulate in the history.
  • Use git diff to see an unstaged change, and git log --oneline to read history newest-first.
  • Use .gitignore to keep a file untracked, and prove it with git 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 --version prints 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 status on an untracked file, git add, git commit, an edit, git diff on the unstaged change, git add + git commit, a .gitignore that keeps secret.key untracked, and git log --oneline. It deletes the temp repo on exit.
  • bash starter/git_basics.sh — the same skeleton with five FILL_ME lines. 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 add stages it, commits accumulate to 2+, an edit shows as modified-but-unstaged, .gitignore excludes a file, HEAD resolves, 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

  1. Run bash examples/git_basics.sh — it must exit without errors and print three commits under git log --oneline.
  2. Confirm secret.key never appears in any git status output.
  3. Complete starter/git_basics.sh (replace all five FILL_ME lines) and run it — it must print git log --oneline with three commits.
  4. Run the tests (next section) — all checks must pass.
  5. 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

  1. Run git cat-file -p HEAD in a temp repo to print the raw commit object and find its tree, parent, author, and message fields — the anatomy the lesson describes.
  2. Stage part of a change with git add -p (patch mode) and commit only some hunks, leaving the rest in the working directory. Confirm with git status.
  3. Add a second ignore pattern (e.g. *.log) to .gitignore, create a matching file, and verify with git check-ignore -v which rule matched.
  • 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 with git rm --cached secret.key, then commit.
  • The pattern does not match. Confirm .gitignore contains exactly secret.key (no trailing spaces). Ask Git why with git 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 -d inside this lab directory (.gitdemo.XXXXXX / .gittest.XXXXXX) and deletes it on exit via a shell trap. Nothing is created outside that temp directory, and no existing repo of yours is touched.

  • Local identity only. The scripts set user.name and user.email with per-command -c flags, so they never write to your global ~/.gitconfig and never change how commits are authored in your real projects. The email used, learner@example.invalid, is in the reserved .invalid domain 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 to sudo a Git command you have not read, stop.

  • Never commit secrets — and preview .gitignore for them. The lab makes this concrete: it writes a fake secret.key and uses .gitignore to keep it untracked, so it is never captured in a commit. In real work, credentials (API keys, tokens, passwords, .env files) must be listed in .gitignore before the first git 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, run git status and read what is staged, and keep a .gitignore that 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.