Git Workflow for Capstone Teams: Branches, Pull Requests and Fixing Merge Conflicts

Almost every capstone team has lived this story. Three members work on the same system, files get passed around in group chats as login_final_v3_REAL.zip, someone copies an old version over a week of work, and two days before the defense nobody is sure which code actually runs. Git and GitHub fix this, but only if the whole team follows the same simple workflow.

This guide gives you a workflow that is easy enough for first-time Git users and solid enough for a real project: one protected main branch, one branch per feature, pull requests for review, and a calm way to fix merge conflicts. To show exactly what you will see on screen, we simulated a two-person team (Ana and Ben) on one computer and copied the real Git output below. In the demo, the shared "remote" is a local folder instead of GitHub, but the commands and messages are the same.

The Rules in One Minute

  1. Nobody commits directly to main. main always contains code that runs and can be demoed to your adviser.
  2. One branch per feature or fix, named clearly: feature/login, feature/reports, fix/receipt-total.
  3. Small commits with clear messages, several times a day: "Add login validation", not "update" or "asdf".
  4. Every branch goes into main through a pull request that at least one teammate reviews.
  5. Pull main into your branch often, so conflicts stay small.
  6. Never commit secrets or generated files. Use a .gitignore from day one.

Step 1: One Person Sets Up the Repository

The team leader creates the repository on GitHub (private is fine; add your teammates as collaborators under Settings > Collaborators), clones it, adds the starting files and a .gitignore, and pushes. Our .gitignore keeps out local config, dependencies and the .env file:

config.local.php
vendor/
node_modules/
.env
$ git add .

$ git commit -m "Initial project structure"
[main (root-commit) 6893849] Initial project structure
 2 files changed, 8 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 config.php

$ git push -u origin main
To /tmp/capstone-demo/remote.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Everyone else clones the repository once with git clone <url>. Ben already had a clone, so he just pulled:

$ git pull
From /tmp/capstone-demo/remote
 * [new branch]      main       -> origin/main

Step 2: Protect the main Branch

On GitHub, open Settings > Branches (or Rules > Rulesets on newer layouts) and add a rule for main that requires a pull request before merging, with at least one approval. This turns rule number 1 from a promise into something GitHub enforces. If an option is not available for your private repository on a free plan, keep the rule as a team agreement, and the leader should be the one who clicks Merge.

Step 3: Work on Your Own Branch

Before starting a feature, create a branch from an up-to-date main. Ana works on the login page. Notice that she also changed the database name in the shared config.php:

$ git switch -c feature/login
Switched to a new branch 'feature/login'

$ git add .

$ git commit -m "Add login page and rename database"
[feature/login 10e16af] Add login page and rename database
 2 files changed, 4 insertions(+), 1 deletion(-)
 create mode 100644 login.php

$ git push -u origin feature/login
To /tmp/capstone-demo/remote.git
 * [new branch]      feature/login -> feature/login
branch 'feature/login' set up to track 'origin/feature/login'.

At the same time, Ben starts the reports module on his own branch and, without knowing it, edits the same line in config.php:

$ git switch -c feature/reports
Switched to a new branch 'feature/reports'

$ git add .

$ git commit -m "Add reports page and use school_db"
[feature/reports 06b6514] Add reports page and use school_db
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 reports.php

Branches let both of them work without blocking each other. git switch -c creates a branch and moves to it. Older tutorials use git checkout -b, which does the same thing.

Step 4: Open a Pull Request and Review It

After pushing, Git prints a link to create a pull request (PR) on GitHub. A good PR for a student team has:

  • A short title: "Add login page".
  • A description of what changed and how to test it: "Log in with admin/admin123, wrong password shows an error".
  • A screenshot if the change is visual.

The reviewer pulls the branch, runs it, reads the changes in the Files changed tab, and either approves or leaves comments. Reviewing is also the best way for every member to understand the whole system, which helps a lot when the panel asks "Who made this part?" during your defense.

Here Ana's PR was approved and merged. On GitHub you click Merge pull request. In our local demo, the same merge looks like this:

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git merge --no-ff feature/login -m "Merge feature/login"
Merge made by the 'ort' strategy.
 config.php | 2 +-
 login.php  | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)
 create mode 100644 login.php

$ git push
To /tmp/capstone-demo/remote.git
   6893849..3f0aa8f  main -> main

Step 5: Update Your Branch and Fix the Merge Conflict

Now main has Ana's work, and Ben's branch is behind. Before opening his own PR, Ben brings main into his branch. Because both changed the same line, Git cannot decide alone and reports a conflict:

$ git fetch origin
From /tmp/capstone-demo/remote
 * [new branch]      feature/login -> origin/feature/login
   6893849..3f0aa8f  main          -> origin/main

$ git merge origin/main
Auto-merging config.php
CONFLICT (content): Merge conflict in config.php
Automatic merge failed; fix conflicts and then commit the result.

$ git status --short
UU config.php
A  login.php

$ cat config.php
<?php
// Database connection
$host = "localhost";
<<<<<<< HEAD
$db   = "school_db";
=======
$db   = "enrollment_db";
>>>>>>> origin/main

Don't panic: nothing is lost. UU means the file has unresolved changes from both sides. Git marks the conflict inside the file:

  • Between <<<<<<< HEAD and ======= is your version (Ben's branch).
  • Between ======= and >>>>>>> origin/main is the version that is already in main (Ana's).

To resolve it, decide what the line should be, talk to your teammate if you are not sure, then edit the file so that only the correct code remains and all three marker lines are gone. Ben confirmed with Ana that the database is now enrollment_db, kept that line, and finished the merge:

$ git add config.php

$ git commit -m "Merge main into feature/reports, keep enrollment_db"
[feature/reports be87212] Merge main into feature/reports, keep enrollment_db

$ git push -u origin feature/reports
To /tmp/capstone-demo/remote.git
 * [new branch]      feature/reports -> feature/reports
branch 'feature/reports' set up to track 'origin/feature/reports'.

$ git log --oneline --graph --all
*   be87212 Merge main into feature/reports, keep enrollment_db
|\  
| *   3f0aa8f Merge feature/login
| |\  
| | * 10e16af Add login page and rename database
| |/  
* / 06b6514 Add reports page and use school_db
|/  
* 6893849 Initial project structure

The graph shows both features meeting cleanly. Ben can now open his pull request, and it will merge into main without conflicts. VS Code makes this even easier: it highlights conflicts and offers buttons such as Accept Current Change, Accept Incoming Change and Accept Both Changes. Always run the project after resolving a conflict, because code that merges without errors can still be wrong.

Daily Routine for Each Member

git switch main
git pull                           # get the latest main
git switch -c feature/my-task      # or: git switch feature/my-task
# ...work, test...
git add .
git commit -m "Describe what you did"
git push -u origin feature/my-task # then open a pull request
git merge origin/main              # after git fetch, when main has moved

Common Mistakes and Quick Fixes

  • "I committed to main by accident." If you have not pushed yet, create a branch from where you are (git switch -c feature/x), push that, then reset your local main to match the remote with git switch main and git reset --hard origin/main. Double-check that your work is on the new branch before resetting.
  • "Push rejected: fetch first." Someone pushed before you. Run git pull, fix any conflicts, then push again. Never use git push --force on a shared branch.
  • Committed a password or .env file. Removing the file in a new commit is not enough because it stays in history. Change the password or API key immediately, then clean up the history.
  • Huge "final" commits. One commit with 80 changed files is impossible to review. Commit each small working step.
  • Database changes that are not in Git. Export your schema to a database/schema.sql file and commit it, so teammates can rebuild the same tables.

Why Advisers and Panels Like This

A clean Git history is proof of teamwork. The commit log shows who built what and when, pull requests show that code was reviewed, and main always has a working version you can demo, even if a feature branch is broken the night before. Many advisers now ask for the repository link, and it is also a ready-made portfolio piece for your first job application.

Conclusion

You do not need advanced Git to work well as a team. Protect main, give every task its own branch, merge through pull requests, and update your branch with main often. When a conflict appears, read the markers, talk to your teammate, keep the right code and test again. Set this up on the first week of your capstone, and the last week will be about polishing your system instead of searching for the latest zip file.