Git
You and a friend are building the next big AI wrapper around Chromium or VS Code, each working on a different part of the app. Your changes worked at 4 PM, but by 6 PM something has broken. You don't know which edit in the last two hours caused it, and Ctrl+Z isn't getting you back to the version that worked.
Your friend is debugging his part too. While building the latest feature, he unknowingly deleted a few files from another component. He's now comparing his project with prod-01-nov.zip, trying to recover those files without overwriting the work he's done since that backup.
After spending the evening fixing both problems, the app finally works on both machines. The next morning, you “merge” your work by copying files between folders. Each version worked on its own; together, they produce 44 import errors and a gazillion compilation errors.
A version control system (VCS) keeps a history of the project revisions you save. That gives you a way to compare changes, recover recorded files, and combine work without manually replacing entire folders.
Linux kernel developers once passed around patches and archived files too. They adopted BitKeeper, a proprietary version control system, in 2002. When its free use for the project ended in 2005, they needed a replacement.
Linus Torvalds began developing Git that year, with speed, distributed development, and support for many parallel branches among its goals. Each developer could keep a local project history and exchange changes with others.
Recording the working app
Once you've untangled that folder merge, you keep a working copy and set up Git on your Mac. With Homebrew already installed:
brew install git
command -v git
git --versionInside your existing ai-wrapper folder, you create a repository and tell Git whose changes it will record. Use your own name and email here:
cd ai-wrapper
git init -b main
git config --local user.name "Your Name"
git config --local user.email "you@example.com"--local sets the identity for this repository; --global would set your defaults across repositories. It identifies the author of commits, not your GitHub login.
Before recording anything, you notice your folder also contains API keys in .env, installed packages, and yesterday's build. Those should stay out of the history. You create .gitignore:
node_modules/
dist/
build/
.env
.env.*
!.env.example
.DS_StoreYou keep .env.example with placeholder values for your friend; the exception allows that template through. Source code and the lockfile belong in Git, while installed dependencies and generated builds can be recreated.
Your TypeScript app has source code, a dependency lockfile, and configuration worth keeping. With the ignore rules in place, you stage the project and review exactly what the first commit will contain:
git status --short
git add .
git diff --cached
git commit -m "Record the working AI wrapper"git add copies the selected contents into the staging area, also called the index. You review that selection with git diff --cached, then record it as a commit. Later commits will link back to it. This first one gives the working app a reference you can return to.
For tracked files, git diff compares your working copy with the staging area; git diff --cached shows what the next commit would change. Later edits need staging again. git restore --staged PATH removes a tracked file's changes from the selection while keeping them on disk. New, untracked files appear in git status; ordinary git diff does not show their contents.
If .env was already tracked, ignoring it would not untrack it. git rm --cached .env stages its removal while keeping the local file. Earlier commits still contain it, so replace any credentials already shared.
Your friend needs the same starting point. You create an empty ai-wrapper repository on GitHub, without an initial README or commit, and give him access. GitHub hosts the shared repository and PRs; Git records and exchanges the history.
You install GitHub's CLI to handle login and PRs, then connect your local repository. Replace YOUR_ACCOUNT with the repository owner's account:
brew install gh
gh auth login --hostname github.com --git-protocol https
gh auth setup-git
git remote add origin https://github.com/YOUR_ACCOUNT/ai-wrapper.git
git remote -v
git push -u origin mainorigin names that remote URL. The push uploads your commits and updates its main; -u sets the branch's upstream for later pushes. Your commits existed locally before GitHub or your friend received them.
Your friend can now get the project and its history:
git clone https://github.com/YOUR_ACCOUNT/ai-wrapper.git
cd ai-wrapper
npm ci
cp .env.example .envnpm ci installs the dependencies from the lockfile. He configures his own commit identity and supplies his development credentials in .env. When he accidentally deletes src/window.ts again, git diff -- src/window.ts shows the deletion. Restoring the last committed copy with git restore --source=HEAD -- src/window.ts leaves his other files alone.
Combining shortcuts and layout changes
You start adding a launcher shortcut while your friend improves the desktop window layout. This time, you create separate branches. A branch is a movable reference to a commit, so creating one does not copy the whole project. On your machine:
git switch -c feature/shortcuts
# Edit the existing shortcut handler in src/shortcuts.ts.
git diff -- src/shortcuts.ts
git add -p src/shortcuts.ts
git diff --cached
git commit -m "Add the launcher shortcut"
git push -u origin feature/shortcutsgit add -p lets you choose individual hunks, useful when the same file contains an unrelated experiment. For a new file, use git add PATH to stage its contents. git switch -c creates and enters the branch; you will also see the older git checkout -b form. Unsaved edits are not automatically isolated by a branch name, so commit or stash them before switching tasks.
Copying your folder over your friend's is no longer the handoff. You open a pull request (PR) asking to merge feature/shortcuts into main:
gh pr create --base main --head feature/shortcuts \
--title "Add the launcher shortcut" \
--body "Register the launcher shortcut and remove it when the window closes."Your friend can review the PR's diff and discuss changes before merging. PRs belong to GitHub, while the commits belong to Git. You also configure a build-and-test workflow for PRs, so failures are visible before merging.
Your PR is #1 in this example. Your friend's window-layout change becomes PR #2 and is ready first. You review its diff, wait for the configured checks, approve it, and merge it:
gh pr diff 2
gh pr checks 2 --watch
gh pr review 2 --approve
gh pr merge 2 --mergeBut his change also updates shortcut registration. Your PR was written against the old interface, and now needs updating. You agree that you will rebase your branch, fetch the latest history, and replay your commits on the updated main:
git switch feature/shortcuts
git fetch origin
reviewed_head=$(git rev-parse origin/feature/shortcuts)
git log --oneline --graph --decorate --all -12
git diff origin/main...HEAD
git rebase origin/mainFetching updates remote-tracking references such as origin/main without merging them into your current branch. The three-dot diff shows your branch's changes since its common ancestor with origin/main. reviewed_head remembers the remote PR revision you intend to replace after rebasing.
Here, rebase stops because both branches changed src/shortcuts.ts. You open the conflict, adapt your shortcut to the new window interface, and remove the conflict markers. Then you stage the resolution, continue, and check that the app still works:
git add src/shortcuts.ts
git rebase --continue
npm test
npm run build
git push --force-with-lease=refs/heads/feature/shortcuts:$reviewed_head origin feature/shortcutsRebase may stop again for another commit; resolve and continue each time. git rebase --abort returns to the pre-rebase state if you need to start over. Even a conflict-free rebase needs testing: combining text successfully does not prove the two features work together.
Rebase changes commit IDs, so this push replaces the PR branch's earlier history. The explicit lease refuses if the remote has moved from reviewed_head. You coordinate this rewrite with your friend and leave shared main intact.
Your friend reviews the updated PR and approves it. You merge it, then bring your own main up to date:
gh pr checks 1 --watch
gh pr merge 1 --merge
git switch main
git pull --ff-only origin main
git branch -d feature/shortcuts--ff-only updates your branch only when it can move forward without a merge commit; it refuses a diverged history. The final command deletes your local feature-branch name. Deleting the remote branch is separate: git push origin --delete feature/shortcuts. The merged work remains in main.
Undoing edits and releasing fixes
A few days later, your chat-history experiment touches the existing src/app.ts and src/window.ts. You rush both changes into one local commit, then decide to split it before pushing or requesting review:
git switch -c feature/chat-history
# Edit the existing src/app.ts and src/window.ts.
git add src/app.ts src/window.ts
git commit -m "WIP chat history and window changes"
git branch backup/chat-history
git reset --soft HEAD~1
git diff --cachedHEAD~1 means the current commit's first parent. --soft moves this branch back one commit while keeping both the staged contents and working files. The backup branch still points to the original combined commit. Nothing has been removed from GitHub because this was local work.
You also want to choose the files again, so you reset the staging area, leaving the files on disk:
git reset --mixed HEAD
git add src/app.ts
git diff --cached
git commit -m "Persist chat history"--mixed is the default reset mode. Here, HEAD is already at the earlier commit, so this command only resets the staging area; it does not go back another commit. Your window edits remain unstaged while the new commit contains the application change.
Testing then reveals that this approach to chat history needs rethinking. You decide to discard the experiment from this branch and return its tracked files to origin/main:
git status --short
git diff
git reset --hard origin/main--hard resets the branch, staging area, and tracked files, discarding those edits. It does not clean up all untracked files. The backup protects your earlier commit, not later unsaved work. Keep this rewrite local; your friend should not discover that shared commits disappeared.
If a branch name gets lost during this cleanup, git reflog -15 shows recent local reference positions. After finding the wanted commit ID, git branch rescue/chat-history RECOVERABLE_SHA gives it a name again. Reflogs expire and are local; they do not replace a backup or recover arbitrary files you never committed.
You return to main, where both reviewed features work, and prepare the first release. An annotated tag gives that exact revision a release name:
git switch main
git pull --ff-only origin main
git tag -a v0.1.0 -m "First AI wrapper release"
git push origin v0.1.0Tags need publishing too, and a published release tag should stay on the revision you shipped. Your build produces the installer; GitHub can attach build/AI-Wrapper.dmg to a release for that tag:
gh release create v0.1.0 build/AI-Wrapper.dmg \
--title "AI Wrapper 0.1.0" --notes "Launcher shortcut and updated desktop layout."The installer stays out of source history, as intended by .gitignore, while users can download the release asset.
While you're adding prompt search, your friend finds a timeout bug in the released app. His pushed feature/provider-settings branch contains the fix alongside unfinished settings work. Only the fix should ship.
You stash your unfinished search work and create a release branch from the tag. The capitalized commit names below stand for actual IDs you inspect in the log:
git switch -c feature/prompt-search
# Work on prompt search until the hotfix interrupts you.
git stash push -u -m "WIP prompt search"
git fetch origin
git switch -c release/0.1 v0.1.0
git log --oneline origin/feature/provider-settings
git show FIX_COMMIT_SHA
git cherry-pick -x FIX_COMMIT_SHA
npm test
npm run build-u stashes untracked files too, but leaves ignored files such as .env. Cherry-pick applies that commit's change here, normally with a new ID; -x records its origin. Check that it works independently of the unfinished settings. Resolve conflicts, stage, and git cherry-pick --continue, or cancel with git cherry-pick --abort.
After testing the fix on the release branch, you publish the branch and a new tag:
git push -u origin release/0.1
git tag -a v0.1.1 -m "Fix provider request timeouts"
git push origin v0.1.1
git switch feature/prompt-search
git stash popSend the fix to main through review too, so the next release includes it. Stash pop restores your unfinished search work; if it conflicts, resolve it before continuing. The stash entry is retained on failure.
If a bad change has already reached shared main, you use a new fix branch and git revert BAD_COMMIT_SHA to add a reversing commit, then send that through review. Resetting your own branch and publishing a reversal to shared history solve different problems. Neither rolls back an external side effect such as a database migration.
Sharing tooling and hunting regressions
The wrapper now shares a prompt-evaluation harness with another app. That tooling has its own repository, and you want to pin a version of it inside the wrapper.
After your search work has gone through review and your working tree is clean, you create feature/prompt-evals from the updated main and add the other repository as a submodule. Substitute the actual repository owner:
git switch main
git pull --ff-only origin main
git switch -c feature/prompt-evals
git submodule add https://github.com/YOUR_ACCOUNT/prompt-evals.git tools/prompt-evals
git add .gitmodules tools/prompt-evals
git commit -m "Pin the prompt evaluation harness"
git push -u origin feature/prompt-evalsYou open and merge a PR for this change using the same review flow. The parent repository records .gitmodules and a pointer to one submodule commit. The harness keeps its own history; committing the wrapper does not commit edits inside the harness.
Your friend's existing checkout needs the submodule contents after pulling the merged change:
git switch main
git pull --ff-only origin main
git submodule update --init --recursiveFor a fresh checkout, git clone --recurse-submodules REPOSITORY_URL does that during cloning. When you want a newer harness revision, fetch it, check out the chosen published commit, and record the changed pointer on a new wrapper branch:
git -C tools/prompt-evals fetch origin
git -C tools/prompt-evals switch --detach EVAL_COMMIT_SHA
git add tools/prompt-evals
git commit -m "Update the pinned evaluation harness"The detached checkout pins a revision. To develop a harness change, create a branch inside that repository and configure your commit identity there too. Commit and push the harness change first, then update the wrapper's pointer so your friend can fetch the referenced commit.
Eventually another shortcut regression appears, and nobody remembers which change introduced it. This time you have a known working release, the current broken revision, and a reproducible check. You keep an executable check-shortcut.sh outside the repository so switching to old commits cannot remove it. It exits 0 when the shortcut works, 1 when it fails, and 125 when that revision cannot be tested:
git switch main
git pull --ff-only origin main
git bisect start HEAD v0.1.0
git bisect run ../check-shortcut.sh
git bisect resetBegin with a clean working tree. Bisect checks candidate revisions and uses the script's result to narrow the search; an unrelated build failure must not be reported as the shortcut bug. git bisect reset returns you to the original checkout. You can now inspect the first bad commit and open a focused fix PR, instead of spending another evening guessing what changed after 4 PM.