--- title: "Git & GitHub Workflows: Branches, Pull Requests, and Code Review" author: "Pradeep Kumar" published: "2026-11-01T07:47:05.749Z" updated: "2026-11-01T07:47:05.749Z" url: "https://zyvop.com/git-github-workflows-branches-pull-requests-and-code-review-ntiub" canonical: "https://zyvop.com/git-github-workflows-branches-pull-requests-and-code-review-ntiub" tags: ["version control","GitHub","AI Engineer","git","code-review"] --- # Git & GitHub Workflows: Branches, Pull Requests, and Code Review Learn Git as a safety net: commits, branches, merges, and undoing mistakes, then use GitHub pull requests and code review the way professional teams do. ![Cover image]() Sooner or later, every programmer has the same awful moment. You change something, it breaks, and you can't remember what the code looked like before. Or you work on a project with someone else, and your changes collide with theirs. Or you accidentally delete a file you needed. **Git** exists to make all of that boring. It keeps a complete history of your project, lets you try risky ideas safely, lets you undo almost anything, and lets many people work on the same code without trampling each other. This post is core, and it's also personal for you: back in Post 3, I asked you to start a repository called `ai-engineer-journey`. By the end of this post, that repository will be a real working project with a proper history, and the habits you build here are the same ones used by every professional team. ## Git and GitHub are not the same thing People mix these up all the time. - **Git** is a tool on your computer. It tracks changes to files, in a folder called a **repository** (or repo). It works completely offline. - **GitHub** is a website that hosts Git repositories and adds collaboration features on top: pull requests, code review, issue tracking, automated checks, and a public profile. GitLab and Bitbucket are similar services. So Git is the engine, and GitHub is one place to park the car and invite people to look at it. Almost everything in the first half of this post is plain Git, and you can practice it without an account. For AI work, Git is more valuable than usual. Prompts, configuration files, evaluation sets, and code all change constantly, and a small tweak to a prompt can quietly make results better or worse. With Git, every change is recorded, and you can always say what changed and when. And a tidy public GitHub profile is one of the best things to show an employer (Post 72). ## The mental model Three ideas make everything else click. **1\. A commit is a snapshot.** When you commit, Git saves the state of your files at that moment, together with a message, an author, a time, and a pointer to the commit that came before it. Each commit is a permanent, named checkpoint. **2\. History is a chain, which makes it a graph.** Every commit points back to its parent, so a project's history is a chain of commits. When work splits into branches and rejoins, it becomes the kind of graph you met in Post 16. Many Git commands are really just walking that graph. **3\. A branch is just a movable label.** A branch is a name that points at one commit. When you make a new commit, the label moves forward to it. That's why branches are cheap, and why you should use lots of them. There's also a special pointer called **HEAD**, which means "where I am right now." And there are three places your files can be: ```sql working directory --git add--> staging area --git commit--> repository (files you edit) (what goes in the (the saved next commit) history) ``` You edit files in the **working directory**. You choose which changes to include using `git add`, which puts them in the **staging area**. Then `git commit` saves exactly what's staged into the **repository**. The staging area looks like an extra step at first. It's actually what lets you make _clean_ commits: if you changed ten things, you can commit them as three tidy groups. ## One-time setup Install Git (from git-scm.com, or your system's package manager), then tell it who you are. This name and email get stamped on every commit you make: ```bash git config --global user.name "Your Name" git config --global user.email "you@example.com" git config --global init.defaultBranch main ``` (Use the same email as your GitHub account so your commits are linked to your profile.) The last line makes new repositories start with a branch called `main`. ## The everyday loop I ran the Git commands in this post in real repositories on my own machine, so those outputs are genuine. (The parts about the GitHub website I describe from knowledge, and I'll flag them when we get there. Your commit hashes, the short codes like `d9fcc09`, will be different from mine. That's normal.) Let's make a little project for prompt experiments: ```bash mkdir prompt-notes cd prompt-notes git init ``` `git init` turns the folder into a repository. Now let's create a few files, and ask Git what it sees: ```bash git status ``` ```rust On branch main No commits yet Untracked files: (use "git add ..." to include in what will be committed) .gitignore README.md prompts/ nothing added to commit but untracked files present (use "git add" to track) ``` `git status` is the command you'll type more than any other. It tells you where you are and what's changed, and it usually suggests what to do next. Make a habit of running it before every `add` and `commit`. The files are "untracked," meaning Git has noticed them but isn't recording them. Stage them, then commit: ```bash git add README.md .gitignore prompts git commit -m "Add README, gitignore, and first summarize prompt" ``` ```sql [main (root-commit) d9fcc09] Add README, gitignore, and first summarize prompt 3 files changed, 8 insertions(+) ``` That's your first commit. Now let's change the prompt and look at what changed, using `git diff`: ```bash git diff ``` ```diff --- a/prompts/summarize.txt +++ b/prompts/summarize.txt @@ -1 +1 @@ -Summarize the text below in two sentences. +Summarize the text below in two sentences. Use plain language. ``` A line starting with `-` was removed, and one starting with `+` was added. Then stage it and commit again. `git diff` shows changes that _aren't_ staged yet, and `git diff --staged` shows what _is_ staged and about to be committed. Finally, look at the history: ```bash git log --oneline ``` ```sql 9d5cb3e Ask for plain language in summarize prompt d9fcc09 Add README, gitignore, and first summarize prompt ``` The newest commit is on top. ### A word on `git add .` You'll see `git add .` in lots of tutorials. It stages _everything_, which is fast, and also how people accidentally commit `.env` files, big data files, and junk. Either add specific paths, as above, or run `git status` first and read what you're about to add. ### Writing good commit messages A commit message is a note to your future self and your teammates. Compare: | Bad | Good | | --- | --- | | `fix` | `Fix off-by-one in chunk_words overlap` | | `update stuff` | `Add retry with backoff to the model client` | | `final version 2` | `Ask for plain language in summarize prompt` | A few habits: - **Start with a short summary line**, around 50 characters, written as a command: "Add", "Fix", "Remove". - **Explain _why_ in the body when it isn't obvious.** The `-m` flag can be used twice, once for the summary and once for the body: ```bash git commit -m "Ask for plain language in summarize prompt" \ -m "Early tests showed jargon-heavy summaries, so the prompt now asks for plain language." ``` - **Commit small, focused changes.** One logical change per commit. "Add retry logic" is a good commit. "Lots of changes" is a bad one, because you can't undo one piece without undoing everything. - **Commit often.** Commits are cheap, and they're your save points. ## What to leave out: .gitignore A file called `.gitignore` lists patterns of files Git should pretend don't exist. Ours contains: ```bash .venv/ __pycache__/ .env *.db ``` Git can tell you which rule matched: ```bash git check-ignore -v .env .venv/junk.txt ``` ```bash .gitignore:3:.env .env .gitignore:1:.venv/ .venv/junk.txt ``` Neither of those files showed up in `git status`, even though they exist. For Python and AI projects, a good starting list of things to ignore includes: - Virtual environments (`.venv/`), and `__pycache__/` (Post 12). - **Secrets**: `.env` files and anything with API keys. Commit a `.env.example` with fake values instead. - Local databases (`*.db`) and logs. - Large datasets and **model weights**. Git isn't built for big binary files, and GitHub rejects individual files over 100 MB. Large assets belong in a different place (such as a model hub or object storage), or a tool like Git LFS. - Notebook checkpoints, build output, and editor settings. GitHub offers ready-made `.gitignore` templates for Python when you create a repository, which is a fine place to start. ## Undoing things: the safety net This is the part that makes people love Git. Almost nothing you do is permanent, as long as you know which tool to reach for: | The situation | The command | | --- | --- | | I edited a file and want to throw the changes away | `git restore ` | | I staged a file by mistake | `git restore --staged ` | | I just committed and want to fix the message or add a forgotten file | `git commit --amend` | | I want to undo an old commit, safely | `git revert ` | | I need to set work aside for a minute | `git stash` | | I want to move a branch back in time | `git reset` | | I think I destroyed something | `git reflog` | Let me show the ones that matter most. **Discarding a bad edit:** ```bash printf "THIS EDIT IS A MISTAKE\n" > prompts/summarize.txt git restore prompts/summarize.txt ``` The file goes back to how it was at the last commit. (This one is final: those uncommitted changes are gone.) **Fixing the last commit.** I committed with a typo: ```bash git commit -m "Add clasify prompt" git commit --amend -m "Add classify prompt" ``` ```sql 4e915f8 Add clasify prompt <- before 8e2f1b4 Add classify prompt <- after ``` Notice the hash changed. Amending _replaces_ the commit with a new one. That's perfectly fine for a commit that only exists on your computer, but never amend a commit you've already shared (more on that below). **Undoing an old commit safely with** `revert`**.** I added a translate prompt, then decided I didn't want it: ```bash git revert --no-edit HEAD ``` ```csharp [main 065b64b] Revert "Add translate prompt (not needed after all)" 1 file changed, 1 deletion(-) ``` `revert` doesn't erase history. It adds a _new_ commit that does the opposite of the old one. That's the right tool for anything that's already been shared, because nobody's history gets rewritten. **Stashing.** You're in the middle of something and need a clean folder for a moment: ```bash git stash # shelves your uncommitted changes git stash pop # brings them back ``` ### The scary one: `reset --hard`, and the rescue `git reset --hard` moves your branch back to an earlier commit and **throws away** the changes in your working folder. It's the one command that can really hurt. But watch what happens when I make a commit and then wipe it: ```bash git reset --hard HEAD~1 ``` ```python HEAD is now at 065b64b Revert "Add translate prompt (not needed after all)" ``` The "Add dates prompt" commit has vanished from the log, and its file is gone from the folder. But Git keeps a private diary of everywhere HEAD has been, called the **reflog**: ```bash git reflog ``` ```yaml 065b64b HEAD@{0}: reset: moving to HEAD~1 ed39bfa HEAD@{1}: commit: Add dates prompt 065b64b HEAD@{2}: revert: Revert "Add translate prompt (not needed after all)" ``` There's the lost commit, `ed39bfa`. Jump back to it: ```bash git reset --hard ed39bfa ``` ```python HEAD is now at ed39bfa Add dates prompt ``` Everything is back, including the file. The lesson: **if you ever committed something, it's almost certainly recoverable**, at least for a while (the reflog keeps entries for weeks by default). What's _not_ recoverable is work you never committed. That's one more reason to commit often. ## Branches and merging A **branch** lets you work on something without disturbing the main line of the project. This is how professionals work: `main` always holds working code, and every new idea gets its own branch. ```bash git switch -c feature/extract-emails ``` That creates a new branch and moves you onto it. (You'll see `git checkout -b` in older tutorials. `git switch` is the newer, clearer command for the same job.) Make a commit there, and `main` doesn't change. Then come back to `main` and **merge**: ```bash git switch main git merge feature/extract-emails ``` ```bash Updating ed39bfa..a5ac0a1 Fast-forward prompts/emails.txt | 1 + ``` A **fast-forward** is the simplest kind: `main` hadn't moved, so Git just slides its label forward to catch up with the branch. No new commit needed. When both branches have moved, Git has to combine them with a **merge commit**. I added a tone prompt on one branch, and edited the README on `main`, then merged: ```sql Merge made by the 'ort' strategy. ``` And `git log --oneline --graph` draws the shape of what happened: ```sql * 4d633d0 Merge branch 'feature/tone-prompt' |\ | * 0dc7feb Add tone prompt * | c1bdedd Describe prompts folder in README |/ * a5ac0a1 Add email extraction prompt ``` Two lines of work split apart and joined back together. That's the graph from Post 16, drawn in your terminal. Some tips on naming: use short, descriptive names like `feature/extract-emails`, `fix/retry-bug`, or `experiment/short-summary`. ### Merge conflicts Here's the moment that scares beginners, so let's meet it on purpose. What if two branches change the _same line_? That happens in AI projects all the time, because people tweak the same prompt. I changed `summarize.txt` to "one sentence" on one branch and to "three bullet points" on `main`, and then merged: ```sql Auto-merging prompts/summarize.txt CONFLICT (content): Merge conflict in prompts/summarize.txt Automatic merge failed; fix conflicts and then commit the result. ``` It isn't an error, and nothing is broken. Git just can't decide which version you want, so it asks you. If you open the file, you'll see both versions marked up: ```markdown <<<<<<< HEAD Summarize the text below in three bullet points. ======= Summarize the text below in one sentence. >>>>>>> experiment/short-summary ``` The part between `<<<<<<<` and `=======` is your side (the branch you're on). The part between `=======` and `>>>>>>>` is the incoming side. To resolve it: 1. **Edit the file** to be what you actually want, and delete all three marker lines. You can keep one side, the other, or write something new that combines them. I wrote: "Summarize the text below in three short bullet points." 2. **Stage it**: `git add prompts/summarize.txt`. 3. **Finish the merge**: `git commit`. That's all a conflict is. VS Code highlights conflicts and offers buttons for "accept current" or "accept incoming," which makes it easier still. And if you panic halfway through, `git merge --abort` puts everything back exactly as it was before you started. (I tested that too.) ### Rebase: a tidier alternative There's a second way to combine branches, called **rebase**. Instead of joining two lines with a merge commit, it _replays_ your branch's commits on top of the latest `main`, as if you'd started your work from there. Before: ```markdown * f92240e F2 * 64d00cc F1 | * 0e3344c M1 <- main moved on |/ * bbfda66 A ``` After `git rebase main` on the feature branch: ```markdown * 65730f4 F2 * ce26bed F1 * 0e3344c M1 * bbfda66 A ``` One straight line, with no merge commit. Many teams like the clean, linear history. But there's an important rule, which is the most important rule in this post: > **Never rebase (or amend, or otherwise rewrite) commits that other people already have.** Rebasing creates _new_ commits with new hashes. If someone else is building on the old ones, their history and yours no longer match, and untangling it is miserable. Rewriting your own private work is fine. Rewriting shared work is not. When in doubt, use `merge`. ## Working with a remote So far everything has been on your own computer. A **remote** is another copy of the repository, usually on GitHub, and by convention the main one is called `origin`. | Command | What it does | | --- | --- | | `git clone ` | Copy a remote repository to your computer | | `git push` | Send your new commits to the remote | | `git fetch` | Download new commits from the remote, but don't touch your files | | `git pull` | Fetch, then bring those commits into your branch | You can't try GitHub without an account, but you can try the whole workflow offline, because a remote can just be another folder. I created a "bare" repository to stand in for GitHub, and cloned it twice, as two imaginary people, Alice and Bob. After Alice pushed the first commit, Bob cloned it and saw the same history. Then they both made a commit. Alice pushed first. When Bob tried to push: ```sql ! [rejected] main -> main (fetch first) error: failed to push some refs to '...' hint: Updates were rejected because the remote contains work that you do not hint: have locally. This is usually caused by another repository pushing to hint: the same ref. If you want to integrate the remote changes, use hint: 'git pull' before pushing again. ``` This is Git protecting Alice's work. Bob's history and the remote's history have **diverged**, and he's not allowed to overwrite hers. He needs to bring her commit in first. After `git fetch`, you can see the fork: ```sql * 6ad0eae Add translate prompt <- Bob's commit | * ca86132 Summaries are two sentences <- Alice's commit |/ * 0eacee0 Initial commit ``` Now Bob has a choice. Either he merges, or he uses `git pull --rebase`, which replays his commit on top of Alice's: ```sql Successfully rebased and updated refs/heads/main. * f73b44b Add translate prompt * ca86132 Summaries are two sentences * 0eacee0 Initial commit ``` Then his push goes through. If you do a plain `git pull` on diverged branches, newer versions of Git stop and ask you to decide how, with a long hint: ```yaml hint: You have divergent branches and need to specify how to reconcile them. hint: git config pull.rebase false # merge hint: git config pull.rebase true # rebase hint: git config pull.ff only # fast-forward only fatal: Need to specify how to reconcile divergent branches. ``` That's not an error in your repo. It's just asking you to pick a default. Either choice is fine. Pick one and set it once with `git config --global pull.rebase true` (or `false`). ### Connecting to GitHub With a real GitHub account, you create an empty repository on the website, then connect your local one to it: ```bash git remote add origin https://github.com/YOUR-NAME/ai-engineer-journey.git git push -u origin main ``` The `-u` flag remembers the connection, so later you can just type `git push` and `git pull`. One note on signing in: GitHub no longer accepts your account password for Git over HTTPS. You use either an **SSH key**, a **personal access token**, or the GitHub CLI (`gh auth login`), which sets it up for you. GitHub's setup pages walk through each, and they change from time to time, so follow their current instructions. (I can't reach GitHub from my test environment, so the GitHub-specific parts of this post are described from knowledge, while the Git commands were all run.) ## Pull requests On a team, you don't usually push straight to `main`. Instead you work on a branch, push that branch, and open a **pull request** (PR): a page on GitHub that says "I'd like to merge these changes into `main`. Please look at them." It shows exactly what changed, line by line, and gives people a place to discuss it. The usual flow is: 1. **Branch**: `git switch -c fix/retry-bug` 2. **Commit** your work in small, clear commits. 3. **Push** the branch: `git push -u origin fix/retry-bug` 4. **Open a pull request** on GitHub. 5. **Review**: teammates comment and ask for changes, and you push more commits to the same branch to address them. 6. **Checks pass**: tests and linters run automatically on every PR (we'll set that up in Post 28). 7. **Merge**, then delete the branch and update your local `main` with `git switch main && git pull`. GitHub offers three ways to merge: - **Merge commit** keeps every commit on the branch, plus a merge commit. - **Squash and merge** squashes the whole branch into a single commit on `main`. It's popular because it keeps `main`'s history tidy, one commit per PR. - **Rebase and merge** replays the branch's commits onto `main`, with no merge commit. ### A good pull request - **Small.** A PR that changes 30 lines gets read carefully. A PR that changes 3,000 gets skimmed and approved. Aim for one idea per PR. - **Explained.** Say what changed, why, and how you tested it. Many repos put a template in `.github/pull_request_template.md`, and GitHub fills it in automatically. Here's a good one for an AI project: ```markdown ## What changed ## Why ## How I tested it ## Checklist - [ ] Tests pass - [ ] No secrets or API keys in the diff - [ ] Prompt changes are noted, with before/after examples ``` - **A draft first, if it's not ready.** GitHub lets you open a "draft" PR to get early feedback. - **Linked to an issue**, if there is one. Many teams also turn on **branch protection**, so that nobody can push straight to `main`, and merging requires a passing build and at least one approval. ## Code review Review is one of the best ways to learn, and one of the most underrated skills. You'll be doing it from both sides. **As the author:** - Review your own PR first. Read the "Files changed" tab as if someone else wrote it. You'll catch debugging leftovers and typos before anyone else sees them. - Don't take comments personally. They're about the code, and the goal is a better project. - Answer every comment, even if it's just "done." - Say thanks. It costs nothing. **As the reviewer:** - Read the description first, so you know what the change is trying to do. - Comment on the code, never the person. "This could fail if the list is empty" beats "you forgot to handle empty lists." - Ask questions instead of giving orders when you're not sure: "What happens if the model returns an empty string here?" - Separate **must-fix** from **nitpick.** Prefix small style comments with "nit:", so the author knows what's optional. - Praise good work. A review that's only criticism teaches people to dread them. For AI projects specifically, here's a checklist worth keeping in your head: - **Secrets.** Any API key, token, or password in the diff? (Even in a "temporary" test.) - **Prompt changes.** Was a prompt edited? Is there evidence it's better, such as examples or eval results (Post 60)? A one-word change to a prompt can shift a lot. - **Tests.** Does new logic have tests, with fake models rather than real ones (Post 13)? - **Errors and retries.** What happens on a timeout, a 429, or a malformed reply (Posts 11 and 20)? - **Cost.** Does this make more model calls, or send much longer prompts (Post 18)? - **Privacy.** Does it log prompts or user data that shouldn't be stored? - **Dependencies.** Was a new library added, and is it a real, maintained one (Post 12)? - **Pinned versions.** Is the model name or version fixed, so behavior doesn't change silently? AI assistants can be a useful extra pair of eyes on a PR, good at catching things you missed. Treat their comments like a quick first pass from a colleague. You're still the reviewer, and responsible for the result. ## Common workflows The most widely used pattern is simple, and sometimes called **GitHub Flow**: 1. `main` is always in a working state. 2. Every change happens on a short-lived branch. 3. Changes get merged through a pull request. 4. Delete the branch after merging. Keep branches short-lived, a few days at most. The longer a branch lives, the more it drifts from `main`, and the nastier the conflicts when you merge. For open-source projects where you don't have permission to push, you use a **fork**: your own copy of the repository on GitHub. You work there, then open a PR from your fork to the original, which is called the **upstream**. That's exactly the process in Post 76, when we make your first open-source contribution. ## Mistakes you'll make (and how to survive them) **Committing a secret.** This one deserves a clear warning, because the fix surprises people. Say you commit a file with an API key, notice, add it to `.gitignore`, and remove it from tracking: ```bash git rm --cached config.env ``` The file no longer shows up in the latest commit. But **it's still in the history.** Anyone who looks at the earlier commit can read it: ```python 203c7b4 Stop tracking config.env 9f8f4d3 Add config <- the key is still readable here ``` Deleting a secret from the latest version doesn't remove it from the past. If a key was ever committed, and especially if you pushed it, **treat it as stolen: revoke it and create a new one**, as we said in Post 12. There are tools that rewrite history to scrub a file out, but rotating the key is the part that actually protects you. GitHub can also scan for known kinds of secrets and warn you or block a push, which is worth turning on. **Committing something huge or junk.** A `.venv/` folder, a dataset, a model file. Use `.gitignore` before you add, and remember GitHub rejects files over 100 MB. **Force-pushing over other people's work.** `git push --force` overwrites the remote's history with yours. After you rebase _your own_ branch, you may need it, and the safer version is `git push --force-with-lease`, which refuses if someone else has pushed in the meantime. Never force-push to `main`. **Working on** `main` **directly.** It's fine for a personal scratch repo, but get in the habit of branching. It costs nothing, and it makes every experiment disposable. ## Git as a detective History isn't just for undoing. It's a database of _why_ the code looks the way it does. `git blame` shows who last changed each line of a file, and in which commit: ```bash git blame prompts/summarize.txt ``` ```yaml cf014ae8 (Asha Learner 2026-11-01 06:56:37 +0000 1) Summarize in a limerick. ``` It isn't about blaming anyone. It's about finding the commit message that explains _why_ the line changed. `git log -- ` lists every commit that touched a file, and `git log -p` shows the changes themselves. `git bisect` is magic, and it's the binary search from Post 17 applied to your history. Suppose something worked a week ago and is broken now, across dozens of commits. Instead of checking each one, you tell Git a "good" commit and a "bad" one, and it checks out the commit in the middle. You say whether that one's good or bad, and it halves the range again, over and over, until it finds the first bad commit. Better still, if you have a script that exits with 0 when things are fine and 1 when they're not (the exit codes from Post 19), Git can do the whole search for you. In a test repo, I made nine commits, and one of them set `temperature=7.0` in a settings file, which my check script rejects. Then: ```bash git bisect start HEAD git bisect run python3 check.py ``` ```sql running 'python3' 'check.py' running 'python3' 'check.py' running 'python3' 'check.py' 6f45376168c02c88eeab0f12ce447976ce49ca97 is the first bad commit Tweak settings ``` Three runs to search nine commits, and it named the guilty commit. When you're done, `git bisect reset` returns you to where you were. This is a great way to find which change made your model's output worse, if you have a script that can tell. **Tags** mark special commits, such as releases: ```bash git tag -a v0.2.0 -m "Second release" git push --tags ``` A tag is a permanent name for one exact version, so you can always get back to what you shipped. ## Git for AI projects A few habits that are specific to our line of work: - **Treat prompts as code.** Keep them in files, commit changes with messages that say _why_, and review them in PRs. - **Commit your lock file** (`uv.lock` or pinned requirements, Post 12), so everyone gets the same library versions. - **Keep secrets out**, and commit `.env.example`. - **Small eval sets can live in the repo**, but large datasets and model weights shouldn't. Store them elsewhere, and keep a note on where to get them. - **Notebooks make noisy diffs**, since their saved outputs change constantly. A tool called `nbstripout` can remove outputs before committing. - **Tag the version that went to production**, so you can reproduce what users saw. - **Link results to commits.** When you record an eval result, record the commit hash too. Then "this score came from this exact code and prompt" is always answerable. ## Mini project: put your journey on GitHub Time to do it for real. We'll use the repository from Post 3, and practice the full team workflow on your own. **Step 1: create the repo.** On GitHub, create a new repository named `ai-engineer-journey`. If you already made one in Post 3, use that. If you're making a fresh one locally, run `git init -b main` in a folder. **Step 2: set up the basics.** Make sure it has a `README.md` (your goals and start date), a Python `.gitignore`, and folders called `notes/` and `projects/`. Commit them with a clear message, connect the remote, and push: ```bash git add README.md .gitignore notes projects git commit -m "Set up journey repo with README and folders" git remote add origin https://github.com/YOUR-NAME/ai-engineer-journey.git git push -u origin main ``` **Step 3: add a project on a branch.** Don't commit straight to `main`. Do it the professional way: ```bash git switch -c add-textkit ``` Copy in the `textkit` package from Post 12 (without its `.venv` folder), and commit it in **several small, logical commits**: the package code, then the tests (Post 13), then the README for it. Then push the branch: ```bash git push -u origin add-textkit ``` **Step 4: open a pull request.** Go to your repo on GitHub, and open a PR from `add-textkit` into `main`. Write a proper description: what, why, how tested. Then review your own PR on the "Files changed" tab, and leave yourself at least one comment. Finally, **squash and merge** it, delete the branch, and update your local copy: ```bash git switch main git pull ``` **Step 5: make a conflict on purpose.** Clone your repo into a second folder, pretending to be a teammate: ```bash cd .. git clone https://github.com/YOUR-NAME/ai-engineer-journey.git ai-engineer-journey-2 ``` In each copy, edit the _same line_ of `README.md` differently, commit, and push one of them. In the other, try to push (it'll be rejected), then `git pull`, resolve the conflict in the file, `git add`, commit, and push. After you've done it once, conflicts stop being scary. **Step 6: add a PR template.** Create `.github/pull_request_template.md` with the template from earlier, commit it on a branch, and open a PR. Open one more PR afterward, and watch GitHub fill in the template. **Step 7: tag a release.** Run `git tag -a v0.1.0 -m "First release"` and `git push --tags`. When you're done, `git log --oneline --graph` should show a history with real structure, and your GitHub profile should have a repository that shows exactly how you work: small commits, clear messages, PRs, and a tidy README. That's something a hiring manager can read. ### Make it yours 1. Write a `CONTRIBUTING.md` that explains how to branch, name commits, and open a PR. 2. Add a `.env.example` file and make sure `.env` is ignored. Check with `git check-ignore -v .env`. 3. Try `git bisect` on your own project by deliberately breaking something in the middle of the history. 4. Install `pre-commit` and add a hook that blocks commits containing something that looks like an API key. 5. Ask a friend to review one of your PRs, or review one of theirs. ## Practice About 20 minutes of real effort on each before asking for help. 1. Make a repo, commit three changes to one file, and use `git log -p` to read the story they tell. 2. Break a file on purpose in three different ways, and fix each using `restore`, `revert`, and `reset --hard` with the reflog. 3. Create two branches that edit the same line of a prompt file. Merge them, resolve the conflict, and read the graph with `git log --oneline --graph --all`. 4. Practice the full Alice-and-Bob exercise with a local bare repo (`git init --bare`) and two clones. Get a rejected push, then fix it with `pull --rebase`. 5. Rewrite five bad commit messages into good ones. 6. Commit a fake secret, remove it, and then find it in the history. Write down exactly what you'd do next in a real case. 7. Write the review comments you'd leave on a PR that logs full user prompts, and has no tests. Make them kind and specific. 8. Use `git bisect run` with a tiny script to find the commit that broke something you planted. Then ask an AI tutor to review your answers, and don't let it rewrite them. ## What to take away - Git tracks your history on your computer. GitHub hosts copies and adds collaboration. - A commit is a snapshot, branches are movable labels, and files move from working directory to staging area to repository. - `git status` before everything. Commit small and often, with clear messages that explain why. - Use `.gitignore` for secrets, environments, and big files. A committed secret stays in history, so revoke it. - Almost everything is undoable: `restore`, `amend`, `revert`, `stash`, and `reflog` as a rescue. Only uncommitted work is truly lost. - Branch for every change. Merge conflicts are just Git asking you to choose. Never rewrite history that others have. - Pull requests with small changes, good descriptions, and kind, specific reviews are how teams stay sane. - `blame`, `log`, and `bisect` turn your history into a debugging tool. * * * **Previous:** Post 21, _Databases in Depth_ **Next in the series:** Post 23, _Linux, Shell & Docker_, the daily toolkit: working in the terminal, writing shell commands, and packaging your app so it runs the same everywhere.