{"schemaVersion":"1.0","type":"Article","types":["Article"],"slug":"git-github-workflows-branches-pull-requests-and-code-review-ntiub","url":"https://zyvop.com/git-github-workflows-branches-pull-requests-and-code-review-ntiub","title":"Git & GitHub Workflows: Branches, Pull Requests, and Code Review","subtitle":"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.","tldr":"Git makes breaking things safe. This post explains commits, branches, merge conflicts, and how to undo almost anything, then covers GitHub pull requests, code review, and Git habits for AI projects, finishing with a hands-on project that puts your learning journey online.","keywords":["version control","GitHub","AI Engineer","git","code-review","AI","Zero to Top AI Engineer (2026): Full Series Plan"],"entities":["Pradeep Kumar","version control","GitHub","AI Engineer","git","code-review","AI","Zero to Top AI Engineer (2026): Full Series Plan","ZyVOP"],"keyTakeaways":["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."],"headings":["Git and GitHub are not the same thing","The mental model","One-time setup","The everyday loop","A word on git add .","Writing good commit messages","What to leave out: .gitignore","Undoing things: the safety net","The scary one: reset --hard, and the rescue","Branches and merging","Merge conflicts","Rebase: a tidier alternative","Working with a remote","Connecting to GitHub","Pull requests","A good pull request","Code review","Common workflows","Mistakes you'll make (and how to survive them)","Git as a detective","Git for AI projects","Mini project: put your journey on GitHub","Make it yours","Practice"],"outboundLinks":[],"contentText":"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: working directory --git add--&gt; staging area --git commit--&gt; 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: 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: mkdir prompt-notes cd prompt-notes git initgit init turns the folder into a repository. Now let's create a few files, and ask Git what it sees: git statusOn branch main No commits yet Untracked files: (use \"git add &lt;file&gt;...\" 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: git add README.md .gitignore prompts git commit -m \"Add README, gitignore, and first summarize prompt\"[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: git 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: git log --oneline9d5cb3e Ask for plain language in summarize prompt d9fcc09 Add README, gitignore, and first summarize promptThe 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: 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: .venv/ __pycache__/ .env *.dbGit can tell you which rule matched: git check-ignore -v .env .venv/junk.txt.gitignore:3:.env .env .gitignore:1:.venv/ .venv/junk.txtNeither 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 &lt;file&gt; I staged a file by mistake git restore --staged &lt;file&gt; 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 &lt;commit&gt; 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: printf \"THIS EDIT IS A MISTAKE\\n\" &gt; prompts/summarize.txt git restore prompts/summarize.txtThe 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: git commit -m \"Add clasify prompt\" git commit --amend -m \"Add classify prompt\"4e915f8 Add clasify prompt &lt;- before 8e2f1b4 Add classify prompt &lt;- afterNotice 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: git revert --no-edit HEAD[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: git stash # shelves your uncommitted changes git stash pop # brings them backThe 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: git reset --hard HEAD~1HEAD 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: git reflog065b64b 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: git reset --hard ed39bfaHEAD is now at ed39bfa Add dates promptEverything 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. git switch -c feature/extract-emailsThat 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: git switch main git merge feature/extract-emailsUpdating 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: Merge made by the 'ort' strategy.And git log --oneline --graph draws the shape of what happened: * 4d633d0 Merge branch 'feature/tone-prompt' |\\ | * 0dc7feb Add tone prompt * | c1bdedd Describe prompts folder in README |/ * a5ac0a1 Add email extraction promptTwo 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: 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: &lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD Summarize the text below in three bullet points. ======= Summarize the text below in one sentence. &gt;&gt;&gt;&gt;&gt;&gt;&gt; experiment/short-summaryThe part between &lt;&lt;&lt;&lt;&lt;&lt;&lt; and ======= is your side (the branch you're on). The part between ======= and &gt;&gt;&gt;&gt;&gt;&gt;&gt; is the incoming side. To resolve it: 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.\" Stage it: git add prompts/summarize.txt. 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: * f92240e F2 * 64d00cc F1 | * 0e3344c M1 &lt;- main moved on |/ * bbfda66 AAfter git rebase main on the feature branch: * 65730f4 F2 * ce26bed F1 * 0e3344c M1 * bbfda66 AOne 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 &lt;url&gt; 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: ! [rejected] main -&gt; 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: * 6ad0eae Add translate prompt &lt;- Bob's commit | * ca86132 Summaries are two sentences &lt;- Alice's commit |/ * 0eacee0 Initial commitNow Bob has a choice. Either he merges, or he uses git pull --rebase, which replays his commit on top of Alice's: Successfully rebased and updated refs/heads/main. * f73b44b Add translate prompt * ca86132 Summaries are two sentences * 0eacee0 Initial commitThen 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: 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: git remote add origin https://github.com/YOUR-NAME/ai-engineer-journey.git git push -u origin mainThe -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: Branch: git switch -c fix/retry-bug Commit your work in small, clear commits. Push the branch: git push -u origin fix/retry-bug Open a pull request on GitHub. Review: teammates comment and ask for changes, and you push more commits to the same branch to address them. Checks pass: tests and linters run automatically on every PR (we'll set that up in Post 28). Merge, then delete the branch and update your local main with git switch main &amp;&amp; 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: ## 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 examplesA 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: main is always in a working state. Every change happens on a short-lived branch. Changes get merged through a pull request. 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: git rm --cached config.envThe 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: 203c7b4 Stop tracking config.env 9f8f4d3 Add config &lt;- the key is still readable hereDeleting 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: git blame prompts/summarize.txtcf014ae8 (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 -- &lt;file&gt; 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: git bisect start HEAD &lt;first-commit&gt; git bisect run python3 check.pyrunning 'python3' 'check.py' running 'python3' 'check.py' running 'python3' 'check.py' 6f45376168c02c88eeab0f12ce447976ce49ca97 is the first bad commit Tweak settingsThree 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: git tag -a v0.2.0 -m \"Second release\" git push --tagsA 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: 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 mainStep 3: add a project on a branch. Don't commit straight to main. Do it the professional way: git switch -c add-textkitCopy 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: git push -u origin add-textkitStep 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: git switch main git pullStep 5: make a conflict on purpose. Clone your repo into a second folder, pretending to be a teammate: cd .. git clone https://github.com/YOUR-NAME/ai-engineer-journey.git ai-engineer-journey-2In 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 Write a CONTRIBUTING.md that explains how to branch, name commits, and open a PR. Add a .env.example file and make sure .env is ignored. Check with git check-ignore -v .env. Try git bisect on your own project by deliberately breaking something in the middle of the history. Install pre-commit and add a hook that blocks commits containing something that looks like an API key. 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. Make a repo, commit three changes to one file, and use git log -p to read the story they tell. Break a file on purpose in three different ways, and fix each using restore, revert, and reset --hard with the reflog. 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. 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. Rewrite five bad commit messages into good ones. 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. Write the review comments you'd leave on a PR that logs full user prompts, and has no tests. Make them kind and specific. 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 &amp; Docker, the daily toolkit: working in the terminal, writing shell commands, and packaging your app so it runs the same everywhere.","contentHash":"sha256:9ac2e837e1a0fbf6e9834ff7b34e5c531b0cf55b8b67e70c21e33189831874fe","authorName":"Pradeep Kumar","authorUrl":"https://zyvop.com/author/pradeep","authorSameAs":[],"category":"AI","tags":["version control","GitHub","AI Engineer","git","code-review"],"audience":"Software engineers and developers building applications with AI","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":21,"wordCount":5543,"faqs":null,"primaryTopic":"AI","publishedAt":"2026-11-01T07:47:05.749Z","updatedAt":"2026-11-01T07:47:05.749Z","canonicalUrl":"https://zyvop.com/git-github-workflows-branches-pull-requests-and-code-review-ntiub"}