1Version Control with GitHub
▶
Every software project — from a one-file script to a system with millions of lines of code — accumulates change. Files get edited, deleted, renamed, and restored. Features are added, then abandoned. Bugs are introduced by a "small tweak" that nobody remembers making. Without a disciplined way to record those changes, a team quickly loses the ability to answer the three questions that matter most: What changed? Who changed it? Why? Version control answers all three automatically, and GitHub turns that answer into a shared, cloud-hosted workspace where an entire team can plan, build, review, and ship together. This topic walks through version control from first principles and then builds up the practical GitHub workflow you will use for the course project.
What Is Version Control?
A version control system (VCS) is a tool that records snapshots of a project over time and lets you move between those snapshots at will. Git — the engine underneath GitHub — is a distributed VCS: every contributor holds a complete copy of the project's entire history on their own machine, not just the latest files. That design makes most operations instant (they happen locally) and makes the history remarkably resilient, since dozens of full backups exist by default.
A complete history of every change. The atomic unit of Git history is the commit: a snapshot of the project plus metadata (author, timestamp, parent commit, and a message explaining the change). Each commit is identified by a SHA-1 hash such as a3f91c7, which is derived from the content itself, so history cannot be silently altered. Because the chain of commits is preserved, you can inspect exactly how a file evolved:
$ git log --oneline -5
a3f91c7 Fix off-by-one error in pagination
7d2e04b Add unit tests for pagination helper
1c8b55a Refactor query builder into its own module
9f0ad31 Add pagination to results page
4e77b12 Initial project scaffold
$ git log -p src/pagination.js # see every change ever made to one file
$ git blame src/pagination.js # see which commit and author touched each line
$ git diff 9f0ad31 a3f91c7 # compare any two points in history
This history is not bookkeeping for its own sake — it is a debugging tool. When a bug appears, git bisect can binary-search the commit history to find the exact commit that introduced it, often narrowing hundreds of commits down to the culprit in eight or nine steps.
Multiple contributors without overwriting each other. Before version control, teams passed around folders named project_final, project_final_v2, project_final_REAL_v2. Two people editing the same file meant one person's work was lost. Git solves this by tracking changes at the line level and merging them. If Amara edits the header of index.html while Ben edits the footer, Git combines both edits automatically. Only when two people change the same lines does Git stop and declare a merge conflict, marking the file so a human can decide:
<<<<<<< HEAD
<h1>Campus Event Finder</h1>
=======
<h1>Campus Events</h1>
>>>>>>> feature/rename-title
You edit the file to the desired final state, remove the marker lines, then git add and commit. Conflicts feel intimidating at first but are routine; the important insight is that Git never silently discards anyone's work.
Restoring any version at any point. Because every commit is a full snapshot, "undo" is always available and always safe:
| Goal | Command | Effect |
|---|---|---|
| Discard uncommitted edits to a file | git restore src/app.js | File returns to its last committed state |
| View the project as it was last Tuesday | git checkout a3f91c7 | Working directory matches that commit (detached HEAD) |
| Recover a single deleted file | git checkout 9f0ad31 -- docs/spec.md | Pulls one file back from history |
| Undo a published commit safely | git revert a3f91c7 | Creates a new commit that inverts the old one |
| Rewind local history (not yet shared) | git reset --hard 7d2e04b | Moves the branch pointer back; destructive |
The distinction between revert and reset matters on a team: revert preserves history and is safe for shared branches; reset --hard rewrites history and should be reserved for commits you have not pushed.
GitHub as a Collaboration Platform
Git manages history; GitHub manages people working on that history together. It is a hosting service built around Git repositories, layered with review, automation, and planning tools.
Cloud-hosted repositories. A GitHub repository is a remote — a canonical copy of the project that every contributor pushes to and pulls from. Practically, this means your work is backed up off your laptop, accessible from the lab machine, a teammate's computer, or a browser-based editor (press . on any repo page to open it in a web VS Code instance). The basic rhythm is:
$ git clone https://github.com/team-orion/event-finder.git
$ cd event-finder
# ...edit files...
$ git add src/calendar.js
$ git commit -m "Add month-view rendering for the calendar widget"
$ git pull --rebase origin main # bring in teammates' work first
$ git push origin main
A repository can be public (visible to anyone, the basis of open source) or private (visible only to invited collaborators). Public repos double as a portfolio: employers routinely read candidates' commit histories and READMEs.
Issues, Projects, and Discussions. GitHub deliberately keeps planning next to the code rather than in a separate tool. Issues are tickets for bugs, features, and chores. Projects are kanban boards and tables built from those issues. Discussions are threaded forums for open-ended questions, design debates, and announcements that would clutter an issue tracker ("Should we switch the database to Postgres?" belongs in Discussions; "Login button is misaligned on Safari" belongs in Issues). Because all three live beside the code, an issue can reference a commit, a commit can close an issue, and a pull request can appear automatically on the project board.
GitHub Actions. Actions is GitHub's built-in automation engine. You define workflows as YAML files in .github/workflows/, and GitHub runs them on hosted virtual machines in response to events such as a push, a pull request, a schedule, or a manual trigger. The classic use is continuous integration: run the test suite on every pull request so broken code is caught before review.
name: CI
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
Each pull request then displays a green check or red X. Teams commonly make these checks required, so a PR physically cannot be merged while tests fail. Beyond testing, Actions handles deployment (publishing a site to GitHub Pages, pushing a container image, releasing a package), scheduled maintenance (nightly data refreshes), and housekeeping (auto-labelling PRs, greeting first-time contributors).
GitHub Copilot. Copilot is an AI pair programmer that integrates into editors such as VS Code, JetBrains IDEs, and the GitHub web interface. It suggests completions ranging from a single line to whole functions based on your current file, surrounding code, and comments. Writing a descriptive comment or function signature is the most reliable way to steer it:
// Return events that start within the next `days` days, sorted soonest first.
function upcomingEvents(events, days) {
// Copilot proposes an implementation here; press Tab to accept.
}
Copilot Chat extends this to conversational use — explaining unfamiliar code, drafting unit tests, proposing fixes for a failing build, or writing a commit message. Treat suggestions as drafts, not truth: they can be subtly wrong, use outdated APIs, or invent functions that do not exist. Always read, test, and understand the code before committing it, and check your course or employer's policy on AI assistance.
Creating and Configuring a Repository
Creating a repository takes thirty seconds; configuring it well is what makes a project pleasant to join six months later.
What a repository stores. A repo holds the working files, the entire commit history, branches and tags, and collaboration metadata — issues, pull requests, releases, wiki pages, and settings. All of the Git data lives in a hidden .git directory at the project root; deleting it would leave the files but erase the history. You can create a repo from the GitHub UI (and clone it), or start locally and connect it afterwards:
$ git init
$ git add .
$ git commit -m "Initial commit"
$ git branch -M main
$ git remote add origin https://github.com/team-orion/event-finder.git
$ git push -u origin main
The README. README.md is rendered automatically on the repository's front page and is the single highest-leverage file in the project. A strong README answers, in order: what this project is, why it exists, how to run it, and how to contribute. A workable skeleton:
# Campus Event Finder
A web app that aggregates student-club events into a single searchable calendar.
## Features
- Browse events by club, date, or tag
- Save events to a personal watchlist
- ICS export for Google/Apple Calendar
## Getting Started
```bash
git clone https://github.com/team-orion/event-finder.git
cd event-finder
npm install
cp .env.example .env # then fill in your API keys
npm run dev # serves at http://localhost:3000
```
## Project Structure
- `src/` — application code
- `tests/` — Jest test suite
- `docs/` — design notes and API reference
## Contributing
Branch from `main` using `feature/`, open a pull request,
and request review from at least one teammate. All PRs must pass CI.
## License
MIT
Larger projects split the contributor guidance into CONTRIBUTING.md and add CODE_OF_CONDUCT.md and LICENSE; GitHub surfaces all of these in the interface automatically.
The .gitignore file. Not everything in your working folder belongs in version control. Build artefacts bloat the repo, machine-specific files cause pointless conflicts, and credentials in a public repo are a genuine security incident — automated bots scan GitHub for leaked keys within minutes of a push. .gitignore lists patterns Git should not track:
# Dependencies
node_modules/
venv/
# Build output
dist/
build/
*.pyc
__pycache__/
# Environment and secrets
.env
.env.local
*.pem
credentials.json
# Editor and OS noise
.vscode/
.idea/
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
Two important caveats. First, .gitignore only prevents untracked files from being added; if a file is already tracked, you must run git rm --cached .env to stop tracking it. Second, if a secret has already been pushed, removing it in a later commit is not enough — it remains in history. The correct response is to rotate the credential immediately and then, if necessary, purge it from history with a tool such as git filter-repo. The safe pattern is to commit a placeholder .env.example with blank values and keep the real .env ignored.
Access permissions. GitHub controls who can do what through repository roles. For a personal repo you invite collaborators directly; in an organization you typically grant access to teams.
| Role | Typical capabilities | Who gets it |
|---|---|---|
| Read | Clone, pull, open issues, comment, review | Stakeholders, instructors, observers |
| Triage | Read, plus manage issues and PRs (label, assign, close) | Community moderators, QA helpers |
| Write | Triage, plus push branches and merge approved PRs | Core team members |
| Maintain | Write, plus most repository settings (not destructive ones) | Team leads |
| Admin | Full control: permissions, branch protection, delete, transfer | Project owner(s) |
Apply the principle of least privilege: give each person the lowest role that lets them do their job. External contributors do not need write access at all — they fork the repository (make their own copy) and submit pull requests from the fork, which the maintainers review before merging. Admins should also enable two-factor authentication and, for the main branch, configure branch protection rules (described below).
Branching and Pull Requests
A branch is simply a movable pointer to a commit. Creating one is instantaneous and costs essentially nothing, which is why branching is the backbone of team workflow rather than an advanced technique.
Keeping main stable. The main branch is the project's source of truth: at any moment, the code on main should build, pass tests, and be safe to demo or deploy. Nobody commits half-finished work directly to it. Mature teams enforce this with branch protection rules on main:
- Require a pull request before merging (direct pushes blocked).
- Require at least one approving review, and dismiss stale approvals when new commits arrive.
- Require status checks (the CI workflow) to pass.
- Require branches to be up to date with
mainbefore merging. - Restrict who can force-push or delete the branch.
Feature branches. Each unit of work — a feature, a bug fix, a documentation update — gets its own short-lived branch cut from main. Naming conventions make the branch list self-documenting: feature/ics-export, fix/142-safari-login, docs/setup-instructions, chore/upgrade-eslint.
$ git switch main
$ git pull origin main # start from the latest code
$ git switch -c feature/ics-export # create and switch in one step
# ...work, committing in small logical steps...
$ git add src/export/ics.js tests/ics.test.js
$ git commit -m "Add ICS export for saved events
Generates RFC 5545 output and streams it as a download.
Closes #87."
$ git push -u origin feature/ics-export
Keep branches small and short-lived — ideally merged within a few days. A branch that lives for three weeks drifts far from main and produces a painful conflict-ridden merge. If main moves on while you work, integrate regularly with git pull --rebase origin main (or git merge main) so conflicts are resolved in small doses.
Pull requests. A pull request (PR) proposes merging your branch into another and opens a structured conversation around the change. It shows the full diff, the commit list, CI results, and a comment thread where reviewers can leave line-level remarks. A good PR description tells the reviewer what they need to know:
## What
Adds an "Export to Calendar" button that downloads saved events as an .ics file.
## Why
Closes #87 — users asked for a way to get events into Google Calendar.
## How
- New `src/export/ics.js` builds RFC 5545 VEVENT blocks
- Route `GET /api/export` streams the file with the correct MIME type
- 12 unit tests covering timezones, all-day events, and empty watchlists
## Testing
`npm test` passes locally; manually verified import into Google Calendar and Apple Calendar.
## Screenshots
(attached)
Reviewers can Comment (feedback without a verdict), Approve, or Request changes. GitHub also supports suggested changes — a reviewer writes the corrected code in a comment and the author applies it with one click. Useful review etiquette: review promptly (a stalled PR blocks a teammate), critique the code rather than the person, distinguish blocking issues from preferences ("nit:" is a common prefix for the latter), and as the author, respond to every comment even if just to say you've addressed it. Draft pull requests let you open a PR early for visibility while signalling it is not ready to merge.
Merging and a clean history. Once approved and green, the branch is merged. GitHub offers three strategies:
| Strategy | What it does | When to use it |
|---|---|---|
| Merge commit | Keeps every branch commit and adds a merge commit with two parents | When the individual commits are meaningful and you want the full branch structure recorded |
| Squash and merge | Collapses all branch commits into one commit on main | Most team workflows — gives main one tidy commit per feature and hides "fix typo" noise |
| Rebase and merge | Replays branch commits on top of main with no merge commit | Teams that want a strictly linear history and write clean commits |
After merging, delete the feature branch (GitHub offers a one-click button) to keep the branch list meaningful; the commits remain in history. Then everyone runs git switch main && git pull before starting their next branch. Used consistently, this loop — branch, commit, push, PR, review, merge, delete — gives you a main branch whose history reads like a changelog of completed features.
GitHub for Project Management
Because planning artefacts live in the same place as the code, GitHub can link intent to implementation automatically.
Issues as task tickets. An issue is a unit of work with a title, a markdown body, and rich metadata:
- Assignees — who is responsible. One assignee is clearer than three.
- Labels — categorisation such as
bug,enhancement,documentation,good first issue,priority:high,blocked. - Milestone — which release or sprint it belongs to.
- Linked pull requests — the code that implements it.
- Task lists — checkboxes inside the body that render as a progress bar.
A well-written bug issue is reproducible by someone who has never seen it:
Title: Login button unresponsive on Safari 17
**Steps to reproduce**
1. Open https://events.example.edu in Safari 17.2 (macOS 14)
2. Click "Log in with university SSO"
**Expected:** redirect to the SSO provider
**Actual:** nothing happens; console shows
`TypeError: undefined is not an object (evaluating 'e.target.closest')`
**Environment:** Safari 17.2 / macOS 14.3 — works in Chrome 121 and Firefox 122
**Severity:** blocks all Safari users from signing in
- [ ] Reproduce locally
- [ ] Add polyfill or fallback
- [ ] Add a regression test
Issues and code are linked through keywords in commit messages or PR descriptions. Writing Closes #142, Fixes #142, or Resolves #142 in a PR causes GitHub to close issue 142 automatically the moment the PR merges, and to display a permanent cross-reference in both directions. Mentioning #142 without a keyword creates the link without closing. You can also type @username to pull someone into the conversation, and use issue templates (stored in .github/ISSUE_TEMPLATE/) so every bug report arrives with the same required fields.
Project boards. GitHub Projects turns a pile of issues into a visual workflow. The classic view is a kanban board with columns representing states:
| Column | Meaning | Exit criteria |
|---|---|---|
| Backlog | Captured but not scheduled | Prioritised into the current sprint |
| To Do | Committed for this sprint, not started | Someone assigns themselves and starts a branch |
| In Progress | Actively being worked on | Branch pushed and pull request opened |
| In Review | PR open, awaiting approval and CI | Approved, checks green, merged |
| Done | Merged into main and verified | Shipped in a release |
Modern GitHub Projects are more than sticky notes: the same set of items can be viewed as a board, a spreadsheet-style table, or a timeline/roadmap, and you can add custom fields such as Estimate (number), Sprint (iteration), Area (single select), or Target date. Built-in automations keep the board honest without manual dragging — common rules include "when an issue is opened, add it to the board in To Do", "when a linked PR is opened, move the item to In Review", and "when the issue is closed, move it to Done". Group by assignee to spot an overloaded teammate, filter by label:bug is:open to run a defect triage, or sort the table by priority before a planning meeting.
Two practices make boards genuinely useful rather than decorative. First, limit work in progress: if every member has four cards in In Progress, nothing is finishing and reviews are piling up. A rule of thumb is one or two in-flight items per person. Second, update the board as part of the work, not as a weekly chore — if the board does not reflect reality, people stop trusting it and revert to asking in chat.
Milestones. A milestone is a named, optionally dated bucket of issues and pull requests that represents a deliverable: v1.0 — Public Beta, Sprint 3, Final Demo. GitHub shows a progress bar (closed versus total items) and the number of days remaining, giving an at-a-glance answer to "are we going to make it?"
| Milestone | Due | Issues closed / total | Status |
|---|---|---|---|
| v0.1 — Walking Skeleton | Week 4 | 14 / 14 | Complete |
| v0.5 — Core Features | Week 8 | 19 / 26 | 73% — on track |
| v1.0 — Public Beta | Week 12 | 3 / 21 | 14% — at risk, needs scope cut |
When a milestone is clearly at risk, the healthy response is to move lower-priority issues out of it rather than to let the date slip silently; the milestone then stays an honest signal. Milestones pair naturally with releases and tags: once every issue in v1.0 is closed, you tag the merge commit (git tag -a v1.0.0 -m "Public beta"), push the tag, and publish a GitHub Release whose notes GitHub can auto-generate from the merged pull requests.
Putting it together. A single day on a well-run project looks like this: you pick the top card in To Do (issue #87, labelled enhancement, milestone v0.5), assign it to yourself, and create feature/ics-export from an up-to-date main. You commit in small steps, push, and open a pull request whose description ends with Closes #87 — which moves the card to In Review automatically and kicks off the CI workflow. A teammate reviews, leaves two suggested changes, you accept them, checks turn green, and they approve. You squash-and-merge, GitHub closes issue #87, moves the card to Done, bumps the v0.5 milestone progress bar, and deletes the branch. The history on main gains one clean, tested, reviewed commit that is permanently traceable back to the conversation that produced it. That traceability — code, reason, review, and plan all linked — is the real payoff of using version control and GitHub together.