Courses Vibe Coding Dashboard Lesson 7.3
7.3 Module 7 · The Development Loop

Version Control for Beginners

Git concepts explained visually — see how version control saves snapshots of your project. Practise creating save points, comparing versions, and rolling back to previous states.

Visual Git Timeline Rollback Practice Lab

Visual Git Timeline

Create commits (save points) with messages, click any node to view what changed, and roll back to a previous state. This is how version control works — every save is a snapshot you can return to.

1commits
3files tracked

Key insight: Version control is not just for teams — it is your personal safety net. Every commit is a snapshot you can return to if something breaks. Think of it as an unlimited undo history for your entire project. Even solo vibe coders should commit after every working state.

Rollback Practice Lab

Something broke! In each scenario, read the problem description and select the correct commit to roll back to. Identify which save point would fix the issue without losing too much work.

0 / 4 solved

How Branches Work

Branches let you experiment without affecting your main project. Click each branch to learn what it does and see how changes flow between them.

Click a branch in the diagram above to learn what it represents.

Previous Lesson Next: Testing Like a Professional
\n', 'style.css': 'body {\n font-family: sans-serif;\n margin: 0;\n padding: 20px;\n}', 'app.js': 'console.log("App started");' }; const fileChanges = [ {adds:['',''],removes:[]}, {adds:['.nav { display: flex; gap: 1rem; }','.btn { padding: 8px 16px; }'],removes:[' margin: 0;']}, {adds:['function handleClick(e) {',' alert("Clicked: " + e.target.id);','}'],removes:['console.log("App started");']}, {adds:['','
Welcome
'],removes:['

Hello World

']}, {adds:['@media (max-width: 640px) {',' .nav { flex-direction: column; }','}'],removes:[]}, {adds:['document.addEventListener("DOMContentLoaded", () => {',' initApp();','});'],removes:['function handleClick(e) {']}, {adds:[''],removes:['My App']}, {adds:['const API_URL = "/api/data";','async function fetchData() { }'],removes:[' alert("Clicked: " + e.target.id);']} ]; let commits = [ { hash: 'a1b2c3d', msg: 'Initial project setup', time: new Date(Date.now() - 7200000), changes: {adds:['','body { font-family: sans-serif; }','console.log("App started");'],removes:[]} } ]; let selectedCommit = 0; let rollbackTarget = null; let nextChangeIdx = 0; function genHash() { return Math.random().toString(16).substr(2,7); } function renderTimeline() { const track = document.getElementById('timeline-track'); track.innerHTML = ''; commits.forEach((c, i) => { if (i > 0) { const conn = document.createElement('div'); conn.className = 'timeline-connector'; track.appendChild(conn); } const node = document.createElement('div'); node.className = 'timeline-node' + (i === selectedCommit ? ' active' : ''); node.innerHTML = `${i + 1}`; node.title = c.msg; node.addEventListener('click', () => { selectedCommit = i; renderTimeline(); showCommitDetail(i); }); // Tooltip label const label = document.createElement('div'); label.className = 'absolute -bottom-8 left-1/2 -translate-x-1/2 text-[9px] text-white/50 whitespace-nowrap max-w-[80px] truncate text-center'; label.textContent = c.msg.length > 12 ? c.msg.substring(0,12) + '...' : c.msg; node.appendChild(label); track.appendChild(node); }); document.getElementById('commit-count').textContent = commits.length; document.getElementById('file-count').textContent = 3 + Math.min(commits.length - 1, 3); } function showCommitDetail(idx) { const c = commits[idx]; const panel = document.getElementById('commit-detail'); panel.style.display = ''; document.getElementById('detail-hash').textContent = '#' + c.hash; document.getElementById('detail-msg').textContent = c.msg; document.getElementById('detail-time').textContent = c.time.toLocaleTimeString() + ' — ' + c.time.toLocaleDateString(); const diffEl = document.getElementById('detail-diff'); diffEl.innerHTML = ''; if (c.changes.removes.length > 0) { c.changes.removes.forEach(r => { diffEl.innerHTML += `
- ${r}
`; }); } if (c.changes.adds.length > 0) { c.changes.adds.forEach(a => { diffEl.innerHTML += `
+ ${a}
`; }); } document.getElementById('rollback-btn').style.display = idx < commits.length - 1 ? '' : 'none'; document.getElementById('rollback-confirm').style.display = 'none'; rollbackTarget = idx; } document.getElementById('create-commit').addEventListener('click', () => { const input = document.getElementById('commit-msg'); const msg = input.value.trim(); if (!msg) { input.focus(); return; } const changes = fileChanges[nextChangeIdx % fileChanges.length]; nextChangeIdx++; commits.push({ hash: genHash(), msg, time: new Date(), changes }); input.value = ''; selectedCommit = commits.length - 1; renderTimeline(); showCommitDetail(selectedCommit); }); document.getElementById('commit-msg').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('create-commit').click(); }); document.getElementById('rollback-btn').addEventListener('click', () => { document.getElementById('rollback-confirm').style.display = ''; }); document.getElementById('cancel-rollback').addEventListener('click', () => { document.getElementById('rollback-confirm').style.display = 'none'; }); document.getElementById('confirm-rollback').addEventListener('click', () => { if (rollbackTarget !== null && rollbackTarget < commits.length - 1) { commits = commits.slice(0, rollbackTarget + 1); selectedCommit = commits.length - 1; renderTimeline(); showCommitDetail(selectedCommit); document.getElementById('rollback-confirm').style.display = 'none'; } }); // ══════════════════════════════════════════ // TOOL 2: ROLLBACK PRACTICE LAB // ══════════════════════════════════════════ const scenarios = [ { id: 's1', title: 'Broken Navigation', problem: 'After adding a dropdown menu, all navigation links stopped working. The dropdown code introduced a JavaScript error that prevents any click events from firing.', commits: [ { hash: 'f3a1', msg: 'Added basic nav links', correct: false }, { hash: 'b7c2', msg: 'Styled nav with flexbox', correct: false }, { hash: 'd9e3', msg: 'Added dropdown menu JS', correct: true }, { hash: 'a2f4', msg: 'Added footer content', correct: false } ], correctIdx: 2, explanation: 'The dropdown JS in commit #3 is where the bug was introduced. Rolling back to just before this commit (keeping commits 1 and 2) restores working navigation. You would then re-implement the dropdown more carefully.' }, { id: 's2', title: 'Missing Styles', problem: 'The entire page lost its styling after a CSS refactor. All colours, spacing, and typography reverted to browser defaults. The page is now unstyled plain HTML.', commits: [ { hash: 'c1d1', msg: 'Initial page layout', correct: false }, { hash: 'e2f2', msg: 'Added colour theme', correct: false }, { hash: 'g3h3', msg: 'Refactored CSS to modules', correct: true }, { hash: 'i4j4', msg: 'Added contact form', correct: false } ], correctIdx: 2, explanation: 'The CSS refactor in commit #3 broke the stylesheet imports. Rolling back to before this commit restores the working styles. The modular CSS approach was correct in principle but had an import path error.' }, { id: 's3', title: 'Data Loss Bug', problem: 'Users report that saving their profile now erases their previously saved preferences. The save function is overwriting the entire user object instead of merging changes.', commits: [ { hash: 'k5l5', msg: 'Built user profile page', correct: false }, { hash: 'm6n6', msg: 'Added preference settings', correct: false }, { hash: 'o7p7', msg: 'Optimised save function', correct: true }, { hash: 'q8r8', msg: 'Added avatar upload', correct: false } ], correctIdx: 2, explanation: 'The "optimised" save function in commit #3 replaced a merge operation with a full overwrite. Rolling back to before this commit restores the correct save behaviour that preserved existing preferences.' }, { id: 's4', title: 'Mobile Layout Broken', problem: 'The website looks perfect on desktop but is completely unusable on mobile. Elements overflow the screen, text is unreadable, and buttons are impossible to tap.', commits: [ { hash: 's9t9', msg: 'Responsive grid layout', correct: false }, { hash: 'u0v0', msg: 'Added desktop animations', correct: false }, { hash: 'w1x1', msg: 'Fixed viewport and media queries', correct: false }, { hash: 'y2z2', msg: 'Replaced flex with fixed widths', correct: true } ], correctIdx: 3, explanation: 'Commit #4 replaced flexible layouts with fixed pixel widths, which broke responsiveness. Rolling back to before this commit restores the flex-based layout that worked on all screen sizes.' } ]; let solvedScenarios = new Set(); let scenarioSelections = {}; function renderScenarios() { const grid = document.getElementById('scenarios-grid'); grid.innerHTML = scenarios.map((s, si) => { const solved = solvedScenarios.has(s.id); const selection = scenarioSelections[s.id]; return `
Scenario ${si + 1} ${s.title} ${solved ? '' : ''}

${s.problem}

Commit History — which commit caused the bug?
${s.commits.map((c, ci) => { let cls = 'commit-option'; if (solved && ci === s.correctIdx) cls += ' correct'; else if (selection !== undefined && selection === ci && !solved) cls += ' selected'; else if (selection !== undefined && selection === ci && solved) cls += ' wrong'; return `
#${c.hash} ${c.msg}
`; }).join('')}
${!solved && selection !== undefined ? `` : ''} ${solved ? `

${s.explanation}

` : ''}
`; }).join(''); // Bind events grid.querySelectorAll('.commit-option').forEach(opt => { opt.addEventListener('click', () => { const sid = opt.dataset.scenario; if (solvedScenarios.has(sid)) return; scenarioSelections[sid] = parseInt(opt.dataset.ci); renderScenarios(); }); }); grid.querySelectorAll('[data-check]').forEach(btn => { btn.addEventListener('click', () => { const sid = btn.dataset.check; const s = scenarios.find(x => x.id === sid); if (scenarioSelections[sid] === s.correctIdx) { solvedScenarios.add(sid); } else { // Show incorrect briefly then allow retry const card = btn.closest('.scenario-card'); card.classList.add('incorrect'); setTimeout(() => { card.classList.remove('incorrect'); }, 600); delete scenarioSelections[sid]; } renderScenarios(); updateScenarioProgress(); }); }); } function updateScenarioProgress() { const total = scenarios.length; const solved = solvedScenarios.size; const pct = Math.round((solved / total) * 100); document.getElementById('scenario-progress').style.width = pct + '%'; document.getElementById('scenario-score').textContent = `${solved} / ${total} solved`; } // ══════════════════════════════════════════ // BRANCH DIAGRAM // ══════════════════════════════════════════ const branches = [ { name: 'main', color: '#84cc16', y: 40, desc: 'The main branch is your production-ready code. It should always be in a working state. All tested and approved changes eventually merge here.' }, { name: 'feature/login', color: '#a3e635', y: 110, desc: 'Feature branches are where you build new functionality. They branch off main, and you work on them without affecting the stable codebase. When the feature is ready and tested, it merges back into main.' }, { name: 'bugfix/nav', color: '#bef264', y: 180, desc: 'Bugfix branches are created to fix specific issues. They are usually short-lived — branch off, fix the bug, test it, and merge back. This keeps your main branch clean while you work on the fix.' } ]; function renderBranchDiagram() { const svg = document.getElementById('branch-diagram'); let html = ''; // Main line html += ``; // Main nodes [100,200,300,400,500,600].forEach(x => { html += ``; }); // Feature branch html += ``; [260,340,460].forEach(x => { html += ``; }); // Bugfix branch html += ``; [360,440].forEach(x => { html += ``; }); // Labels html += `main`; html += `feature/login`; html += `bugfix/nav`; svg.innerHTML = html; // Click handlers svg.querySelectorAll('[data-branch]').forEach(el => { el.addEventListener('click', () => { const name = el.dataset.branch; const b = branches.find(x => x.name === name); if (b) { document.getElementById('branch-info').innerHTML = `
${b.name}

${b.desc}

`; } }); }); } // ══════════════════════════════════════════ // INIT // ══════════════════════════════════════════ renderTimeline(); renderScenarios(); updateScenarioProgress(); renderBranchDiagram(); // ── Nav scroll ── const nav = document.getElementById('main-nav'); window.addEventListener('scroll', () => { nav.classList.toggle('nav-scrolled', window.scrollY > 30); }); // ── Mobile menu ── document.getElementById('mobile-toggle').addEventListener('click', () => { document.getElementById('mobile-menu').classList.toggle('hidden'); });