Git records versions. GitHub hosts repositories and provides a place to discuss issues and review code. Knowing how to use both is a basic development skill. Pushing code is only one step. A typical change also raises questions about branches, commits, review, automated checks and release. This post follows one change through that process.

# Organising a repository

# What belongs in the README?

When you open a project for the first time, you want to know what it does, what’s working so far, and how to run it. A README should answer those questions up front, then point you to the docs for more detail.

A useful order is introduction, demo, main features, installation or startup, contributing and licence. For a graphical application, one real screenshot often conveys more than a long feature list. Include the prerequisites and commands someone needs to install dependencies and run it.

In my DungeonTavern README, I start with the project and documentation links, show the actual scene, then cover features and startup. Long design notes can live in separate documents. Relative links to files and screenshots continue to work when readers switch branches or download the source.

DungeonTavern’s introduction, documentation links and scene preview

# Explain usage and contributions

The licence describes permitted uses. A CONTRIBUTING.md can explain environment setup, validation and contribution submission. Link both from the README. Templates make these reports more consistent. An issue template can ask for reproduction steps. A pull request template can ask what changed and how the contributor checked it.

# Exclude files that don’t belong in history

Use .gitignore to exclude files you don’t want under version control. A Node.js project might ignore installed dependencies, build products and local configuration:

node_modules/
dist/
.env
.env.*
!.env.example

Keep only variable names and dummy values in .env.example . Whether to track dist/ depends on distribution: some repositories intentionally version their built output.

Adding an already tracked file to .gitignore won’t untrack it. To remove it from the index and keep the local copy:

git rm --cached .env

Old commits still contain it. Revoke or rotate a secret that was committed.

# Create a branch for the change

Check for unfinished local changes first, then create the working branch from an up-to-date main branch. The example assumes a main branch named main and a remote named origin :

git status
git switch main
git pull --ff-only origin main
git switch -c fix/category-wrap

--ff-only stops if local and remote histories have diverged. That gives you a chance to inspect both sides. Use a name that describes the change, such as fix/category-wrap or feat/search . Keep a release script edit separate from a category layout fix so each can be reviewed, reverted and located in history independently. Short-lived branches suit many small projects; version branches become useful when maintaining multiple release lines.

# Bring in changes from main

Commit your work in progress or stash it. Once the working tree and index are clean, fetch:

git fetch origin

Choose one of these approaches according to the project’s conventions:

# 在当前工作分支中合入主分支
git merge origin/main
# 或者,把当前工作分支的提交重放到新的主分支之后
git rebase origin/main

A merge preserves the existing commits and can join diverged histories with a merge commit. A rebase replays changes on a new base and gives the replayed commits new IDs.

Situation Approach Reason
Others share the branch or work on top of it Prefer a merge Their existing commit IDs remain valid.
The work is entirely local A rebase is reasonable Nobody else depends on the old IDs.
The branch was pushed but only you use it Follow the repository convention Updating the remote after a rebase usually requires a force push. Check who depends on it first.
The target requires linear history Use a compatible rebase, squash or fast-forward A merge commit would violate the rule; a fast-forward creates none.

Coordinate before rewriting shared history. Don’t rewrite a public main branch simply to sync your work. Choosing a PR merge method below concerns a later step.

Commit relationships after merging or rebasing

Resolve conflicts in the files, then mark them resolved with git add . Complete the merge commit or run git rebase --continue as appropriate. To abandon the operation, use git merge --abort or git rebase --abort . Restore stashed changes with git stash pop ; restoring can itself cause conflicts. Run the relevant checks even after a clean textual merge.

# Organise changes into commits

Inspect the changes before committing:

git status
git diff
git add -p
git diff --cached

git diff shows unstaged changes and git diff --cached shows what the next commit will contain. Stage selected hunks with git add -p ; add new files separately after checking status.

Implementation, tests and documentation can belong in one commit when they accomplish one thing. Splitting mechanically by file makes less sense than separating unrelated formatting, debugging files and behaviour changes. Mixing those together makes reverting the functional change much harder.

For a public project, English commit messages can help contributors who speak other languages. Follow existing language conventions. Git itself doesn’t require English. Describe the effect of the change, with context where needed:

Fix wrapping of long category titles on mobile
Allow titles to wrap within the text column instead of moving
below the date.

Once you’ve checked the staged changes, run git commit and enter the message in your editor. A subject like the one above tells you what changed when you browse the history. An entry like update or fix bug leaves you guessing. The Git project’s contribution guidelines also ask contributors to group changes logically and explain why they made them.

Prefixes such as feat: and fix: are a team convention and can help generate release notes. Use them if they suit the project. What matters is that the messages are clear and consistent within the repository.


If you’ve just committed and discover a related change you left out, stage it and run the following while the commit is still unshared:

git commit --amend

Amending changes the commit ID. If others already depend on it, adding a follow-up commit is usually easier than rewriting history just to tidy up the message.

# Open and merge a pull request

Push the working branch and establish its upstream:

git push -u origin fix/category-wrap

Open a PR against the intended base. Explain the problem, the change and how you verified it. Here’s an example description:

## 改动
修复手机端分类长标题换行后挤开日期的问题。
## 验证
- 检查长标题和短标题的排列。
- 检查桌面与窄屏下的分类跳转。
Closes #42

Only list checks actually performed. Verify that Closes #42 points to the issue this change resolves. Use a draft PR to discuss an unfinished approach. Review existing behaviour and edge cases as well as the new code. Push follow-up fixes to the same branch; the PR updates automatically. The VS Code contribution guide asks contributors to keep PRs focused and avoid unrelated formatting, which makes changes easier to review.

# Choose a merge method

Syncing the working branch during development and deciding what history lands on main are distinct decisions. A branch rebased locally can still be squash-merged. A branch kept current with merge commits can also be squashed.

GitHub option Result on the target When it fits
Create a merge commit Keeps the branch commits and adds a merge commit The full branch relationship is worth retaining.
Squash and merge Records the PR as one new commit Many small fixes together form a single change.
Rebase and merge Replays the commits individually, with new IDs and no merge commit The separate commits already tell the intended story.

Follow the project’s convention.

# Clean up after the merge

After confirming the merge, remove the remote working branch if appropriate and update local main. A squash of commits A, B and C produces a new S on main. A merge commit keeps A, B and C and joins them with M.

git branch -d checks ancestry against the tracked upstream, or against the current branch if there’s no upstream. It doesn’t compare resulting content. A squash-merged branch can therefore report not fully merged . Confirm the PR landed and that the working branch has no later unmerged commits. Only then, if needed:

git switch main
git branch -D fix/category-wrap

Capital -D skips the ancestry check. Removing the local branch reference doesn’t undo the squash commit.

# Automating checks and releases with GitHub Actions

Running a PR’s build and tests automatically gives reviewers a result and a log for every update.

# CI/CD

Continuous integration means integrating code frequently and running automated builds and tests to catch problems early. For example, a project can build and run its tests on each PR. Continuous delivery also prepares a releasable package, with release potentially left to a person. Continuous deployment sends passing changes to production without that final approval.

These are workflows made up of concrete operations such as testing, building, uploading and deploying. GitHub Actions can orchestrate them, and can also perform unrelated repository tasks such as applying labels.

# Add a check workflow

Store workflows under .github/workflows/ . Events such as pushes, PR changes and manual dispatch trigger workflows. Each workflow contains one or more jobs. Each job runs on a runner, and its steps run commands or invoke actions in order. Use needs to express dependencies between jobs.

For this example the project uses Node.js 24, has a committed package-lock.json , and defines CI-compatible test and build scripts in package.json . Create .github/workflows/ci.yml :

name: CI
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  workflow_dispatch:
permissions:
  contents: read
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: npm
      - run: npm ci
      - run: npm test
      - run: npm run build

This workflow only checks the application. It runs on PR updates and checks main after a merge. For manual dispatch the workflow must already exist on the default branch. npm ci installs from the lockfile. If there’s no test script, substitute the project’s actual check command. When strict action pinning is required, use a verified full commit SHA.

In Actions, open a run and find the first failing step. Its context distinguishes dependency installation, test and build failures. This actual OneDrop CI run includes formatting, TypeScript compilation, tests and two build targets:

Steps in a successful OneDrop CI run

# Caches, artifacts and releases

cache: npm reuses npm’s package cache, not node_modules ; the workflow still runs npm ci . Artifacts preserve results such as installers or test reports for later jobs or downloads.

A separate release workflow might trigger from a tag, a published release or manual dispatch. Deployment environment protections can require approval where the repository and plan support them. The check example needs only contents: read . Grant additional permissions only to jobs that need them. Put tokens in Secrets. External fork PRs have restrictions on tokens and secrets: avoid executing untrusted PR code in a privileged pull_request_target context.

# Protect main

GitHub offers classic branch protections and rulesets. Multiple rulesets can apply alongside existing protections. The table uses the English labels from Rulesets. The classic branch protection page groups and names its options slightly differently.

Rule Purpose
Require a pull request before merging Route changes through a PR.
Require status checks to pass Require named checks such as CI.
Required approvals Set the approval count within the PR rule.
Block force pushes Protect existing history.
Restrict deletions Protect the branch from deletion.

Require linear history rules out merge commits on the target. Select compatible squash or rebase merges. See GitHub’s ruleset reference.

Run a workflow at least once before selecting its check name. Path filters or conditional jobs can otherwise leave a PR waiting for a check that never appears. For a solo project, avoid requiring approvals nobody can give; larger projects can add reviewers or code owners. Also check plan availability and who can bypass a rule.

# Contributing through a fork

With write access, push a working branch to the project. Otherwise fork the project on GitHub, clone your fork locally and propose changes upstream. Forking creates a hosting relationship; cloning downloads a repository.

After cloning your fork, origin generally points to it. Add a remote for the original project, commonly called upstream . These names are conventions. Replace OWNER and REPOSITORY with the original project’s owner and name. If upstream already exists, verify its URL instead of adding it again.

git remote -v
git remote add upstream https://github.com/OWNER/REPOSITORY.git

Here main follows upstream and personal changes stay on working branches. Handle uncommitted work first, then sync:

git fetch origin
git fetch upstream
git switch main
git merge --ff-only origin/main
git merge --ff-only upstream/main
git push origin main
git switch -c fix/category-wrap

Both fast-forward checks can stop on divergence between local main, your fork and the original repository. Inspect the unique commits and decide what to keep before merging or rebasing. A force push would rewrite history without answering that question.

Create a working branch, make the change, push to your fork’s origin and open the PR against the branch requested by the project.

Local checkout, origin and upstream in a fork workflow

Read the contribution guide for the target branch, tests and whether an issue discussion is expected first.

# Managing large files with Git LFS

Images, models and audio sources may need versioning alongside code. Git LFS stores small pointers in Git and keeps the contents in LFS storage. GitHub rejects ordinary Git files over 100 MiB. Consider LFS for required versioned binaries, ignore build caches and put downloadable release installers in Releases.

Once you’ve installed Git LFS, initialise it in the repository and add a tracking rule. Here’s an example for PSD files that ordinary Git hasn’t tracked yet:

git lfs install
git lfs track "*.psd"
git add .gitattributes
git add assets/example.psd
git commit -m "Track PSD assets with Git LFS"
git lfs ls-files

git lfs track writes the rule to .gitattributes . Commit that file so collaborators use the same rules. Check git lfs ls-files to confirm the files are managed by LFS. Collaborators need Git LFS too. If a checkout contains only pointer text, run git lfs pull .

Adding a tracking rule doesn’t shrink existing history. Migrating old binaries rewrites commits: back up, define the scope and coordinate with collaborators before doing so. Check the account’s storage and download allowances as well.

# Tagging a version

After confirming the exact release commit, create and push an annotated tag:

git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

A GitHub Release builds on a tag with release notes and downloadable assets. Document visible changes, fixes and upgrade requirements. Attach built installers separately; an automatically generated source archive isn’t the compiled application.

# Undoing changes

Run git status and git diff first to see which changes you want to keep.

Situation Command Effect
Reselect staged changes git restore --staged path/to/file Keeps working-tree edits.
Discard unwanted unstaged changes to a tracked file git restore path/to/file Restores the index version and discards those edits.
Reorganise the most recent unshared commit git reset --soft HEAD~1 Removes the commit from the branch tip while retaining staged changes.
Undo an ordinary commit on a shared branch git revert COMMIT Adds an inverse commit and preserves history.

The default mixed reset also resets the index but keeps working-tree changes. A hard reset overwrites affected working-tree changes too. Reverts can conflict; reverting a merge requires deciding which parent is the mainline. Fixes and reversions on public branches can go through the same branch, PR and check process.