← Back to Tools

Git Command Wizard

Broke something? Pick your problem → get the exact fix. No Googling, no guessing.

Git Safety Rules — Before You Run Anything

Half of Git panic comes from running commands without understanding what they do. Here's the cheat sheet.

Always check status first

Run git status and git log --oneline -5 before any undo operation. Know your current state before changing it.

--hard = data loss risk

Any command with --hard discards uncommitted changes permanently. There's no Ctrl+Z. If unsure, stash first: git stash.

Never force-push to main/master

git push --force on a shared branch rewrites history for everyone. Use --force-with-lease at minimum — it fails if others have pushed since you last pulled.

Reflog is your safety net

Git keeps a local log of every HEAD move for ~90 days. Run git reflog to find any commit you think you've lost — then git checkout <hash>.

er.jsx' } ], gen(f) { const fp = f.file.trim() || ''; return `git checkout -- ${fp}\n# Or (modern Git):\ngit restore ${fp}`; }, explain() { return 'This throws away all unsaved working-directory changes in that file. The file goes back to exactly what the last commit says. No undo.'; } }, { id: 'unstage', cat: 'undo', icon: '📤', label: 'Unstage a file', desc: 'You ran git add by mistake — take a file out of staging', danger: 'low', dangerLabel: '✅ Safe — no data lost', fields: [ { id:'file', type:'text', label:'File path (leave blank for all)', placeholder:'src/utils.js' } ], gen(f) { const fp = f.file.trim(); return fp ? `git restore --staged ${fp}` : `git restore --staged .`; }, explain() { return 'Moves the file out of the staging area back to your working directory. Changes are NOT deleted — just un-staged.'; } }, { id: 'revert-commit', cat: 'undo', icon: '🔄', label: 'Revert a pushed commit', desc: 'Safely undo a commit that\'s already on the remote', danger: 'low', dangerLabel: '✅ Safe — creates new commit', fields: [ { id:'hash', type:'text', label:'Commit hash (from git log)', placeholder:'a3f1c9d' } ], gen(f) { const h = f.hash.trim() || ''; return `git revert ${h}\ngit push`; }, explain() { return 'git revert creates a new commit that undoes the target commit. History is preserved — safe for shared/main branches. Never rewrites existing commits.'; } }, /* ── BRANCHES ── */ { id: 'delete-local-branch', cat: 'branch', icon: '🗑️', label: 'Delete a local branch', desc: 'Clean up branches you\'re done with', danger: 'med', dangerLabel: '⚠️ Unmerged data may be lost', fields: [ { id:'branch', type:'text', label:'Branch name', placeholder:'feature/old-login' }, { id:'force', type:'select', label:'Force delete?', options:[ {v:'safe', l:'No — only if merged (-d)'}, {v:'force', l:'Yes — even if unmerged (-D)'} ]} ], gen(f) { const b = f.branch.trim() || ''; const flag = f.force === 'force' ? '-D' : '-d'; return `git branch ${flag} ${b}`; }, explain(f) { if(f.force==='force') return 'The -D flag force-deletes even with unmerged commits. Those commits become orphaned and will be garbage-collected eventually. Use git reflog to recover if needed.'; return '-d is the safe flag — Git refuses to delete if there are unmerged commits, protecting your work.'; } }, { id: 'rename-branch', cat: 'branch', icon: '✏️', label: 'Rename a branch', desc: 'Rename local (and optionally remote) branch', danger: 'med', dangerLabel: '⚠️ Updates remote refs', fields: [ { id:'old', type:'text', label:'Current branch name', placeholder:'feture/login' }, { id:'new', type:'text', label:'New branch name', placeholder:'feature/login' }, { id:'remote', type:'select', label:'Also rename on remote?', options:[ {v:'yes', l:'Yes (push + delete old)'}, {v:'no', l:'Local only'} ]} ], gen(f) { const o = f.old.trim() || ''; const n = f.new.trim() || ''; let cmd = `git branch -m ${o} ${n}`; if(f.remote === 'yes') cmd += `\ngit push origin --delete ${o}\ngit push origin ${n}\ngit branch --set-upstream-to=origin/${n} ${n}`; return cmd; }, explain(f) { if(f.remote==='yes') return 'This renames locally, deletes the old remote branch, pushes the new one, and updates your tracking reference. Anyone else who has the old branch checked out will need to update their remote refs.'; return 'Renames only your local branch. Remote stays the same until you push.'; } }, { id: 'switch-branch', cat: 'branch', icon: '🌿', label: 'Create & switch to new branch', desc: 'Start fresh work on a new branch from a base', danger: 'low', dangerLabel: '✅ Safe', fields: [ { id:'name', type:'text', label:'New branch name', placeholder:'feature/dark-mode' }, { id:'base', type:'text', label:'Base branch (default: current)', placeholder:'main' } ], gen(f) { const n = f.name.trim() || ''; const b = f.base.trim(); return b ? `git checkout ${b}\ngit checkout -b ${n}` : `git checkout -b ${n}`; }, explain() { return 'Creates and immediately switches to the new branch. If you specified a base, it first checks that out so your new branch starts from the right point.'; } }, { id: 'commit-wrong-branch', cat: 'branch', icon: '😱', label: 'Committed to wrong branch', desc: 'Move your last commit(s) to the correct branch', danger: 'med', dangerLabel: '⚠️ Rewrites current branch history', fields: [ { id:'target', type:'text', label:'Branch where commit should go', placeholder:'feature/auth' }, { id:'n', type:'select', label:'How many commits to move?', options:[ {v:'1',l:'Last 1 commit'},{v:'2',l:'Last 2 commits'},{v:'3',l:'Last 3 commits'} ]} ], gen(f) { const t = f.target.trim() || ''; const n = f.n || '1'; return `# 1. Note the commit hash(es) you need to move:\ngit log --oneline -5\n\n# 2. Switch to target branch and cherry-pick:\ngit checkout ${t}\ngit cherry-pick HEAD~${n === '1' ? '' : `${parseInt(n)-1}..`}HEAD\n\n# 3. Go back and remove from wrong branch:\ngit checkout -\ngit reset --hard HEAD~${n}`; }, explain() { return 'Uses cherry-pick to copy the commit(s) onto the right branch, then removes them from the wrong branch with reset --hard. Step 1 is critical — check the log first.'; }, steps: true }, /* ── REMOTE ── */ { id: 'undo-push', cat: 'remote', icon: '☁️', label: 'Undo a push to remote', desc: 'You pushed something bad — recover if others haven\'t pulled yet', danger: 'high', dangerLabel: '🔴 Danger — rewrites shared history', fields: [ { id:'branch', type:'text', label:'Branch name', placeholder:'main' }, { id:'n', type:'select', label:'Commits to undo', options:[ {v:'1',l:'1 commit'},{v:'2',l:'2 commits'},{v:'3',l:'3 commits'} ]} ], gen(f) { const b = f.branch.trim() || ''; const n = f.n || '1'; return `# ONLY if no one else has pulled yet:\ngit reset --hard HEAD~${n}\ngit push origin ${b} --force-with-lease\n\n# Safer alternative (keeps history):\ngit revert HEAD~${n}..HEAD\ngit push`; }, explain() { return 'Force-pushing rewrites public history — it breaks everyone who has already pulled. The safe alternative (revert) creates new commits that undo the bad ones without touching history. Always prefer revert on shared branches.'; } }, { id: 'pull-overwrite', cat: 'remote', icon: '⬇️', label: 'Override local with remote', desc: 'Throw away local changes and match remote exactly', danger: 'high', dangerLabel: '🔴 Permanent — local changes lost', fields: [ { id:'branch', type:'text', label:'Branch', placeholder:'main' } ], gen(f) { const b = f.branch.trim() || 'main'; return `git fetch origin\ngit reset --hard origin/${b}`; }, explain() { return 'Fetches the latest remote state, then hard-resets your local branch to match it exactly. All local uncommitted changes are gone. Run git stash first if you want to keep anything.'; } }, { id: 'set-upstream', cat: 'remote', icon: '🔗', label: 'Set remote tracking branch', desc: 'Fix "no upstream branch" errors when pushing', danger: 'low', dangerLabel: '✅ Safe', fields: [ { id:'branch', type:'text', label:'Branch name', placeholder:'feature/dashboard' } ], gen(f) { const b = f.branch.trim() || ''; return `git push --set-upstream origin ${b}\n# Or shorthand:\ngit push -u origin ${b}`; }, explain() { return 'Sets the remote tracking reference so future git push and git pull commands know where to go without specifying the remote each time.'; } }, /* ── MERGE & REBASE ── */ { id: 'abort-merge', cat: 'merge', icon: '🛑', label: 'Abort a merge in progress', desc: 'Mid-merge conflict and you want out', danger: 'low', dangerLabel: '✅ Safe — restores pre-merge state', fields: [], gen() { return `git merge --abort`; }, explain() { return 'Stops the merge and returns everything to how it was before you ran git merge. Your branch is unchanged. Safe to run anytime during a conflicted merge.'; } }, { id: 'abort-rebase', cat: 'merge', icon: '⛔', label: 'Abort a rebase in progress', desc: 'Rebase went sideways — escape cleanly', danger: 'low', dangerLabel: '✅ Safe — restores pre-rebase state', fields: [], gen() { return `git rebase --abort`; }, explain() { return 'Cancels the rebase and puts you back on your branch exactly where you were before. No commits are changed.'; } }, { id: 'squash-commits', cat: 'merge', icon: '🗜️', label: 'Squash last N commits into one', desc: 'Clean up messy WIP commits before merging', danger: 'med', dangerLabel: '⚠️ Rewrites local history', fields: [ { id:'n', type:'select', label:'Commits to squash', options:[ {v:'2',l:'2 commits'},{v:'3',l:'3 commits'},{v:'4',l:'4 commits'},{v:'5',l:'5 commits'} ]}, { id:'msg', type:'text', label:'Final commit message', placeholder:'feat: complete user auth flow' } ], gen(f) { const n = f.n || '2'; const m = f.msg.trim() || 'squashed commits'; return `git reset --soft HEAD~${n}\ngit commit -m "${m}"`; }, explain() { return '--soft resets back N commits but keeps all the changes staged. Then you make one clean commit. Don\'t do this with already-pushed commits — it rewrites history and will break teammates.'; } }, { id: 'resolve-conflict', cat: 'merge', icon: '⚔️', label: 'Mark conflicts resolved & continue', desc: 'You fixed the conflict markers — now tell Git', danger: 'low', dangerLabel: '✅ Safe', fields: [ { id:'op', type:'select', label:'What are you in the middle of?', options:[ {v:'merge',l:'git merge'},{v:'rebase',l:'git rebase'},{v:'cherry',l:'git cherry-pick'} ]}, { id:'file', type:'text', label:'Resolved file (blank = all)', placeholder:'src/app.js' } ], gen(f) { const file = f.file.trim(); const add = file ? `git add ${file}` : `git add .`; const cont = f.op==='merge' ? `git merge --continue` : f.op==='rebase' ? `git rebase --continue` : `git cherry-pick --continue`; return `# 1. Open the file and fix conflict markers (<<, ====, >>>>)\n# 2. Stage the resolved file:\n${add}\n# 3. Continue the operation:\n${cont}`; }, explain() { return 'After editing the conflict markers out of the file, you must stage it (git add) to tell Git it\'s resolved. Then continue the operation. Never commit manually mid-rebase.'; } }, /* ── STASH ── */ { id: 'stash-save', cat: 'stash', icon: '📦', label: 'Stash current changes', desc: 'Save unfinished work and get a clean working tree', danger: 'low', dangerLabel: '✅ Safe', fields: [ { id:'msg', type:'text', label:'Stash description (optional)', placeholder:'WIP: dark mode toggle' }, { id:'untracked', type:'select', label:'Include untracked files?', options:[ {v:'no',l:'No (default)'},{v:'yes',l:'Yes (-u)'} ]} ], gen(f) { const m = f.msg.trim(); const u = f.untracked === 'yes' ? ' -u' : ''; return m ? `git stash push${u} -m "${m}"` : `git stash${u}`; }, explain() { return 'Stash saves your working directory and staging area to a temporary stack. Your branch returns to the last commit. Retrieve later with git stash pop. Add -u to also stash new untracked files.'; } }, { id: 'stash-pop', cat: 'stash', icon: '📤', label: 'Apply (pop) a stash', desc: 'Bring back stashed changes to your working tree', danger: 'low', dangerLabel: '✅ Safe', fields: [ { id:'which', type:'select', label:'Which stash?', options:[ {v:'latest', l:'Latest stash (stash@{0})'}, {v:'list', l:'List all first, then pick'} ]} ], gen(f) { if(f.which === 'list') return `# List all stashes:\ngit stash list\n\n# Apply a specific one (replace 2 with index):\ngit stash pop stash@{2}`; return `git stash pop`; }, explain() { return 'git stash pop applies the stash AND removes it from the stack. Use git stash apply instead if you want to keep it on the stack (useful for applying to multiple branches).'; } }, /* ── HISTORY ── */ { id: 'find-commit', cat: 'history', icon: '🔍', label: 'Find a commit by message or code', desc: 'Search history for when something changed', danger: 'low', dangerLabel: '✅ Read-only', fields: [ { id:'mode', type:'select', label:'Search by', options:[ {v:'msg',l:'Commit message keyword'}, {v:'code',l:'Code string / function name'}, {v:'file',l:'Changes to a specific file'} ]}, { id:'term', type:'text', label:'Search term', placeholder:'auth middleware' } ], gen(f) { const t = f.term.trim() || ''; if(f.mode === 'msg') return `git log --oneline --grep="${t}"`; if(f.mode === 'code') return `git log -S "${t}" --oneline`; return `git log --oneline -- ${t}`; }, explain(f) { if(f.mode==='code') return '-S (the "pickaxe" flag) searches for commits that added or removed that exact string in any file. Great for finding when a function was introduced or deleted.'; if(f.mode==='file') return 'Shows all commits that touched the specified file path. Chain with -p to see the actual diffs.'; return '--grep searches commit messages. Case-insensitive by default; add -i flag to be explicit.'; } }, { id: 'recover-deleted', cat: 'history', icon: '💀', label: 'Recover a deleted branch / lost commit', desc: 'Git reflog to the rescue', danger: 'low', dangerLabel: '✅ Read-only recovery', fields: [], gen() { return `# 1. Find the lost commit hash:\ngit reflog\n\n# 2. Restore as a new branch:\ngit checkout -b recovered-branch \n\n# Or just checkout the commit:\ngit checkout `; }, explain() { return 'git reflog records every position HEAD has been in for the last ~90 days — even after deletions. Find the commit hash before the deletion and check it out. Everything is recoverable until garbage-collection runs.'; }, steps: true }, { id: 'show-diff', cat: 'history', icon: '🔬', label: 'See what changed', desc: 'Diff between branches, commits, or staged vs working', danger: 'low', dangerLabel: '✅ Read-only', fields: [ { id:'mode', type:'select', label:'Compare', options:[ {v:'staged', l:'Staged vs last commit'}, {v:'unstaged', l:'Working dir vs last commit'}, {v:'branches', l:'Two branches'}, {v:'commits', l:'Two commits'} ]}, { id:'a', type:'text', label:'Branch/Commit A (if comparing two)', placeholder:'main' }, { id:'b', type:'text', label:'Branch/Commit B (if comparing two)', placeholder:'feature/auth' } ], gen(f) { if(f.mode==='staged') return `git diff --staged`; if(f.mode==='unstaged') return `git diff`; const a = f.a.trim() || 'main'; const b = f.b.trim() || 'feature/branch'; return `git diff ${a}..${b}`; }, explain(f) { if(f.mode==='staged') return '--staged (or --cached) shows what you\'ve staged for the next commit vs the last commit.'; if(f.mode==='unstaged') return 'Plain git diff shows working directory vs last commit — changes you haven\'t staged yet.'; return 'Shows everything in B that is not in A. Swap the order to flip perspective. Add a filename at the end to limit the diff to one file.'; } } ]; /* ══════════════════════════════════════ RENDER ══════════════════════════════════════ */ let currentCat = 'all'; let currentIntent = null; function renderGrid(cat) { const grid = document.getElementById('intentGrid'); const filtered = cat === 'all' ? intents : intents.filter(i => i.cat === cat); grid.innerHTML = filtered.map(intent => `
${intent.icon}
${intent.label}
${intent.desc}
`).join(''); } function filterCat(cat, btn) { currentCat = cat; document.querySelectorAll('.cat-pill').forEach(p => p.classList.remove('active')); btn.classList.add('active'); document.getElementById('contextSection').style.display = 'none'; currentIntent = null; renderGrid(cat); } function clearSelected() { document.querySelectorAll('.intent-card').forEach(c => c.classList.remove('selected')); currentIntent = null; } function selectIntent(id) { const intent = intents.find(i => i.id === id); if (!intent) return; currentIntent = intent; document.querySelectorAll('.intent-card').forEach(c => c.classList.remove('selected')); const card = document.getElementById('ic-' + id); if (card) card.classList.add('selected'); buildContextForm(intent); document.getElementById('contextSection').style.display = 'grid'; document.getElementById('contextSection').scrollIntoView({behavior:'smooth', block:'nearest'}); generateCommand(); } function buildContextForm(intent) { const form = document.getElementById('contextForm'); if (!intent.fields || intent.fields.length === 0) { form.innerHTML = `

No extra inputs needed for this command.

`; return; } let html = ``; for (const f of intent.fields) { html += ``; if (f.type === 'text') { html += ``; } else if (f.type === 'select') { html += ``; } } form.innerHTML = html; } function getFieldValues(intent) { const vals = {}; for (const f of (intent.fields || [])) { const el = document.getElementById('f-' + f.id); vals[f.id] = el ? el.value : ''; } return vals; } function generateCommand() { if (!currentIntent) return; const f = getFieldValues(currentIntent); const cmd = currentIntent.gen(f); document.getElementById('cmdOutput').textContent = cmd; // Danger badge const db = document.getElementById('dangerBadge'); const cls = {high:'danger-high', med:'danger-med', low:'danger-low'}[currentIntent.danger]; db.innerHTML = `${currentIntent.dangerLabel}`; // Explain const exp = document.getElementById('explainOut'); if (currentIntent.explain) { exp.innerHTML = `

${currentIntent.explain(f)}

`; } else { exp.innerHTML = ''; } } async function copyCmd() { const btn = document.getElementById('copyBtn'); const text = document.getElementById('cmdOutput').textContent; await navigator.clipboard.writeText(text); btn.textContent = '✓ COPIED'; btn.classList.add('success'); setTimeout(() => { btn.textContent = 'COPY COMMAND'; btn.classList.remove('success'); }, 1600); } /* ── INIT ── */ renderGrid('all');