Best Practices for Naming Git Branches in Scientific Projects

By Dr. Zubair Khalid, DVM, MS, PhD ·

Best Practices for Naming Git Branches in Scientific Projects

By the end of this guide you will be able to write a branch naming policy for your lab, validate any candidate name against Git's actual rules before you use it, create, rename, and delete branches with the correct commands, and decide what belongs in a branch name versus a tag or a pull request description. To follow along you need Git 2.23 or newer (check with git, version), a terminal, a GitHub account, and a scratch repository to practice in. Every command shown here behaves the same on macOS, Windows, and Linux.

Version control has become a publishing requirement rather than an optional skill. Major surgical journals now request code availability statements, yet most surgical trainees and faculty receive no formal training in software version control [1]. Git for scientists is therefore a job skill, and a branch name is the first thing a collaborator, a reviewer, or your own future self reads when opening a repository. Vague names such as final2 or test-fix turn every visit to the repository into archaeology. Precise names turn the branch list into a readable record of what the lab is doing.

Why Branch Names Carry Weight in Research Code

Git rests on four mental models, which a primer written for surgical data science describes as the repository, the commit, the branch, and the remote [1]. Each term has a precise meaning:

  • A repository is the full history of a project, stored in a hidden .git directory inside your working folder.
  • A commit is a frozen snapshot of the tracked files at one moment, with an author, a timestamp, and a unique hash such as 3f2a9c1.
  • A branch is a movable label that points at one commit. Creating a branch copies nothing. It writes a 41-byte file containing a hash.
  • A remote is another copy of the repository, usually on GitHub, that you push to and pull from.

Because a branch is only a label, the name you choose is pure communication. It costs nothing to store and it is read constantly. In multi-institutional clinical studies, where analysts at several centers push to the same repository, the branch list is often the only shared view of who is doing what [1]. In community-maintained research software, outsiders read the branch and pull request lists to decide whether a project is active and how to contribute. EasyAmplicon, an open-source amplicon analysis pipeline, is maintained and updated by its authors with user help provided through GitHub, which means its public-facing repository metadata serves as part of its documentation [2].

Bad names have a measurable cost. When every long-running branch is called dev, wip, or a person's first name, collaborators must open each branch and read commits to learn its purpose. Good names let you answer three questions from the branch list alone: what kind of work is this, what topic does it touch, and is it still active.

The Rules Git Enforces on Every Branch Name

Git stores branches under an internal prefix, refs/heads/. You never type that prefix, but the rules that apply to refs apply to everything after it. The following restrictions come from Git's official reference format documentation and apply to every branch name:

  1. No component of the name may begin with a dot. feature/.draft is invalid.
  2. The name may not contain two consecutive dots. analysis..v2 is invalid.
  3. The name may not contain an ASCII control character, a space, a tilde (~), a caret (^), a colon (:), a question mark (?), an asterisk (*), an opening bracket ([), or a backslash (\).
  4. The name may not begin or end with a slash, and it may not contain two slashes in a row.
  5. The name may not end with a period or with .lock.
  6. The name may not contain the sequence @{ and may not be the single character @.

Two practical consequences follow. First, slashes are allowed and are the standard way to create a hierarchy, so analysis/ibd-pilot-16s is legal while analysis//ibd is not. Second, a branch name that collides with an existing branch prefix cannot be created. If feature/qc exists, Git refuses to create a branch called feature, because a ref cannot be both a file and a directory. The attempt fails with a lock error:

$ git switch -c feature
fatal: cannot lock ref 'refs/heads/feature': 'refs/heads/feature/qc' exists

Case sensitivity is the other trap. Git itself treats Feature/QC and feature/qc as different branches, but the filesystems on Windows and macOS default to case-insensitive storage, so the two names collide on those machines. A collaborator on Linux can create both, and everyone on a Mac or a Windows laptop then sees corruption. The rule is simple: never distinguish two branches by capitalization alone.

You can validate any name before using it with git check-ref-format, which is Git's own validator:

$ git check-ref-format --branch "analysis/IBD pilot"
fatal: 'analysis/IBD pilot' is not a valid branch name
$ git check-ref-format --branch "analysis/ibd-pilot-16s"
analysis/ibd-pilot-16s

The first command fails because of the space. The second prints the name and exits cleanly, which is what you want to see before scripting a branch creation step.

Choosing a Git Branch Naming Convention for Research Work

A convention that survives contact with a real lab has three properties. It is short, it is predictable, and it sorts well. The format below satisfies all three and works for everything from a single-student R project to a multi-center clinical pipeline:

<type>/<topic>[-<issue-number>][-<date-or-version>]

Each component has rules:

ComponentRequiredRulesExample
typeYesOne lowercase word from the lab's fixed listanalysis
topicYesTwo to four words, lowercase, hyphens between words, no identifiers of people or specimensibd-pilot-16s
issue numberOptionalTracker number as a prefix on the topic142-rnaqc
dateOptionalISO 8601 format, YYYY-MM-DD, which sorts chronologically as text2026-03-14
versionRelease branches onlyv followed by semantic versioning, vMAJOR.MINOR.PATCHv1.2.0

The type prefix is the part worth standardizing, because it lets everyone filter the branch list mentally. This table is a template you can copy into your lab's contributing guide and adjust:

PrefixUse it forExample
feature/A new capability in a pipeline or scriptfeature/qc-trimmer-wrapper
fix/A bug fix that is not urgentfix/alpha-diversity-sort
hotfix/An urgent fix to a released versionhotfix/barcode-off-by-one
analysis/One defined analysis, usually tied to a paper questionanalysis/ibd-pilot-16s
experiment/Exploratory runs you may discardexperiment/minimap2-k-15
manuscript/Writing, figures, and revision textmanuscript/gut-paper-r1
reviewer-response/Point-by-point response workreviewer-response/r2-stats
data/Preparing a data release or codebookdata/ercc-spikein-v2
release/Stabilizing a version before releaserelease/v1.0.0
docs/Documentation onlydocs/install-steps
initials/Personal scratch space on a shared remoteab/plot-theme-test

Two conventions deserve a note. Dates should use ISO 8601 (2026-03-14), because that format sorts correctly as plain text, while 3-14-26 and 14/03/2026 do not. Issue numbers, when your lab uses GitHub issues, belong in the branch name for human readers. GitHub closes issues when the pull request description contains a phrase such as fixes #42, not because of the branch name, so treat the number in the name as a signpost for people rather than automation.

Finally, decide what never goes into a name. Do not encode full parameter sets (analysis/k15-evalue-1e5-minlen50 ages badly), machine names, absolute paths, or anything that identifies a patient. The next section on data safety returns to that last point.

Worked Example: Naming Branches in an Amplicon Sequencing Project

The scenario is a common one. A lab runs 16S rRNA gene amplicon analysis in R through an integrated pipeline of the EasyAmplicon style, which bundles more than 30 cross-platform modules and R packages for steps from quality control through biomarker discovery and visualization [2]. The analysis lives in a Git repository with a GitHub remote, and three people work in it. The remote in the examples below is named origin (the default name Git assigns to the GitHub copy) and the repository is lab/gut-microbiome.

Step 1: Look before you branch

Run these from the repository root:

$ git branch
* main
$ git branch --show-current
main

git branch lists local branches and marks the current one with an asterisk. git branch, show-current prints only the current branch name, which is what you want inside scripts. Add -a to include remote-tracking branches such as origin/main, which are your local read-only mirrors of branches on GitHub.

Step 2: Create the analysis branch

$ git switch -c analysis/ibd-pilot-16s
Switched to a new branch 'analysis/ibd-pilot-16s'

git switch has handled branch selection since Git 2.23. The -c flag means create, and it moves HEAD (the pointer to your current position in history) to the new label. The older command git checkout -b analysis/ibd-pilot-16s does the same thing and appears in older tutorials. If the branch already exists, Git refuses:

$ git switch -c analysis/ibd-pilot-16s
fatal: a branch named 'analysis/ibd-pilot-16s' already exists

That message means the name is taken, not that anything is broken. Switch to the existing branch with git switch analysis/ibd-pilot-16s if that is what you intended.

Step 3: Validate the name before scripting it

If a script creates branches automatically, validate first:

$ git check-ref-format --branch "$BRANCH_NAME" || exit 1

The validator prints the name and exits with status 0 when the name is legal, and prints fatal: '<name>' is not a valid branch name with a nonzero status when it is not. This one line prevents the most common pipeline failure, which is a generated name containing a space or a slash in the wrong place.

Step 4: Push the branch and set its upstream

A branch that exists only on your laptop cannot be reviewed. Push it and bind it to its remote counterpart in one command:

$ git push -u origin analysis/ibd-pilot-16s
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 402 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Create a pull request for 'analysis/ibd-pilot-16s' on GitHub by visiting:
remote:      https://github.com/lab/gut-microbiome/pull/new/analysis/ibd-pilot-16s
To github.com:lab/gut-microbiome.git
 * [new branch]      analysis/ibd-pilot-16s -> analysis/ibd-pilot-16s
branch 'analysis/ibd-pilot-16s' set up to track 'origin/analysis/ibd-pilot-16s'.

The -u flag (short for -set-upstream) records the pairing between your local branch and origin/analysis/ibd-pilot-16s, so later git push and git pull need no arguments. GitHub even prints the URL at which you can open the pull request.

Step 5: Rename a branch correctly

Suppose the pilot gains a second dataset and the name needs to change. Git has no single rename-remote-branch command, so a rename is three operations:

$ git branch -m analysis/ibd-pilot analysis/ibd-pilot-16s
$ git push origin -d analysis/ibd-pilot
To github.com:lab/gut-microbiome.git
 - [deleted]         analysis/ibd-pilot
$ git push -u origin analysis/ibd-pilot-16s

-m renames the local branch, -d (short for -delete) removes the old name on the remote, and the final push publishes the new name with its upstream. Warn collaborators before renaming a shared branch. Their local clones keep tracking the old remote name until they run git fetch -p (Step 8), and an open pull request keeps pointing at the old name until it is retargeted on GitHub.

Two errors appear often here. Pushing a deletion for a branch that no longer exists remotely gives:

$ git push origin -d analysis/ibd-pilot
error: unable to delete 'analysis/ibd-pilot': remote ref does not exist
error: failed to push some refs to 'github.com:lab/gut-microbiome.git'

The fix is to run git fetch -p so your view of the remote catches up, then continue. And if you try to create a name that is already taken locally, the fatal: a branch named ... already exists message from Step 2 is the cause.

Step 6: Find and delete finished branches

After pull requests merge, the local and remote lists fill with finished work. List the branches already merged into main:

$ git branch --merged main
  analysis/old-pilot
* main

Every branch in that list can be deleted without losing commits, because its changes are already part of main:

$ git branch -d analysis/old-pilot
Deleted branch analysis/old-pilot (was 3f2a9c1).

Git refuses -d on a branch with unmerged commits, which is a safety feature. -D forces the deletion, and it is the flag to reach for only when you consciously want to discard an abandoned experiment.

Step 7: Prune stale remote-tracking names

When someone else deletes a branch on GitHub, your local mirror of it lingers. Prune it:

$ git fetch -p
From github.com:lab/gut-microbiome
 - [deleted]         (none)     -> origin/analysis/ibd-pilot

The -p flag (short for -prune) removes remote-tracking refs whose branches no longer exist on the remote. Running git fetch -p once a week keeps every branch list honest.

Step 8: Verify the result

Two commands confirm that local and remote agree:

$ git ls-remote --heads origin
3f2a9c1e4b7d2f8a0c6e1d9b5a3f7c2e8d4b6a90	refs/heads/analysis/ibd-pilot-16s
5c7d0e9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d	refs/heads/main
$ git branch -vv
* analysis/ibd-pilot-16s 3f2a9c1 [origin/analysis/ibd-pilot-16s] Add alpha diversity plots
  main                   5c7d0e9 [origin/main] Merge pull request #12

git ls-remote queries GitHub directly and shows what actually exists there. git branch -vv shows each local branch, its tracking partner, and whether it is ahead or behind. If a branch appears in one list and not the other, Step 5 or Step 7 is the remedy.

Step 9: Record the branch in your analysis provenance

Because a branch label moves, tie each analysis run to both the branch and the exact commit:

$ git branch --show-current
analysis/ibd-pilot-16s
$ git rev-parse --short HEAD
3f2a9c1

Write both values into the run log or a run-info.txt file whenever a pipeline executes. When a reviewer asks which code produced a figure, the pair analysis/ibd-pilot-16s at 3f2a9c1 answers the question in seconds. This habit pairs naturally with the edit-stage-commit cycle that dominates daily Git work [1], and it costs one line per run.

Branches Versus Tags for Versions You Cite

A branch name is a moving target. The label analysis/ibd-pilot-16s points at a different commit every time someone pushes. A tag is the opposite: a permanent name for one specific commit. When your pipeline reaches a state worth citing, freeze it:

$ git tag -a v1.0.0 -m "Amplicon pipeline, first internal release"
$ git push origin v1.0.0
To github.com:lab/gut-microbiome.git
 * [new tag]         v1.0.0 -> v1.0.0

The -a flag creates an annotated tag, which stores a message, an author, and a date, and which is the right choice for releases. The version string follows semantic versioning: increment the major number for breaking changes, the minor number for new features, and the patch number for bug fixes.

Tags behave differently from branches when you check them out. Because a tag points at a bare commit rather than a branch, switching to one detaches HEAD:

$ git switch v1.0.0
Note: switching to 'v1.0.0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

Detached HEAD means you are no longer on any branch, so new commits are stranded if you switch away. If you need to change code at a tagged version, create a branch at that point first with git switch -c hotfix/v1.0.0-fix.

The division of labor is then clean. Branches carry work in progress and are deleted when merged. Tags carry citable versions and are never deleted. Community research software shows why this matters at scale. EasyAmplicon is updated regularly by its maintainers while serving many labs at once, which only works because users can pin their analyses to stable released states rather than to a moving development branch [2]. When you publish, cite the commit hash or the tag, and archive the release commit through a DOI-issuing service so the reference survives even if the branch names change.

Keeping Patient Identifiers Out of Branch Names

Clinical research code carries an obligation that ordinary software does not. The primer on Git for surgical data science states the rule plainly: identifiable patient data must never enter a version-controlled repository, and a .gitignore file (a list of filename patterns Git is told to ignore) prevents inadvertent commits of such files [1].

Branch names deserve the same discipline, for a mechanical reason. A branch name is not private. The moment you push, it is copied to GitHub and then to every collaborator's clone, and it appears in pull request URLs, commit pages, and continuous integration logs. A name such as analysis/patient-MRN-4821 or fix/subject-jdoe-2026-03 therefore distributes an identifier to every place the repository travels, including backups and mirrors you do not control.

Extend the rule to the whole naming surface. Branch names, commit messages, pull request titles, and release notes must not contain medical record numbers, dates of birth, full specimen identifiers, or restricted dataset accession strings. Use the study's internal subject codes only if your institutional policy approves them for the repository, and de-identify before anything reaches git add. The same review habit that protects the data files protects the metadata around them.

Collaborating on GitHub With Pull Requests

GitHub gives researchers a shared home for branches, pull requests, and issues, and the pull request is where branch naming pays off. A primer on version control for surgical data science presents branches and pull requests as the standard mechanism for collaboration, including multi-institutional studies where analysts at different centers review each other's code [1].

A pull request (a proposal to merge one branch into another, with a discussion thread and a diff) inherits its headline from the branch name, so a name like feature/qc-trimmer-wrapper produces a self-explanatory request while ab-branch-3 produces a mystery. Four settings and habits keep the flow orderly:

  1. Branch protection. On GitHub, protect main so that direct pushes are rejected and every change arrives through a pull request with at least one review. This is a repository settings change, not a command.
  2. Delete after merge. Use the delete-branch button on the merged pull request, then run git fetch -p locally. The branch list stays short, and the commits remain in history.
  3. Draft pull requests. Open the request as a draft when the work is unfinished, so collaborators can follow progress without the branch looking ready for review.
  4. A written policy. Put the prefix table from this guide into a CONTRIBUTING.md file at the repository root, so external collaborators see the convention before their first push.

For collaborators outside your institution who should not push directly at all, use the fork workflow. They fork the repository (create their own copy on GitHub), push a well-named branch to their fork, and open a pull request across repositories. The naming convention travels with the branch name, which is exactly what you want when a reviewer or a requesting journal asks to see the code behind a figure [1].

Common Mistakes and Limitations

These are the errors that actually occur in research repositories, with the fix for each.

  1. Spaces in branch names. The name fails with fatal: '<name>' is not a valid branch name, and even where tolerated, spaces break shell scripts. Use hyphens.
  2. Case-only duplicates. Feature/QC and feature/qc coexist on a Linux machine and collide on Windows and macOS. Enforce all-lowercase names.
  3. Vague names. update, final, final2, new, and test convey nothing. Replace them with a type prefix plus a topic, and put detail in the pull request description.
  4. The prefix collision. After feature/qc exists, no branch named feature can be created, because Git reports cannot lock ref. Keep the slash in every name so the first path component is always a directory.
  5. Dates standing in for versions. A branch named release-2026-03-14 keeps moving, so nobody knows what it contained last month. Tag the commit as v1.0.0 instead and let the branch be deleted.
  6. Branch names treated as private. Names sync to the remote and to every clone. Anything you would not email to the whole study team, including patient identifiers, does not belong in a name, a commit message, or a pull request title.
  7. Renaming shared branches silently. Renaming a branch others track leaves their clones pointing at a deleted remote name until they prune. Announce the rename, then follow the three-step procedure in Step 5.
  8. Never deleting merged branches. A hundred finished branches hide the three active ones. List them with git branch, merged main, delete with -d, and prune with git fetch -p.
  9. Overlong names. GitHub truncates long branch names in pull request lists and commit pages. Keep the topic to two or four words and move detail into the description.
  10. Assuming the convention fixes reproducibility by itself. A readable branch list helps people, but a citable result needs a tag or commit hash, pinned software environments, and data stored outside the repository.

One limitation belongs to the method rather than to any mistake. Naming branches in scientific projects is a policy decision, and the right policy depends on how your lab works, how many institutions are involved, and what your data sharing agreements allow. Agree on a written convention as a team instead of copying one wholesale, and revisit it when a collaboration changes shape.

Frequently Asked Questions

What format should a Git branch name follow in a research project?

Use <type>/<topic> in lowercase kebab-case, for example analysis/ibd-pilot-16s or feature/qc-trimmer-wrapper. The type comes from a short fixed list your lab agrees on, the topic is two to four hyphenated words, and an optional issue number or ISO 8601 date can follow.

Are slashes allowed in Git branch names?

Yes. Slashes create a hierarchy, which is why analysis/ibd-pilot-16s is legal and readable. The restrictions are that no name component may start with a dot, the name may not start or end with a slash, two slashes in a row are invalid, and a bare branch named feature cannot exist once feature/qc does.

Should I put dates in branch names?

Optionally, and only in ISO 8601 form such as experiment/2026-03-14-run1, because that format sorts correctly as text. Dates do not replace version tags. A date-stamped branch keeps moving, while a tag such as v1.0.0 freezes one commit for citation.

How do I rename a branch that is already on GitHub?

Rename locally with git branch -m old new, delete the old remote name with git push origin -d old, then push the new name with git push -u origin new. There is no single rename command for a remote branch, so tell collaborators first and run git fetch -p afterward.

What should I name branches for journal revisions and reviewer responses?

Use manuscript/<paper-slug>-r1 for revision text and figures, and reviewer-response/r2-<topic> for point-by-point response work, for example manuscript/gut-paper-r1 and reviewer-response/r2-stats. The round number in the name makes the branch list read like the submission history.

Do branch names matter for reproducibility?

Indirectly. Reviewers and collaborators cite commits or tags, not branch names, because branches move. Names matter by making the repository navigable and by carrying provenance in run logs, while the citable unit is a tag such as v1.0.0 or a commit hash such as 3f2a9c1.

Can two branch names differ only by capitalization?

Technically yes on a case-sensitive system, and you should never do it. Windows and macOS filesystems are case-insensitive by default, so the two names collide on those machines and can corrupt the repository for those collaborators. Enforce all-lowercase names.

How do I clean up old branches without losing work?

List finished branches with git branch, merged main, delete them with git branch -d <name>, remove their remote counterparts with git push origin -d <name>, and run git fetch -p to prune stale mirrors. Git refuses -d on branches with unmerged commits, so the safe path loses nothing.

Sources

  1. Version Control for Surgical Data Science: A Primer on Git and GitHub.
  2. EasyAmplicon: An easy-to-use, open-source, reproducible, and community-based pipeline for amplicon data analysis in microbiome research.