Skip to content
GitTree

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.

Progress saved in this browser.

Playing loads the video from YouTube (privacy-enhanced mode).

English narration · English and Arabic captions

Open the video pageWatch on YouTube

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

File excerpt · html
<!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

File excerpt · css
: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

File excerpt · javascript
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

File excerpt · json
{
  "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

File excerpt · json
{
  "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

File excerpt · javascript
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

File excerpt · javascript
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

File excerpt · javascript
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

File excerpt · javascript
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

File excerpt · gitignore
dist/

In GitTree

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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:

Command · bash
git diff -- index.html

Observed full output:

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:

Command · bash
git status --short
Output
 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:

Command · bash
npm run --silent lint
npm test -- --test-reporter=tap
npm run --silent build
git check-ignore dist/index.html

Observed lint output:

Output
Source formatting checks passed.

Observed test-summary excerpt, excluding per-test timing:

Output
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0

Observed build and ignore outputs, one command each:

Output
Built the static site in dist/.
dist/index.html

The 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:

Command · bash
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 --short

The 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:

VersionDimensionsBytesSHA-256
before64 × 641629a09ba97c890da0d542166ba17882bea8f7bf9b9bd4f5ec068a156f6b7256773
after64 × 641712ddabb4b9859aa53ff9c433a48cb7db0213e184b425a86fef20d8502eed8974f

before — full PNG Base64 bytes

Output
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAaUlEQVR4nO3QAQkAAAjAMEtYyr7m0xgi7PACi6yey68LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4D/AAqNletEZdhfWAAAAAElFTkSuQmCC

after — full PNG Base64 bytes

Output
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAcklEQVR4nO3QQQ0AIBAEsTOBA9QQ1OANfaCC7INOMgZabeyTPF0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ7f14wOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfABwAT8dDllEqFx1AAAAAElFTkSuQmCC

A recording operator can decode one payload at a time in the isolated copy:

Command · bash
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:

Command · bash
git diff --numstat -- assets/diff-card.png

Observed in the binary probe:

Output
-	-	assets/diff-card.png

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

  1. Question 1 of 6

    What does ordinary git diff compare?

    Choose one answer.

  2. Question 2 of 6

    Which actions leave repository snapshots unchanged?

    Choose every answer that applies.

  3. Question 3 of 6
    Required

    New site.js is untracked. Ordinary diff is empty for it. Is it absent?

    Choose one answer.

  4. Question 4 of 6
    Required

    If you stage that new file, where can its full addition be reviewed?

    Choose one answer.

  5. Question 5 of 6

    Image numstat prints two dashes. What can you conclude?

    Choose one answer.

  6. Question 6 of 6
    Required

    Verify the finished checkpoint. Which facts are required together?

    Choose every answer that applies.

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.

Glossary terms introduced

Diff — فروق; hunk — كتلة; index — الفهرس; working tree — مجلد العمل; binary — ثنائي; untracked — غير مُتتبَّع.

Video transcript

The narration of the video, cue by cue.

Show the transcript
  1. Read the comparison before judging the change.
  2. The later hero line is still working content, not a new commit.
  3. Ordinary and staged diffs compare different snapshots.
  4. Add the full runnable inputs, keeping the existing supporting-line edit.
  5. New files can be missing from ordinary diff until they enter the index.
  6. Stage the declared inputs, not generated build output.
  7. The layout changes your view, not the compared bytes.
  8. Follow the surrounding text before interpreting an addition or removal.
  9. Hiding whitespace is not deleting a whitespace change.
  10. Run the local formatting check before committing.
  11. All six input-validation tests pass in this fixture.
  12. Build output belongs in the ignored directory, not the commit.
  13. Commit the reviewed runnable project locally.
  14. Verify two commits and no remaining tracked difference.
  15. Use a separate copy for image comparison, not the continuing checkpoint.
  16. The two exact PNG payloads identify the image states.
  17. Compare the named sides and their actual dimensions.
  18. Move the divider without changing either file.
  19. Opacity changes the comparison, not the saved pixels.
  20. Git's binary summary is not a count of changed pixels.
  21. An unsupported or oversized file must keep its honest notice.
  22. Keep the image fixture outside the canonical history.
  23. Predict which comparison shows a newly staged file.
  24. Next, select hunks and lines without discarding other work.