Lesson 6 · Everyday work
Read the comparison before judging a change
Turn the hero into a runnable static project, then distinguish working, staged and recorded differences. Inspect a separate deterministic image variant without adding an image commit to the continuing course history.
- Level: Beginner
- 13 min read
- 5 min video
- Updated
- Before this lesson: Stage the intended snapshot and make your first commit
Progress saved in this browser.
Playing loads the video from YouTube (privacy-enhanced mode).
What you will learn
- Name both sides of a text comparison and interpret additions/removals.
- Explain why an untracked file can be absent from ordinary git diff.
- Compare images visually without pretending Git provides a textual image patch.
- Add and verify the tiny local checks before committing the runnable page.
Before you start
You start where lesson 5 ended: the original main with one hero commit. HEAD/index contain HTML-01; working index.html contains HTML-01-WORKING. No remote exists. Preserve that supporting-line edit in HTML-02; do not discard it.
The video was recorded with Git 2.52.0, Node 22.22.0, npm 10.9.4 and GitTree 2.0 on Ubuntu 24.04: the ten files were saved in GitTree's own editor, staged and committed in the app, and the terminal checks ran against what the app created, or against separate copies for the state before staging and for the image. The Node checks passed on Node 22, the version the project declares. Nothing was run on macOS or Windows, and Ubuntu is the only Linux system recorded. GitTree's inspected Git floor is 2.39.0. Once you are signed in to GitTree, local Git is on the Free plan and needs no provider or API credentials.
Concept
A diff answers a question about two snapshots, not whether a file is “good.” Ordinary git diff compares index → working tree; git diff --cached compares HEAD → index; git diff HEAD compares HEAD → working tree. A plus marks new-side text and a minus marks old-side text, not approval or blame. Read Git's diff reference for comparison semantics.
An untracked file has no ordinary index entry. Empty ordinary diff therefore does not prove that no new file exists. Review status and newly added file contents too. After staging a new file, its full addition can be reviewed in the cached comparison.
Changing Inline/Split/Hunk, wrapping, image mode or hidden-whitespace display changes the view, not file bytes, index, commits or remote refs. Staging does change the index. The final normal commit moves local main; no server state changes.
Full runnable project inputs
Keep README-01 unchanged. Replace index.html with HTML-02 and create all remaining files below with UTF-8/LF and a final newline. The minimal .gitignore is intentional: build output must not accidentally enter this first runnable commit. Broader ignore policy comes later. These are course-document payloads, not application files installed into the web repository.
index.html — HTML-02
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitTree Features — course demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="site-header">
<a href="#main">GitTree Features</a>
</header>
<main id="main">
<section class="hero" aria-labelledby="hero-title">
<p class="eyebrow">GitTree Learn demo project</p>
<h1 id="hero-title">Understand every change.</h1>
<p id="hero-copy">Choose one change, then check what Git saved.</p>
</section>
<section class="features" aria-labelledby="features-title">
<h2 id="features-title">Follow your work</h2>
<div class="cards">
<article class="feature-card">
<h3>History</h3>
<p>Read commits and branches together.</p>
</article>
<article class="feature-card">
<h3>Changes</h3>
<p>Review the exact content selected for a commit.</p>
</article>
<article class="feature-card">
<h3>Recovery</h3>
<p>Inspect the scope of a recovery point before using it.</p>
</article>
</div>
</section>
</main>
<footer><p>Built by a fictional team for GitTree Learn.</p></footer>
<script type="module" src="site.js"></script>
</body>
</html>styles.css — CSS-01
:root {
color-scheme: light;
--page: #f7f8fa;
--surface: #ffffff;
--text: #17212e;
--link: #154ab5;
--line: #bac4d0;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--page);
color: var(--text);
font: 1rem/1.6 system-ui, sans-serif;
}
a {
color: var(--link);
}
.site-header,
main,
footer {
width: min(68rem, 100% - 2rem);
margin-inline: auto;
}
.site-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
padding-block: 1rem;
}
.hero {
max-width: 44rem;
padding-block: 3rem;
}
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: 1.2;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
gap: 1rem;
}
.feature-card {
padding: 1rem;
border: 1px solid var(--line);
border-radius: 0.5rem;
background: var(--surface);
}
section {
margin-block-end: 2rem;
}
button,
input {
font: inherit;
}
button,
input {
padding: 0.5rem 0.75rem;
}
:focus-visible {
outline: 3px solid var(--link);
outline-offset: 3px;
}
form {
display: grid;
max-width: 28rem;
gap: 0.5rem;
}site.js — JS-01
export function validateEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
}
if (typeof document !== 'undefined') {
const form = document.querySelector('#signup-form');
const email = document.querySelector('#demo-email');
const result = document.querySelector('#signup-result');
const toggle = document.querySelector('#theme-toggle');
if (form && email && result) {
form.addEventListener('submit', (event) => {
event.preventDefault();
result.textContent = validateEmail(email.value)
? 'Valid format. This demo sent nothing.'
: 'Enter a complete fictional email address.';
});
}
if (toggle) {
toggle.addEventListener('click', () => {
const enabled = document.documentElement.dataset.theme !== 'dark';
document.documentElement.dataset.theme = enabled ? 'dark' : 'light';
toggle.setAttribute('aria-pressed', String(enabled));
toggle.textContent = enabled ? 'Use light mode' : 'Use dark mode';
});
}
}package.json — PACKAGE-01
{
"name": "gittree-features",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22"
},
"scripts": {
"lint": "node scripts/lint.mjs",
"test": "node --test",
"build": "node scripts/build.mjs",
"start": "node scripts/serve.mjs"
}
}package-lock.json — LOCK-01
{
"name": "gittree-features",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gittree-features",
"version": "0.1.0",
"engines": {
"node": ">=22"
}
}
}
}tests/site.test.mjs — TEST-01
import assert from 'node:assert/strict';
import test from 'node:test';
import { validateEmail } from '../site.js';
test('accepts the fictional team address', () => {
assert.equal(validateEmail('maya@example.com'), true);
});
test('trims surrounding whitespace', () => {
assert.equal(validateEmail(' leo@example.com '), true);
});
test('rejects a missing domain', () => {
assert.equal(validateEmail('omar@'), false);
});
test('rejects a missing local part', () => {
assert.equal(validateEmail('@example.com'), false);
});
test('rejects spaces inside an address', () => {
assert.equal(validateEmail('sara name@example.com'), false);
});
test('rejects input without an at sign', () => {
assert.equal(validateEmail('priya.example.com'), false);
});scripts/lint.mjs — LINT-01
import { readFile } from 'node:fs/promises';
const files = ['index.html', 'styles.css', 'site.js'];
const problems = [];
for (const file of files) {
const source = await readFile(file, 'utf8');
if (!source.endsWith('\n')) problems.push(file + ': missing final newline');
const lines = source.split('\n');
for (const [index, line] of lines.entries()) {
if (/[\t ]+$/.test(line)) {
problems.push(file + ':' + (index + 1) + ': trailing whitespace');
}
}
}
if (problems.length > 0) {
process.stderr.write(problems.join('\n') + '\n');
process.exitCode = 1;
} else {
process.stdout.write('Source formatting checks passed.\n');
}scripts/build.mjs — BUILD-01
import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
import path from 'node:path';
const output = path.resolve('dist');
const queue = ['index.html', 'styles.css', 'site.js', 'assets', 'vendor/snippets'];
await mkdir(output, { recursive: true });
while (queue.length > 0) {
const source = queue.pop();
let details;
try {
details = await stat(source);
} catch (error) {
if (error.code === 'ENOENT' && (source === 'assets' || source === 'vendor/snippets')) continue;
throw error;
}
if (details.isDirectory()) {
const entries = await readdir(source, { withFileTypes: true });
for (const entry of entries) {
if (entry.name !== '.git') queue.push(path.join(source, entry.name));
}
continue;
}
const target = path.join(output, source);
await mkdir(path.dirname(target), { recursive: true });
await copyFile(source, target);
}
process.stdout.write('Built the static site in dist/.\n');scripts/serve.mjs — SERVE-01
import { readFile, stat } from 'node:fs/promises';
import http from 'node:http';
import path from 'node:path';
const root = path.resolve('dist');
const port = Number(process.env.PORT ?? 8080);
const types = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.bmp': 'image/bmp',
};
await stat(path.join(root, 'index.html'));
const server = http.createServer(async (request, response) => {
try {
const address = new URL(request.url, 'http://localhost');
const pathname = decodeURIComponent(address.pathname);
const target = path.resolve(root, '.' + (pathname === '/' ? '/index.html' : pathname));
const extension = path.extname(target);
if (!target.startsWith(root + path.sep) || !Object.hasOwn(types, extension)) {
response.writeHead(404).end('Not found');
return;
}
const body = await readFile(target);
response.writeHead(200, {
'Content-Type': types[extension],
'X-Content-Type-Options': 'nosniff',
}).end(body);
} catch {
response.writeHead(404).end('Not found');
}
});
server.listen(port, '127.0.0.1', () => {
process.stdout.write('Demo site: http://127.0.0.1:' + port + '\n');
});
process.on('SIGINT', () => server.close());
process.on('SIGTERM', () => server.close());.gitignore — IGNORE-BUILD
dist/In GitTree
- In the original repository, use Command Palette… (Command/Ctrl+Shift+P) → Show Working Changes. Click index.html in Unstaged Files. Before replacing it, identify the retained supporting-line edit from lesson 5.
- Use Show Files (Command/Ctrl+Shift+E); focus index.html in the file tree and press Enter. Replace its working contents with HTML-02, then Save (Command/Ctrl+S in the editor). Wait for the unsaved indicator to clear.
- For each other full file above: palette New File… → Path from the project folder, type the exact root-relative path → Create, paste its own complete body → Save. Nested paths create their missing directories. Existing-file refusal means inspect that file, not overwrite unknown work.
- Back to Graph → palette Show Working Changes. Review the untracked files as well as index.html. Use Stage File on exactly the ten changed paths named above, not generated dist output. Leave the existing README untouched.
- Click each path under Staged Files. Use Hunk, Inline and Split as available in Diff view. Palette equivalents are Show Diff as Hunks, Show Diff Inline, Show Diff Side by Side. Next change is F7; Previous change is Shift+F7. Old remains left and new right in Split even in Arabic.
- Inspect Ignore whitespace / palette Ignore Whitespace in Diffs, then restore it off. Hidden whitespace is not absent content; partial selection can be unavailable when whitespace changes are hidden. Do not use a display filter as evidence of a clean index.
- Run the native checks below in the original folder. Then enter Summary
feat: add styling and local project checks, keep amend/push/empty/skip-hooks off, and choose Commit Changes to 10 Files. Command/Ctrl+Enter is an alternative submission, not a second step. Verify two commits and clean tracked status.
In the terminal
At the input checkpoint, this read-only query captures the still-uncommitted lesson 5 edit:
git diff -- index.htmlObserved full output:
diff --git a/index.html b/index.html
index 5470374..27f8718 100644
--- a/index.html
+++ b/index.html
@@ -13,7 +13,7 @@
<section class="hero" aria-labelledby="hero-title">
<p class="eyebrow">GitTree Learn demo project</p>
<h1 id="hero-title">Understand every change.</h1>
- <p id="hero-copy">A clear view of Git history.</p>
+ <p id="hero-copy">Choose one change, then check what Git saved.</p>
</section>
</main>
<footer><p>Built by a fictional team for GitTree Learn.</p></footer>After saving all payloads, before staging:
git status --short M index.html
?? .gitignore
?? package-lock.json
?? package.json
?? scripts/
?? site.js
?? styles.css
?? tests/Directory entries scripts/ and tests/ are grouped untracked status, not evidence that their file bodies were reviewed.
Run checks; no dependency installation or network is needed because these scripts use only Node built-ins:
npm run --silent lint
npm test -- --test-reporter=tap
npm run --silent build
git check-ignore dist/index.htmlObserved lint output:
Source formatting checks passed.Observed test-summary excerpt, excluding per-test timing:
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0Observed build and ignore outputs, one command each:
Built the static site in dist/.
dist/index.htmlThe written npm start script binds loopback and serves dist. It is long-running; do not confuse a server process with a passing test. Its live HTTP behavior still needs a recording-environment check; no server take is asserted here.
Native stage/commit alternative, only if the app has not already performed those mutations:
git add -- .gitignore index.html styles.css site.js package.json package-lock.json tests/site.test.mjs scripts/lint.mjs scripts/build.mjs scripts/serve.mjs
git diff --cached --stat
git commit --quiet -m "feat: add styling and local project checks"
git rev-list --count HEAD
git status --shortThe quiet commit succeeded silently; count prints 2, and final short status is empty. The stat is a review aid, not a substitute for reading all ten file bodies. dist is ignored, not committed; no remote was added.
Separate image-comparison variant
Prepare an isolated copy of this same GitTree Features checkpoint, never a second unrelated example or the continuing original. Add assets/diff-card.png with the before bytes, make a clearly labeled fixture-only commit, then replace its working bytes with after. No image file or final media is delivered by these Markdown files.
Both PNGs have a blue left half. Before has a white right half; after has a green bottom-right quarter. Exact complete bytes:
| Version | Dimensions | Bytes | SHA-256 |
|---|---|---|---|
| before | 64 × 64 | 162 | 9a09ba97c890da0d542166ba17882bea8f7bf9b9bd4f5ec068a156f6b7256773 |
| after | 64 × 64 | 171 | 2ddabb4b9859aa53ff9c433a48cb7db0213e184b425a86fef20d8502eed8974f |
before — full PNG Base64 bytes
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAaUlEQVR4nO3QAQkAAAjAMEtYyr7m0xgi7PACi6yey68LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4D/AAqNletEZdhfWAAAAAElFTkSuQmCCafter — full PNG Base64 bytes
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAcklEQVR4nO3QQQ0AIBAEsTOBA9QQ1OANfaCC7INOMgZabeyTPF0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ7f14wOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfABwAT8dDllEqFx1AAAAAElFTkSuQmCCA recording operator can decode one payload at a time in the isolated copy:
node --input-type=module -e "import {mkdirSync,writeFileSync} from 'node:fs'; mkdirSync('assets',{recursive:true}); writeFileSync('assets/diff-card.png',Buffer.from('<selected-full-base64>','base64'));"Replace the angle-bracket placeholder with one full payload, without the brackets. Decode before, stage/commit the fixture, then decode after without staging. The supplied byte counts/digests identify each state; never reuse a fabricated image screenshot.
In that variant, native:
git diff --numstat -- assets/diff-card.pngObserved in the binary probe:
- - assets/diff-card.pngDashes mean Git reports binary data, not zero changed pixels.
In the real app: Show Working Changes → click assets/diff-card.png in Unstaged Files → Side by side → Swipe, move Swipe position with arrow keys → Onion skin, change After image opacity. Palette equivalents: Show Image Changes Side by Side, Show Image Changes with a Swipe, Show Image Changes as Onion Skin. Both sides must actually decode; absent/too-large/not-an-image is a different valid state, not a reason to fake the image. Source recognizes PNG/JPEG/GIF/WebP, not BMP; the later LFS BMP fixture must not be promised this image view.
Return to the original, the checkpoint for the next lesson. The image variant's extra commit/assets never enter canonical history.
Result
Two commits on main; README plus the ten declared inputs tracked; index/working files agree with HEAD; generated dist may exist but is ignored. No image fixture or remote in the canonical tree.
Common mistakes
- Empty ordinary diff while new files are untracked: inspect status and staged additions.
- Reading --stat as correctness: inspect actual content and both comparison endpoints.
- Treating whitespace hiding as deleting whitespace: it is only a view.
- Committing dist or the image-side fixture: stage the exact canonical list.
If something goes wrong
A failed lint/test/build is a real signal; retain its output and fix only the stated demo input. Do not skip checks or delete unknown folders. A blocked/unsupported image should remain an honest binary/size notice until its actual format/size is reconciled. Cancel a stale partial-selection request and reload the current diff rather than forcing a patch.
Check your understanding
Answer each question, then read why. Questions marked “Required” count toward completing the lesson.
0 of 6 correct
Answer the required questions correctly to complete the lesson.
Checked in this browser.
Practice
At the original, verify count 2, empty working/cached diff and ignored dist. In the separate image variant, name which quadrant changed and why native numstat reports dashes. Changed input: if a new file remains untracked instead of staged, predict that ordinary diff can still omit it.
Your own confirmation: the site cannot see your computer.
To complete the lesson, answer the required questions correctly and confirm the practical exercise.
Recap
- Name both endpoints before reading a diff.
- Text, binary and decoded image views provide different evidence.
- Checks and exact staging protect the runnable checkpoint.
Related
Glossary terms introduced
Diff — فروق; hunk — كتلة; index — الفهرس; working tree — مجلد العمل; binary — ثنائي; untracked — غير مُتتبَّع.
Video transcript
The narration of the video, cue by cue.
Show the transcript
- Read the comparison before judging the change.
- The later hero line is still working content, not a new commit.
- Ordinary and staged diffs compare different snapshots.
- Add the full runnable inputs, keeping the existing supporting-line edit.
- New files can be missing from ordinary diff until they enter the index.
- Stage the declared inputs, not generated build output.
- The layout changes your view, not the compared bytes.
- Follow the surrounding text before interpreting an addition or removal.
- Hiding whitespace is not deleting a whitespace change.
- Run the local formatting check before committing.
- All six input-validation tests pass in this fixture.
- Build output belongs in the ignored directory, not the commit.
- Commit the reviewed runnable project locally.
- Verify two commits and no remaining tracked difference.
- Use a separate copy for image comparison, not the continuing checkpoint.
- The two exact PNG payloads identify the image states.
- Compare the named sides and their actual dimensions.
- Move the divider without changing either file.
- Opacity changes the comparison, not the saved pixels.
- Git's binary summary is not a count of changed pixels.
- An unsupported or oversized file must keep its honest notice.
- Keep the image fixture outside the canonical history.
- Predict which comparison shows a newly staged file.
- Next, select hunks and lines without discarding other work.