レッスン 6 · 日常の仕事
変更を判断する前に比較を読んでください
ヒーローを実行可能な静的プロジェクトに変換し、作業中の差異、ステージングされた差異、および記録された差異を区別します。継続するコース履歴にイメージコミットを追加せずに、別の決定論的なイメージバリアントを検査します。
- レベル: 初心者
- 8 分で読みました
- 5 分の動画
- 更新されました
- このレッスンの前に: 目的のスナップショットをステージングし、最初のコミットを行います
このブラウザに進行状況が保存されました。
再生すると、YouTube からビデオが読み込まれます (プライバシー強化モード)。
何を学ぶか
- テキスト比較の両側に名前を付け、追加/削除を解釈します。
- 追跡されていないファイルが通常の git diff に存在しない理由を説明します。
- Git がテキストのイメージ パッチを提供しているかのように装うことなく、イメージを視覚的に比較します。
- 実行可能なページをコミットする前に、小さなローカル チェックを追加して検証します。
始める前に
レッスン 5 が終了したところから開始します。つまり、ヒーロー コミットが 1 つあるオリジナルのメインです。 HEAD/インデックスには HTML-01 が含まれます。 workingindex.html には HTML-01-WORKING が含まれています。リモコンが存在しません。サポート行の編集を HTML-02 に保持します。捨てないでください。
ビデオは、Ubuntu 24.04 の Git 2.52.0、ノード 22.22.0、npm 10.9.4、および GitTree 2.0 を使用して録画されました。10 個のファイルは、GitTree 独自のエディターに保存され、アプリ内でステージングおよびコミットされ、ターミナル チェックはアプリが作成したものに対して、またはステージング前の状態と画像の個別のコピーに対して実行されました。ノード チェックはノード 22 (プロジェクトが宣言するバージョン) で渡されます。 Nothing was run on macOS or Windows, and Ubuntu is the only Linux system recorded. GitTree の検査された Git フロアは 2.39.0 です。 Once you are signed in to GitTree, local Git is on the Free plan and needs no provider or API credentials.
コンセプト
A diff answers a question about two snapshots, not whether a file is “good.” Ordinary git diff compares index → working tree; git diff --cached は HEAD → インデックスを比較します。 git diff HEAD は HEAD → 作業ツリーを比較します。 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.
追跡されていないファイルには通常のインデックス エントリがありません。したがって、空の通常の差分は、新しいファイルが存在しないことを証明するものではありません。ステータスや新しく追加されたファイルの内容も確認してください。 After staging a new file, its full addition can be reviewed in the cached comparison.
インライン/分割/ハンク、ラッピング、イメージ モード、または非表示の空白表示を変更すると、ファイル バイト、インデックス、コミット、リモート参照ではなく、view が変更されます。ステージングによってインデックスは変更されます。最後の通常のコミットはローカルのメインを移動します。サーバーの状態は変化しません。
完全な実行可能なプロジェクト入力
README-01 は変更しないでください。 index.html を HTML-02 に置き換え、以下の残りのファイルをすべて UTF-8/LF と最後の改行で作成します。最小限の .gitignore は意図的なものです。ビルド出力が誤ってこの最初の実行可能なコミットに入ってはいけません。より広範な無視ポリシーは後で説明します。 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>スタイル.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 — パッケージ-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"
}
}パッケージロック.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"
}
}
}
}テスト/site.test.mjs — テスト-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 — ビルドを無視する
dist/GitTreeで
- In the original repository, use Command Palette… (Command/Ctrl+Shift+P) → Show Working Changes. Unstaged Filesのindex.htmlをクリックします。 Before replacing it, identify the retained supporting-line edit from lesson 5.
- Show Files (Command/Ctrl+Shift+E) を使用します。ファイルツリー内のindex.htmlにフォーカスし、Enterを押します。作業内容を HTML-02、次に Save (エディターで Command/Ctrl+S) に置き換えます。未保存インジケーターが消えるまで待ちます。
- 上記の他の完全なファイルについては、パレット New File… → Path from the project folder、正確なルート相対パスを入力 → Create、独自の完全な本文を貼り付け → Save となります。ネストされたパスにより、不足しているディレクトリが作成されます。既存のファイルの拒否は、未知の作業を上書きするのではなく、そのファイルを検査することを意味します。
- Back to Graph → パレット Show Working Changes。追跡されていないファイルとindex.htmlを確認します。生成された dist 出力ではなく、上記で指定した 10 個の変更されたパスに Stage File を使用します。既存の README はそのままにしておきます。
- Staged Files の下の各パスをクリックします。 Diff view で利用可能な Hunk、Inline、および Split を使用します。パレットに相当するものは、Show Diff as Hunks、Show Diff Inline、Show Diff Side by Side です。 Next change は F7 です。 Previous change は Shift+F7 です。アラビア語でもスプリットでは古いものが左に、新しいものが右に残ります。
- Ignore whitespace / パレット Ignore Whitespace in Diffs を検査し、オフに戻します。隠された空白はコンテンツを欠いているわけではありません。空白の変更が非表示になっている場合、部分選択は使用できない場合があります。クリーンなインデックスの証拠として表示フィルターを使用しないでください。
- 元のフォルダーで以下のネイティブ チェックを実行します。次に、概要
feat: add styling and local project checksを入力し、修正/プッシュ/空/スキップフックをオフにして、Commit Changes to 10 Files を選択します。 Command/Ctrl+Enter は、2 番目のステップではなく、代替送信です。 2 つのコミットを確認し、追跡ステータスをクリーンアップします。
ターミナル内
この読み取り専用クエリは、input チェックポイントで、まだコミットされていないレッスン 5 の編集をキャプチャします。
git diff -- index.html観察された完全な出力:
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>すべてのペイロードを保存した後、ステージングの前に次の操作を行います。
git status --short M index.html
?? .gitignore
?? package-lock.json
?? package.json
?? scripts/
?? site.js
?? styles.css
?? tests/ディレクトリ エントリ script/ および testing/ は、ファイル本体がレビューされた証拠ではなく、追跡されていないステータスとしてグループ化されます。
チェックを実行します。これらのスクリプトはノードの組み込みのみを使用するため、依存関係のインストールやネットワークは必要ありません。
npm run --silent lint
npm test -- --test-reporter=tap
npm run --silent build
git check-ignore dist/index.html観察された lint 出力:
Source formatting checks passed.テストごとのタイミングを除いて、test-summary excerpt が観察されました:
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0観察されたビルド出力と無視出力 (それぞれ 1 つのコマンド):
Built the static site in dist/.
dist/index.html書かれた npm start スクリプトはループバックをバインドし、dist を提供します。それは長期にわたるものです。サーバープロセスと合格したテストを混同しないでください。そのライブ HTTP 動作には、引き続き記録環境のチェックが必要です。ここではサーバーの取得はアサートされません。
アプリがまだこれらのミューテーションを実行していない場合のみ、ネイティブ ステージ/コミットの代替:
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静かなコミットは静かに成功しました。 count は 2 を出力し、最終的な短いステータスは空です。この統計はレビューを支援するものであり、10 個のファイル本体すべてを読み取ることに代わるものではありません。 dist は無視され、コミットされません。リモコンは追加されませんでした。
個別の画像比較バリアント
独立した copy of this same GitTree Features checkpoint を準備し、無関係な 2 番目の例や継続するオリジナルを作成しないでください。前のバイトを含むassets/diff-card.pngを追加し、明確にラベル付けされたフィクスチャのみのコミットを作成して、その作業バイトを後のバイトに置き換えます。これらの Markdown ファイルでは、画像ファイルや最終メディアは配信されません。
どちらの PNG も左半分が青色です。前は右半分が白いです。後は右下の 4 分の 1 が緑色になります。正確な完全なバイト:
| バージョン | 寸法 | バイト | SHA-256 |
|---|---|---|---|
| 前に | 64 × 64 | 162 | 9a09ba97c890da0d542166ba17882bea8f7bf9b9bd4f5ec068a156f6b7256773 |
| その後 | 64 × 64 | 171 | 2ddabb4b9859aa53ff9c433a48cb7db0213e184b425a86fef20d8502eed8974f |
before — full PNG Base64 bytes
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAaUlEQVR4nO3QAQkAAAjAMEtYyr7m0xgi7PACi6yey68LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4D/AAqNletEZdhfWAAAAAElFTkSuQmCCafter — full PNG Base64 bytes
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAcklEQVR4nO3QQQ0AIBAEsTOBA9QQ1OANfaCC7INOMgZabeyTPF0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ7f14wOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfABwAT8dDllEqFx1AAAAAElFTkSuQmCC記録オペレーターは、分離されたコピーで一度に 1 つのペイロードをデコードできます。
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'));"山形ブラケットのプレースホルダーを、ブラケットのない 1 つの完全なペイロードに置き換えます。前にデコードし、フィクスチャをステージング/コミットし、その後ステージングなしでデコードします。提供されたバイト カウント/ダイジェストによって各状態が識別されます。捏造された画像のスクリーンショットを再利用しないでください。
そのバリアントでは、ネイティブ:
git diff --numstat -- assets/diff-card.pngバイナリ プローブで観察されたもの:
- - assets/diff-card.pngダッシュは、Git がゼロ変更ピクセルではなくバイナリ データを報告することを意味します。
実際のアプリ: Show Working Changes → Unstaged Files のassets/diff-card.pngをクリック → Side by side → Swipe、矢印キーでSwipe positionを移動 → Onion skin、After image opacityを変更します。同等のパレット: Show Image Changes Side by Side、Show Image Changes with a Swipe、Show Image Changes as Onion Skin。双方が実際にデコードする必要があります。画像が存在しない/大きすぎる/画像ではないということは、別の有効な状態であり、画像を偽装する理由にはなりません。ソースは PNG/JPEG/GIF/WebP、not BMP を認識します。以降の LFS BMP フィクスチャでは、この画像ビューを約束してはなりません。
元に戻り、次のレッスンのチェックポイントになります。イメージ バリアントの追加のコミット/アセットが正規の歴史に組み込まれることはありません。
結果
メインで 2 つのコミット。 README と宣言された 10 個の入力が追跡されます。インデックス/作業ファイルは HEAD と一致します。生成された dist は存在する可能性がありますが、無視されます。正規ツリーには画像器具やリモコンがありません。
よくある間違い
- 新しいファイルが追跡されていない間は、通常の差分を空にします。ステータスと段階的な追加を検査します。
- --stat を正確に読み取る: 実際のコンテンツと両方の比較エンドポイントを検査します。
- 空白の非表示を空白の削除として扱います。これは単なるビューです。
- dist またはイメージ側フィクスチャをコミットする: 正確な正規リストをステージングします。
何か問題が発生した場合
lint/テスト/ビルドの失敗は実際の信号です。その出力を保持し、記載されているデモ入力のみを修正します。チェックを省略したり、不明なフォルダーを削除したりしないでください。ブロックされている/サポートされていない画像は、実際の形式/サイズが調整されるまで、正直なバイナリ/サイズ通知のままである必要があります。パッチを強制するのではなく、古い部分選択リクエストをキャンセルし、現在の差分を再ロードします。
理解度を確認してください
各質問に答えてから、その理由を読みましょう。 「必須」とマークされた質問は、レッスンの完了にカウントされます。
6 個中 0 個が正解です
必須の質問に正しく答えると、レッスンを完了できます。
このブラウザで確認しました。
練習する
オリジナルでは、カウント 2、空の作業/キャッシュされた diff および無視された dist を確認します。別のイメージ バリアントで、変更された象限の名前と、ネイティブ numstat がダッシュを報告する理由を示します。入力の変更: 新しいファイルがステージングされずに追跡されないままの場合、通常の差分は引き続き省略される可能性があると予測します。
あなた自身の確認: サイトはあなたのコンピュータを認識できません。
レッスンを完了するには、必須の質問に正しく答え、実践的な演習を確認してください。
要約
- 差分を読み取る前に、両方のエンドポイントに名前を付けます。
- テキスト、バイナリ、およびデコードされた画像ビューは、さまざまな証拠を提供します。
- チェックと正確なステージングにより、実行可能なチェックポイントが保護されます。
関連している
用語集の用語の紹介
差分 — فروق;塊 — كتلة;インデックス — 番号;ワーキングツリー — مجلد العمل;バイナリ — バイナリ;追跡されていません — غير مُتتبَّع。
ビデオトランスクリプト
ビデオのナレーションが次々と流れます。
トランスクリプトを表示する
- 比較を読む 変化を判断する前に。
- 後のヒーローラインはまだです 新しいコミットではなく、作業中のコンテンツ。
- 通常の差分と段階的な差分 さまざまなスナップショットを比較します。
- 実行可能な入力をすべて追加します。 既存のサポートラインの編集。
- 新しいファイルが通常のファイルから失われる可能性があります インデックスに入るまで diff します。
- 宣言された入力をステージングします。 ビルド出力は生成されません。
- レイアウトが変わると、 比較されたバイトではなく表示されます。
- 前後のテキストに従ってください 追加または削除の解釈。
- 空白を隠すことはできません 空白の変更を削除します。
- ローカルフォーマットを実行する コミットする前に確認してください。
- 6 つすべての入力検証 このフィクスチャではテストに合格します。
- ビルド出力は次の場所に属します。 コミットではなくディレクトリを無視しました。
- レビューした内容をコミットする ローカルで実行可能なプロジェクト。
- 2 つのコミットと何もないことを確認します。 残りの追跡差。
- 画像比較には別のコピーを使用します。 継続チェックポイントではありません。
- 2 つの正確な PNG ペイロード 画像の状態を識別します。
- 名前付きの側面を比較し、 実際の寸法。
- ディバイダを移動せずに いずれかのファイルを変更します。
- 不透明度によって比較が変化します。 保存されたピクセルではありません。
- Git のバイナリ サマリは、 変更されたピクセルの数。
- サポートされていないファイルまたはサイズが大きすぎるファイル 正直な通知を守らなければなりません。
- 画像治具を保管しておいてください 正史の外側。
- どの比較を予測するか は、新しくステージングされたファイルを示しています。
- 次に、ハンクとラインを選択します 他の仕事を放棄することなく。