Simulate a Team Git Workflow

Practice GitHub Flow with branches, PRs, CI, and merge conflict resolution.

Introduction

30 Second Summary

When multiple developers work on the same codebase, things get chaotic fast without a shared workflow. Someone overwrites someone else's code, unfinished features sneak into production, and nobody knows which version is safe to ship.

In this project, you will simulate a professional team Git workflow by building a developer portfolio page. The HTML is intentionally simple. The real deliverable is the GitHub repository and Git history that proves you can operate like a team player.

What You'll Build

A developer portfolio page managed with GitHub Flow, the branching strategy used by most professional web teams.

By the end of this project, you'll have:

  • A protected main branch with required status checks and a GitHub Actions CI workflow that validates your code on every pull request.
  • Hands-on experience with the full GitHub Flow cycle: feature branches, pull requests, merge conflict resolution, and interactive rebase for squashing commits.
  • A clean Git history with squashed commits, conventional commit messages, and a linear history you can reference in interviews.
  • Secret Mission: Use git reflog to recover a lost commit after a hard reset, proving you can handle Git disasters.

Are there any prerequisites?

You should be comfortable with basic Git commands like add, commit, push, and pull. You also need a GitHub account.

Set Up Your Repository and Branch Protection

Professional development teams never push code directly to their main branch. They use a workflow called GitHub Flow where every change goes through a feature branch and a pull request before it reaches production.

In this step, you will create a GitHub repository, add a CI workflow that validates your code on every pull request, and lock down the main branch so that nobody (including you) can push to it directly.

In this step, get ready to:
  • Create a public GitHub repository with initial project files.
  • Add a GitHub Actions CI workflow that runs on every pull request.
  • Enable branch protection so direct pushes to main are rejected.
Create a new GitHub repository and clone it locally

You need a remote repository on GitHub to store your code and host your pull requests. You will create one, then clone it to your machine so you can work locally.

  • Open your browser and go to github.com.
  • In the upper-right corner of any page, click the + menu, then click New repository.
  • Type dev-portfolio in the repository name field.
  • Select Public for the repository visibility.
  • Leave all other options unchecked (do not initialize with a README or .gitignore).
  • Click Create repository.

Why no README initialization?

You will create all project files locally and push them yourself. Initializing on GitHub would create a commit you do not have locally, forcing you to deal with a merge before you have even started.

GitHub will show you the repository's quick setup page with a URL you need for cloning.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone your new repository by running this command:
git clone https://github.com/[[GITHUB_USERNAME="your-github-username"]]/dev-portfolio.git
  • Move into the project folder by running:
cd dev-portfolio

Clone failing with 'repository not found'?

Double-check that your GitHub username in the URL matches your actual account name. The repository must also be set to Public.

If you are still stuck, help me troubleshoot my git clone error.

Now you will create the initial project files. These give the repository something to validate in your CI workflow later.

  • Open VS Code in the dev-portfolio folder by running:
code .
  • In VS Code, create a new file called index.html by clicking File then New File.
  • When prompted for a filename, type index.html and press Enter.
  • Paste the following HTML into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dev Portfolio</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
</body>
</html>

What does this code do?

  • This is a minimal HTML boilerplate. It declares the document type, sets the character encoding, and links to a style.css file you will create in a later step.
  • The <!DOCTYPE html> declaration is important. Your CI workflow will check for this to validate the file's structure.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Create another new file called README.md and paste the following content:
# Dev Portfolio

A developer portfolio page built to practice professional Git workflows.

## Workflow

This repository follows GitHub Flow:
- `main` is always deployable and protected
- All changes go through feature branches and pull requests
- CI runs on every PR via GitHub Actions
- Commits are squashed for a clean history

What does this code do?

The README describes the workflow rules your repository follows. Anyone visiting your repo on GitHub will see this on the landing page. It signals that you understand professional Git practices.

  • Save README.md.
  • Back in your terminal, commit and push both files by running these commands:
git add .
git commit -m "docs: add initial index.html and README"
git push origin main
  • Refresh your dev-portfolio repository page on GitHub. You should see index.html and README.md listed, with the README content displayed below the file list.

Push rejected or asking for credentials?

If Git asks for a username and password, GitHub no longer accepts password authentication. Set up a personal access token or SSH key instead. GitHub will show instructions on your repository page under the HTTPS/SSH toggle.

If you need help, help me authenticate Git with GitHub.

Add a GitHub Actions CI workflow

A CI (continuous integration) workflow automatically runs checks every time someone opens a pull request. Your workflow will validate that index.html exists and has a proper DOCTYPE declaration.

  • GitHub Actions looks for workflow files inside a .github/workflows/ directory. Create that directory structure by running:
mkdir -p .github/workflows
  • In VS Code, create a new file at .github/workflows/ci.yml and paste the following workflow:
name: CI

on:
  pull_request:
    branches: ["main"]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Validate index.html exists
        run: |
          if [ ! -f index.html ]; then
            echo "ERROR: index.html not found"
            exit 1
          fi
          echo "index.html found - validation passed"
      - name: Check HTML structure
        run: |
          if grep -q "<!DOCTYPE html>" index.html; then
            echo "Valid HTML document"
          else
            echo "ERROR: Missing DOCTYPE declaration"
            exit 1
          fi

What does this workflow do?

  • on: pull_request triggers this workflow whenever someone opens a PR targeting main.
  • runs-on: ubuntu-latest tells GitHub to run the job on a Linux virtual machine.
  • actions/checkout@v4 checks out your repository code onto that machine so the scripts can access your files.
  • The two run steps are shell scripts. The first checks that index.html exists. The second checks that it contains a DOCTYPE declaration.
  • If either check fails, exit 1 causes the workflow to report a failure. This blocks the PR from being merged.
  • Save ci.yml.
  • Commit and push the workflow file by running:
git add .
git commit -m "ci: add validation workflow for pull requests"
git push origin main
  • On GitHub, click the Actions tab in your repository. You should see the CI workflow listed on the left sidebar. It has not run yet because it only triggers on pull requests.

Don't see the workflow in the Actions tab?

Make sure the file is saved at exactly .github/workflows/ci.yml. The path is case-sensitive. If you placed it in the wrong directory, GitHub will not detect it.

If you need help, help me find why my GitHub Actions workflow is not showing up.

Enable branch protection on main

Branch protection rules prevent anyone from pushing directly to main. Every change must go through a pull request and pass CI first. This is how professional teams keep their production branch stable.

  • In your dev-portfolio repository on GitHub, click the Settings tab.
  • In the left sidebar under Code and automation, click Branches.
  • Click Add branch protection rule.
  • In the Branch name pattern field, type main.
  • Check the box for Require a pull request before merging.
  • Check the box for Require status checks to pass before merging.
  • In the search field that appears, type validate and select the validate check from the list.
  • Scroll to the bottom and click Create to save the rule.

Why is the status check called 'validate'?

GitHub uses the job name from your workflow file as the status check name. In your ci.yml, the job is named validate under the jobs: key. That is the name that appears in branch protection settings.

Status check not appearing in the search?

The validate check only appears in the search after the workflow has run at least once. If you do not see it, you can still save the rule and the check will be enforced once it runs on your first PR.

Now confirm that branch protection is working. Try pushing a change directly to main.

  • In your terminal, create a test file and attempt to push it directly by running:
echo "test" > test.txt
git add test.txt
git commit -m "test: verify branch protection"
git push origin main

You should see an error message from GitHub rejecting the push. The message will mention that the protected branch update failed.

  • Clean up the test commit by running:
git reset --hard HEAD~1

What does git reset --hard HEAD~1 do?

This removes the last commit from your local history entirely. HEAD~1 means "one commit before where I am now." Since the push was rejected, the commit only existed locally, so resetting it leaves your local branch in sync with the remote.

Push was NOT rejected?

If the push succeeded, your branch protection rule is not active. Go back to Settings > Branches and verify the rule targets main (not master or a different name). Make sure both checkboxes are enabled.

If you are stuck, help me set up branch protection on my repository.

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files and that your GitHub repository shows index.html, README.md, and .github/workflows/ci.yml.

ⓧ I'd like to double check the full code

Here are the complete files you should have at the end of this step.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dev Portfolio</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
</body>
</html>
# Dev Portfolio

A developer portfolio page built to practice professional Git workflows.

## Workflow

This repository follows GitHub Flow:
- `main` is always deployable and protected
- All changes go through feature branches and pull requests
- CI runs on every PR via GitHub Actions
- Commits are squashed for a clean history
name: CI

on:
  pull_request:
    branches: ["main"]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Validate index.html exists
        run: |
          if [ ! -f index.html ]; then
            echo "ERROR: index.html not found"
            exit 1
          fi
          echo "index.html found - validation passed"
      - name: Check HTML structure
        run: |
          if grep -q "<!DOCTYPE html>" index.html; then
            echo "Valid HTML document"
          else
            echo "ERROR: Missing DOCTYPE declaration"
            exit 1
          fi

Your repository is locked down and ready for professional team-style development. Next up, you will create your first feature branch and experience the full pull request workflow.

Create Your First Feature Branch and Pull Request

Your repository is locked down. No one can push directly to main, which means every change must go through a pull request.

This is exactly how professional teams work with GitHub Flow. You create a short-lived feature branch, make your changes, push the branch, then open a PR for review and CI checks before merging.

In this step, get ready to:
  • Create a feature branch and build the portfolio header with HTML and CSS.
  • Open a pull request on GitHub and watch CI run.
  • Merge the PR using squash and merge for a clean commit history.
Create a feature branch and build the portfolio header

In GitHub Flow, every new feature starts on its own branch. This keeps main stable while you experiment freely on your branch.

  • In your terminal, make sure you're in the dev-portfolio folder.
  • Create and switch to a new feature branch by running this command:
git checkout -b feature/add-header

What does this command do?

The -b flag creates a new branch and switches to it in one step. The name feature/add-header follows a common naming convention: the prefix describes the type of work, and the rest describes the change.

Now you'll build the navigation bar for your portfolio. Professional teams use conventional commits to label each commit with a prefix like feat: or fix: so the history is readable at a glance.

  • Open index.html in VS Code.
  • Find the empty <body> tags.
  • Add the following navigation section inside the <body> tags:
    <nav>
        <a href="#about">About</a>
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
    </nav>

What does this code do?

The <nav> element creates a navigation bar with anchor links. Each link points to a section you'll add later in the project using id attributes.

  • Save index.html.
  • Stage and commit this change by running these commands:
git add index.html
git commit -m "feat: add navigation bar"

Now add the header section with your name and title. This is the hero area visitors see first.

  • In index.html, add the following header section below the closing </nav> tag:

    <header>
        <h1>Your Name</h1>
        <p>Software Developer</p>
    </header>

What does this code do?

The <header> section acts as the hero banner for your portfolio page. It displays your name and role prominently at the top.

  • Save index.html.
  • Now create a new file called style.css in the same folder as index.html.
  • Paste the following CSS into style.css:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
    color: #1a1a2e;
    background-color: #f5f5f5;
}

nav {
    background-color: #1a1a2e;
    padding: 1rem 2rem;
    display: flex;
    gap: 1.5rem;
}

nav a {
    color: #e0e0e0;
    text-decoration: none;
}

nav a:hover {
    color: #ffffff;
}

header {
    text-align: center;
    padding: 4rem 2rem;
    background-color: #16213e;
    color: #ffffff;
}

header h1 {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
}

What does this code do?

The CSS resets default browser spacing, then styles the nav as a dark horizontal bar with light links. The header gets a centered layout with a dark blue background to make your name stand out.

  • Save style.css.
  • Stage both files and commit by running these commands:
git add .
git commit -m "feat: add hero section with name and title"

Why multiple small commits?

Making focused commits on a feature branch lets your reviewer see the progression of your work. Each commit tells a story. When you merge, these commits get squashed into one clean entry in main.

  • Push your feature branch to GitHub by running this command:
git push -u origin feature/add-header
  • You should see output confirming a new remote branch was created on GitHub.

Push rejected or authentication failed?

If you see an authentication error, make sure your Git credentials (SSH key or personal access token) are configured for this repository.

If you see a rejection mentioning protected branches, double check you are pushing to feature/add-header and not main. Branch protection only blocks direct pushes to main.

Help me troubleshoot my git push failing

Open a pull request and observe CI

Your code is on GitHub, but it is not in main yet. The only way to get it there is through a pull request. This is where your CI workflow will validate your code automatically.

  • Open your dev-portfolio repository on GitHub in your browser.
  • You should see a yellow banner saying your branch had recent pushes with a Compare & pull request button. Click it.
  • If you do not see the banner, click the Pull requests tab, then click New pull request. Select feature/add-header as the compare branch.
  • In the PR title field, enter:
feat: add header section
  • In the description field, write a brief summary of what this PR does. For example: "Adds a navigation bar and hero header with name and title to the portfolio page. Includes base CSS styling."
  • Click Create pull request.

After creating the PR, scroll down to the checks section at the bottom. You should see your GitHub Actions CI workflow running.

  • Wait for the check to complete. A green checkmark confirms your code passed validation.

What is the CI check doing?

The workflow you set up in Step 1 triggers on every PR to main. It checks that index.html exists and has a valid DOCTYPE declaration. Since your file has both, the check passes.

CI check not appearing or failing?

If the check does not appear, make sure your .github/workflows/ci.yml file exists on the main branch (not just on your feature branch).

If the check fails, click Details next to the failed check to see the log output. Confirm your index.html starts with <!DOCTYPE html>.

Help me figure out why my CI check is failing

Merge the pull request

With CI green, your PR is ready to merge. You will use Squash and merge to combine your 2 commits into a single clean entry on main. This keeps the history readable.

  • On the PR page, find the green Merge pull request button at the bottom.
  • Click the small dropdown arrow next to the button.
  • Select Squash and merge from the dropdown.
  • Click Squash and merge to confirm.
  • Click Confirm squash and merge.

Why squash and merge?

Your feature branch had 2 commits ("feat: add navigation bar" and "feat: add hero section with name and title"). Squash and merge combines them into one commit on main.

This means anyone reading main's history sees one commit per feature instead of dozens of intermediate commits. This is how most professional teams keep their history clean.

  • After the merge completes, GitHub will offer to delete the branch. Click Delete branch to clean up the remote branch.

Now sync your local repository with the merged changes.

  • Switch back to main and pull the latest changes by running these commands:
git checkout main
git pull
  • Verify your clean history by running:
git log --oneline

You should see one squash-merged commit for the entire header feature. No individual "feat: add navigation bar" or "feat: add hero section" commits appear on main. Just one clean entry.

Seeing multiple commits instead of one?

If you see both individual commits on main, you likely used "Create a merge commit" instead of "Squash and merge" when merging the PR. This is not a problem for continuing the project. The next PR will give you another chance to use squash and merge.

Help me understand my git log output

✔️ Awesome, I've got everything!

Great. Double check you've saved both files, then move on.

ⓧ I'd like to double check the full code

Here is what your files should look like after this step.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dev Portfolio</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <a href="#about">About</a>
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
    </nav>

    <header>
        <h1>Your Name</h1>
        <p>Software Developer</p>
    </header>
</body>
</html>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
    color: #1a1a2e;
    background-color: #f5f5f5;
}

nav {
    background-color: #1a1a2e;
    padding: 1rem 2rem;
    display: flex;
    gap: 1.5rem;
}

nav a {
    color: #e0e0e0;
    text-decoration: none;
}

nav a:hover {
    color: #ffffff;
}

header {
    text-align: center;
    padding: 4rem 2rem;
    background-color: #16213e;
    color: #ffffff;
}

header h1 {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
}

You have completed one full cycle of GitHub Flow. Your feature went from a branch to a PR to a clean commit on main. Next, you will create a merge conflict on purpose and learn how to resolve it.

Create and Resolve a Merge Conflict

In a real team, two developers often work on related parts of the codebase at the same time. When both change the same lines in the same file, Git cannot decide which version to keep. It marks the file as conflicted and asks you to resolve it manually.

In this step, you will deliberately create this situation by making two branches that both style the same About Me section with different color schemes. One branch merges cleanly. The second branch creates a conflict that you resolve by hand.

In this step, get ready to:
  • Set up two branches that modify the same CSS file in conflicting ways.
  • Merge the first branch and observe GitHub flagging the conflict on the second.
  • Resolve the conflict locally and complete the merge through a pull request.
Set up two conflicting branches

You will create two branches from the same starting point on main. Both will add an About Me section to the portfolio, but with different CSS color schemes. This guarantees a conflict when the second branch tries to merge.

  • Create and switch to the first feature branch by running this command:
git checkout -b feature/add-about-dark
  • In VS Code, open index.html. Find the closing </header> tag, then add the following About Me section below it:
    <section id="about">
        <h2>About Me</h2>
        <p>I am a developer passionate about building great software and collaborating with teams using modern workflows.</p>
    </section>

What does this code do?

This adds a new page section with an id of about that the nav link from earlier already points to. The content is simple on purpose. The focus of this step is the Git workflow, not the HTML.

  • Now open style.css and add the following dark-themed styles at the bottom of the file:
section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
    background-color: #2d2d44;
}

section h2 {
    margin-bottom: 1rem;
    color: #e0e0e0;
}

Why a dark color scheme?

You are choosing a dark background (#2d2d44) and light heading color (#e0e0e0) for this branch. The second branch will use a light scheme instead. These conflicting values in the same CSS properties are what create the merge conflict.

  • Save both files.
  • Commit and push this branch by running these commands:
git add .
git commit -m "feat: add about section with dark theme"
git push -u origin feature/add-about-dark
  • Switch back to main by running:
git checkout main

You are back on main, which does not have the About Me section yet. Now you will create the second branch from this same starting point.

  • Create and switch to the second feature branch by running:
git checkout -b feature/add-about-light
  • Open index.html and add the exact same About Me section below </header>:
    <section id="about">
        <h2>About Me</h2>
        <p>I am a developer passionate about building great software and collaborating with teams using modern workflows.</p>
    </section>
  • Open style.css and add the following light-themed styles at the bottom of the file:
section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
}

section h2 {
    margin-bottom: 1rem;
    color: #1a1a2e;
}

What is different here?

This version has no background-color on section (it inherits the page background) and uses a dark heading color (#1a1a2e). Both branches wrote to the same lines in style.css. Git will not know which version to keep.

  • Save both files.
  • Commit and push this branch by running:
git add .
git commit -m "feat: add about section with light theme"
git push -u origin feature/add-about-light

Push rejected?

If you see an error about the remote branch already existing, double-check you are on feature/add-about-light (not main) by running git branch. The asterisk marks your current branch.

Help me fix this push rejection error

Merge the first branch, then observe the conflict

With both branches pushed, you will merge the dark branch first. Then you will see what happens when the light branch tries to merge into a main that has already changed.

  • Switch to your browser and open your dev-portfolio repository on GitHub.
  • Click the Pull requests tab.
  • Click New pull request.
  • Set the compare branch to feature/add-about-dark.
  • Click Create pull request.
  • Give it the title feat: add about section with dark theme and click Create pull request again.
  • Wait for the CI check to pass (green checkmark).
  • Click the Squash and merge button.
  • Click Confirm squash and merge.
  • Click Delete branch to clean up the remote branch.

Main now has the dark-themed About Me section. Time to update your local copy.

  • Return to your terminal and pull the latest main by running:
git checkout main
git pull

Now open a pull request for the light branch and observe what GitHub reports.

  • Back on GitHub, click New pull request again.
  • Set the compare branch to feature/add-about-light.
  • Click Create pull request, give it the title feat: add about section with light theme, and click Create pull request once more.

GitHub will display a message indicating this branch has conflicts that must be resolved before merging. You cannot merge from the GitHub UI until the conflict is fixed.

Why does GitHub show a conflict?

Both branches modified the same lines in style.css. The dark branch already merged those lines into main. Now the light branch's version of those same lines contradicts what is already there. Git cannot choose automatically, so it blocks the merge.

Resolve the conflict locally

To fix a merge conflict, you bring the latest main into your feature branch locally, resolve the conflicting lines by hand, and push the result.

  • Switch to the light branch locally by running:
git checkout feature/add-about-light
  • Merge main into this branch by running:
git merge main

Git will print a message saying there is a conflict and automatic merge has failed. It names the conflicting file(s).

  • Open style.css in VS Code. You will see conflict markers that look like this:
<<<<<<< HEAD
section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
}

section h2 {
    margin-bottom: 1rem;
    color: #1a1a2e;
}
=======
section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
    background-color: #2d2d44;
}

section h2 {
    margin-bottom: 1rem;
    color: #e0e0e0;
}
>>>>>>> main

What do the conflict markers mean?

The <<<<<<< marker starts the conflict. Everything between it and ======= is YOUR branch's version (the light theme). Everything between ======= and >>>>>>> is what is already on main (the dark theme).

Your job is to decide what the final code should look like. You can keep one side, the other side, or combine them however you want. Then you delete all the marker lines.

  • Delete all three marker lines (<<<<<<<, =======, >>>>>>>).
  • Keep the light theme version (your branch's code). Remove the dark theme lines so the section styles look like this:
section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
}

section h2 {
    margin-bottom: 1rem;
    color: #1a1a2e;
}

What about index.html?

You may also see a conflict in index.html because both branches added the same About Me section in the same spot. Since the content is identical on both sides, keep one copy and delete the markers and the duplicate.

  • If index.html also has conflict markers, resolve it by keeping a single copy of the About Me section. The final index.html should have one <section id="about"> block after </header>.
  • Save both files.
  • Stage the resolved files and commit by running:
git add .
git commit -m "resolve: merge conflict, keep light theme"
  • Push the resolved branch by running:
git push

Back on GitHub, your pull request for feature/add-about-light now shows no conflicts. The CI check runs again.

  • On GitHub, wait for CI to pass (green checkmark).
  • Click Squash and merge.
  • Click Confirm squash and merge.
  • Click Delete branch.
  • Return to your terminal and update your local main by running:
git checkout main
git pull
  • Verify your history by running:
git log --oneline

You should see clean squash-merged commits for both the dark and light about features. The messy merge-conflict resolution commit stays hidden inside the squash.

Still seeing a conflict on the PR?

Make sure you pushed from the feature/add-about-light branch, not from main. Run git branch to confirm you are on the right branch before pushing.

Also confirm you removed all conflict markers from the files. Leftover <<<<<<< or >>>>>>> lines will fail the CI HTML validation check.

Help me debug my conflict resolution

✔️ Awesome, I've got everything!

Great. Double check you have saved both index.html and style.css and that your main branch is up to date.

ⓧ I'd like to double check the full code

After resolving the conflict and merging both PRs, your files on main should look like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dev Portfolio</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <a href="#about">About</a>
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
    </nav>

    <header>
        <h1>Your Name</h1>
        <p>Software Developer</p>
    </header>

    <section id="about">
        <h2>About Me</h2>
        <p>I am a developer passionate about building great software and collaborating with teams using modern workflows.</p>
    </section>
</body>
</html>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
    color: #1a1a2e;
    background-color: #f5f5f5;
}

nav {
    background-color: #1a1a2e;
    padding: 1rem 2rem;
    display: flex;
    gap: 1.5rem;
}

nav a {
    color: #e0e0e0;
    text-decoration: none;
}

nav a:hover {
    color: #ffffff;
}

header {
    text-align: center;
    padding: 4rem 2rem;
    background-color: #16213e;
    color: #ffffff;
}

header h1 {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
}

section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
}

section h2 {
    margin-bottom: 1rem;
    color: #1a1a2e;
}

You just resolved your first merge conflict. This is one of the most common interview questions for developer roles. Next up, you will learn how to rewrite messy commit history into something clean using interactive rebase.

Rebase and Squash Your Commits

You have resolved a real merge conflict and proved you can handle competing changes. Now it is time to tackle another professional skill: cleaning up messy Git history.

In real development, you rarely write perfect code in one shot. You make small fixes, forget things, and add more changes. That means your branch ends up with commit messages like "fix typo" and "oops forgot semicolon." Before merging to main, professionals squash these into one clean commit using interactive rebase.

In this step, get ready to:
  • Create a feature branch with intentionally messy commits.
  • Use interactive rebase to squash four commits into one clean entry.
  • Push rewritten history safely and merge via pull request.
Create a messy feature branch

You will add a projects section to your portfolio. Instead of writing it in one perfect commit, you will intentionally split it into four small, sloppy commits. This simulates how real development actually looks before cleanup.

  • Make sure you are on main with the latest changes by running:
git checkout main
git pull origin main
  • Create and switch to a new feature branch by running:
git checkout -b feature/add-projects

Now you will build the projects section across four commits. Here is the full HTML you will end up with, but you will add it in pieces.

  • In VS Code, open index.html.
  • Find the closing </section> tag of the About Me section.
  • Below it (but above </body>), add the following projects section with just one card:
    <section id="projects">
        <h2>Projects</h2>
        <div class="project-card">
            <h3>Project One</h3>
            <p>A description of your first project.</p>
        </div>
    </section>
  • Save index.html and make your first messy commit by running:
git add index.html
git commit -m "add projects section roughly"
  • Now add the second project card. In index.html, find the closing </div> of the first project card.
  • Below it (but still inside the <section id="projects">), add:
        <div class="project-card">
            <h3>Project Two</h3>
            <p>A description of your second project.</p>
        </div>
  • Save and make your second commit:
git add index.html
git commit -m "actually add the projects"
  • Now open style.css and add the project card styling at the bottom of the file:
.project-card {
    background-color: #ffffff;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1rem;
}
  • Save and commit with another messy message:
git add style.css
git commit -m "style the cards"
  • Add one more CSS rule below .project-card for the card headings:
.project-card h3 {
    margin-bottom: 0.5rem;
    color: #16213e;
}
  • Save and make your fourth messy commit:
git add style.css
git commit -m "oops forgot semicolon"
  • Check your commit history by running:
git log --oneline

You should see four messy commits at the top of the log. Commit messages like "oops forgot semicolon" and "actually add the projects" would look unprofessional on a shared main branch.

Why do messy commits matter?

When a team reviews your main branch history, each commit should tell a clear story. A commit log full of "fix typo" and "oops" makes it hard to understand what actually changed and when.

Professional teams squash work-in-progress commits into one clean commit before merging. This keeps the main branch history readable and useful for debugging.

Don't see four commits?

Make sure you saved the file before each git add command. Git only stages saved changes.

If you see fewer than four commits, add another small change (like editing a card description) and commit it with a silly message. You need at least four commits for the rebase exercise.

Need help? Why aren't my commits showing up in git log?

Rebase onto main and squash commits

Interactive rebase lets you rewrite your branch's commit history before sharing it. You will combine all four commits into a single commit with a clean, professional message.

  • Start an interactive rebase for the last four commits by running:
git rebase -i HEAD~4

This opens an editor showing your four commits, each prefixed with the word pick.

What is interactive rebase?

The git rebase -i HEAD~4 command opens an editor listing your last 4 commits. Each line starts with pick, meaning "keep this commit as-is."

By changing pick to squash (or just s), you tell Git to combine that commit into the one above it. The first commit keeps pick because every squash needs a base commit to merge into.

  • In the editor, you will see something like this:
pick abc1234 add projects section roughly
pick def5678 actually add the projects
pick ghi9012 style the cards
pick jkl3456 oops forgot semicolon
  • Change pick to squash (or s) on all lines except the first one, so it looks like:
pick abc1234 add projects section roughly
squash def5678 actually add the projects
squash ghi9012 style the cards
squash jkl3456 oops forgot semicolon
  • Save and close the editor.
  • Git opens a second editor for the combined commit message. Delete all the existing text and replace it with a clean conventional commit message:
feat: add projects section with card layout
  • Save and close this editor to complete the rebase.
  • Verify the squash worked by running:
git log --oneline

You should now see one clean commit instead of four. The message reads feat: add projects section with card layout.

VS Code as your Git editor

If the rebase opened in a terminal editor like Vim, you can press i to enter insert mode, make your changes, then press Esc followed by :wq to save and quit.

To use VS Code as your default Git editor in the future, run: git config --global core.editor "code --wait".

Rebase got stuck or errored?

If something went wrong during the rebase, you can safely abort and start over by running git rebase --abort. This returns your branch to the exact state it was in before you started.

Make sure you only changed pick to squash on lines 2, 3, and 4. The first line must stay as pick.

Need help? Help me fix my interactive rebase

Why squash instead of just merging?

Squashing gives you a linear, readable history. When someone runs git log on main six months from now, they will see one commit per feature instead of dozens of work-in-progress entries.

In interviews, teams want to hear that you keep history clean. Knowing how to squash with interactive rebase is a strong signal that you understand professional workflows.

Push and merge the rebased branch

Your local branch now has rewritten history. A normal git push will be rejected because the remote branch still has the old four commits. You need a force push, but you will use the safer version.

  • Push your rebased branch to GitHub by running:
git push --force-with-lease origin feature/add-projects

Why --force-with-lease instead of --force?

A regular --force overwrites the remote branch no matter what. If a teammate had pushed new commits, you would destroy their work.

The --force-with-lease flag checks that the remote branch is in the state you expect. If someone else pushed in the meantime, it refuses the push and protects their work. Always use this version on team projects.

  • On GitHub, open a pull request from feature/add-projects to main.
  • Write a descriptive PR title like "Add projects section with card layout."
  • Wait for the CI check to pass (green checkmark).
  • Click Squash and merge to merge the PR.
  • Click Delete branch to clean up the remote branch.
  • Switch back to main and pull the merged changes by running:
git checkout main
git pull origin main
  • Verify your clean history by running:
git log --oneline

Your entire projects feature should appear as a single commit with a clear message. No "fix typo" or "oops" commits visible.

Push was rejected even with --force-with-lease?

This means the remote branch was updated since you last fetched. Run git fetch origin and check if someone (or a previous push) changed the branch.

If this is your personal repo and nobody else pushed, try the push command again. The fetch updates your local reference and the next push should succeed.

Need help? Why is my force push being rejected?

✔️ Awesome, I've got everything!

Great. Double check you have saved both index.html and style.css, and that your main branch is up to date.

ⓧ I'd like to double check the full code

Here is the full state of each file after this step.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dev Portfolio</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <a href="#about">About</a>
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
    </nav>

    <header>
        <h1>Your Name</h1>
        <p>Software Developer</p>
    </header>

    <section id="about">
        <h2>About Me</h2>
        <p>I am a developer passionate about building great software and collaborating with teams using modern workflows.</p>
    </section>

    <section id="projects">
        <h2>Projects</h2>
        <div class="project-card">
            <h3>Project One</h3>
            <p>A description of your first project.</p>
        </div>
        <div class="project-card">
            <h3>Project Two</h3>
            <p>A description of your second project.</p>
        </div>
    </section>
</body>
</html>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: system-ui, -apple-system, sans-serif;
    line-height: 1.6;
    color: #1a1a2e;
    background-color: #f5f5f5;
}

nav {
    background-color: #1a1a2e;
    padding: 1rem 2rem;
    display: flex;
    gap: 1.5rem;
}

nav a {
    color: #e0e0e0;
    text-decoration: none;
}

nav a:hover {
    color: #ffffff;
}

header {
    text-align: center;
    padding: 4rem 2rem;
    background-color: #16213e;
    color: #ffffff;
}

header h1 {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
}

section {
    max-width: 800px;
    margin: 2rem auto;
    padding: 2rem;
}

section h2 {
    margin-bottom: 1rem;
    color: #1a1a2e;
}

.project-card {
    background-color: #ffffff;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1rem;
}

.project-card h3 {
    margin-bottom: 0.5rem;
    color: #16213e;
}

Secret mission

Recover a Lost Commit with Reflog

What happens when you accidentally delete your work? In this challenge, you'll simulate a Git disaster by hard-resetting a commit into oblivion, then use git reflog to bring it back from the dead. This is one of the most common advanced Git interview questions.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on GitHub's free tier and your local machine, so there are no ongoing costs. Decide whether to keep your resources as a portfolio piece, pause them, or delete them entirely.

Resources you used:

  • GitHub repository dev-portfolio (remote, on GitHub).
  • Local dev-portfolio folder on your Desktop.
  • GitHub Actions minutes consumed (free tier covers 2,000 minutes/month for public repos).

Keep everything running

No action needed. Choose this if you want to keep the repository public as a portfolio piece for interviews.

  • Your dev-portfolio repository stays live on GitHub at no cost.
  • Your local dev-portfolio folder stays on your Desktop for future edits.
  • GitHub Actions only runs when you open new pull requests, so it consumes no minutes while idle.

Pause - I'll come back to this later

Archive the repository to make it read-only and prevent accidental changes, while keeping it visible on your profile.

  • On GitHub, open your dev-portfolio repository.
  • Click Settings.
  • Scroll down to the Danger Zone section.
  • Click Archive this repository.
  • Type the repository name to confirm, then click I understand the consequences, archive this repository.
  • You can unarchive later from the same Danger Zone section when you're ready to resume.

Optionally, delete the local folder to free up disk space.

  • Delete the local folder by running this command:
cd ~/Desktop
rm -rf dev-portfolio

You can always clone the repository again from GitHub when you want to continue.

Delete - I don't want to use this again

Remove all project resources completely. This cannot be undone.

  • Delete the GitHub repository:
  • On GitHub, open your dev-portfolio repository.
  • Click Settings.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Type the repository name to confirm, then click I understand the consequences, delete this repository.
  • Delete the local folder by running this command:
cd ~/Desktop
rm -rf dev-portfolio
  • Confirm the folder is gone by running:
ls ~/Desktop | grep dev-portfolio

If no output appears, the folder has been removed successfully.

Nice Work!

Nice Work!

You just simulated a full team Git workflow from scratch. Your GitHub repository now tells a story that interviewers can read: protected branches, CI checks, resolved conflicts, and a clean squashed history.

You've learned how to:

  • Practice the complete GitHub Flow workflow with feature branches, pull requests, CI status checks, and squash merges.
  • Create and resolve a real merge conflict between two competing branches editing the same file.
  • Use interactive rebase to squash messy commits into clean, professional history and push safely with --force-with-lease.
  • Secret Mission: Recovered a lost commit using git reflog, proving you can handle Git disasters with confidence.

Ready to quiz yourself?