28 KiB
28 KiB
# Git Branching
## Why branching matters (and why Git is different)
- Branching (general VCS concept)
- Meaning: diverge from the main line of development
- Goal: keep working without disturbing the main line
- Traditional VCS branching (typical tradeoff)
- Often “expensive”
- May require creating a full new copy of the source directory
- Large projects → branching can take a long time
- Git branching “killer feature”
- Incredibly lightweight model
- Branch operations are (nearly) instantaneous
- Switching branches is typically just as fast
- Encourages frequent branching + merging (even multiple times per day)
- Mastering branching can significantly change how you develop
## Branches in a Nutshell (how Git’s model works)
- Why you must understand Git’s storage model
- Branching is “just pointers” in Git, but that only makes sense once you know what commits are
- Reference: earlier concept “What is Git?” (snapshots, SHA-1, objects)
### Git stores snapshots, not diffs/changesets
- Git’s core model
- Instead of storing a sequence of diffs, Git stores a sequence of snapshots
- Each commit represents the state of the project at that point
### Commit objects: what a commit contains
- Commit object includes
- Pointer to the snapshot you committed (via a tree object)
- Metadata
- author name + email
- commit message
- Parent commit pointer(s)
- 0 parents → initial commit
- 1 parent → normal commit
- 2+ parents → merge commit (merging 2+ branches)
### Example: first commit with 3 files (blobs + tree + commit)
- Scenario
- Working directory contains 3 files
- You stage all and commit
- Staging step (`git add …`)
- Example:
- `git add README test.rb LICENSE`
- Git computes a checksum (SHA-1) for each file version
- Git stores each file version as a **blob** object
- The staging area (index) records the blob checksums for what’s staged
- Commit step (`git commit …`)
- Example:
- `git commit -m "Initial commit"`
- Git checksums each directory (here: project root) and stores a **tree** object
- Tree object
- Lists directory contents
- Maps filenames → blob IDs
- (And subdirectories → subtree IDs)
- Git creates a **commit** object
- Contains metadata + pointer to the root tree
- Object count after this commit (in this example)
- 3 blobs (file contents)
- 1 tree (directory listing + blob references)
- 1 commit (metadata + pointer to root tree)
### Commit history: parents create a graph
- Each new commit typically
- Points to a new snapshot (tree)
- Points to its direct parent commit (previous tip)
- Merge commits
- Have multiple parents
- Represent snapshots produced by merges
### What a branch is (Git definition)
- Branch = lightweight movable pointer to a commit (the tip)
- Default starting branch name
- `master` (historical default name)
- Moves forward automatically as you commit on it
#### Note: “master” is not special
- It’s identical to any other branch
- It’s common because
- `git init` creates it by default
- many repos never rename it
### Creating a new branch (pointer creation only)
- Command
- `git branch testing`
- Effect
- Creates a new pointer named `testing`
- Points to the same commit you’re currently on
- Does **not** switch your working branch
### HEAD: how Git tracks “current branch”
- HEAD in Git
- Special pointer to the local branch you currently have checked out
- Difference from other VCSs (conceptual)
- In Git, HEAD is a pointer to the current local branch (not just “latest revision” in a repo)
### Seeing branch pointers in `git log`
- Useful visualization option
- `git log --oneline --decorate`
- What `--decorate` shows
- Labels like `HEAD -> master`
- Other branch pointers (e.g., `testing`) attached to commits
### Switching branches (checkout)
- Switch to an existing branch
- `git checkout testing`
- Effect
- Moves HEAD to point to `testing`
- Your next commit will advance `testing` (because HEAD points to it)
### Committing advances only the checked-out branch pointer
- If you commit while on `testing`
- `testing` pointer moves forward
- `master` pointer stays behind (unchanged)
### Switching back updates pointers and your working directory
- Switch back
- `git checkout master`
- Checkout does two major things
- Moves HEAD to `master`
- Resets working directory to match the snapshot at `master`’s tip
- Result
- Your future work on `master` diverges from the commit you left behind on `testing`
#### Note: `git log` doesn’t show all branches by default
- Default behavior
- `git log` shows the history reachable from the currently checked-out branch
- To see another branch’s history explicitly
- `git log testing`
- To see all branches
- `git log --all` (often paired with `--graph` and `--decorate`)
#### Note: switching branches changes working directory files
- On branch switch, Git may
- add files
- remove files
- modify files
- Safety rule
- If Git can’t switch cleanly (because it would overwrite conflicting uncommitted changes), it will block the checkout
### Divergent history and visualization
- When both branches get new commits after diverging
- History becomes a DAG with multiple “tips”
- View divergence clearly
- `git log --oneline --decorate --graph --all`
### Why Git branches are cheap
- A branch is stored as a simple reference (a file) containing
- the 40-character SHA-1 of the commit it points to
- plus a newline → ~41 bytes written
- Consequences
- Create/delete branches instantly
- Switching is fast because it’s mostly pointer movement + updating working directory snapshot
- Contrast: older VCS branching
- Often implemented by copying the entire project directory
- Can take seconds/minutes depending on repo size
- Merge support benefit
- Git records parent pointers in commits
- Merge-base detection for merges is typically automatic and easy
### Creating a branch and switching immediately
- Common pattern
- `git checkout -b <newbranch>`
- Git ≥ 2.23 alternative: `git switch`
- switch to existing: `git switch <branch>`
- create + switch: `git switch -c <newbranch>` (or `--create`)
- return to previous branch: `git switch -`
## Basic Branching and Merging (realistic workflow)
- Example workflow goal: develop features while handling urgent production fixes
- High-level steps (website scenario)
- Work on site
- Create branch for a user story
- Work on that branch
- Urgent hotfix appears
- switch to production branch
- create hotfix branch
- test hotfix
- merge hotfix and deploy
- return to user story branch
### Basic Branching example (issue branch + hotfix branch)
- Starting assumption
- You already have a few commits on `master`
#### Create and work on a topic branch (issue #53)
- Create + switch
- `git checkout -b iss53`
- Shorthand for
- `git branch iss53`
- `git checkout iss53`
- Do work and commit
- edit `index.html`
- `git commit -a -m "Create new footer [issue 53]"`
- Result
- `iss53` advances (HEAD points to it)
#### Interrupt with urgent hotfix (without mixing in feature work)
- Key rule before switching branches
- If working directory or staging area has uncommitted changes that would conflict, Git blocks switching
- Best practice: keep a clean working state when switching
- Mentioned workarounds (covered later): stashing, commit amending
- Switch back to production/stable branch
- `git checkout master`
- What you gain
- working directory restored to `master` snapshot (pre-issue work)
- you can focus on hotfix cleanly
#### Create and finish the hotfix
- Create + switch to hotfix branch
- `git checkout -b hotfix`
- Fix and commit
- edit `index.html`
- `git commit -a -m "Fix broken email address"`
#### Merge hotfix into master (fast-forward)
- Merge steps
- `git checkout master`
- `git merge hotfix`
- Why it’s a “fast-forward” merge
- hotfix tip commit is directly ahead of master tip commit
- No divergence to reconcile
- Git simply moves the `master` pointer forward
- Deployment outcome
- master now points to a commit whose snapshot includes the hotfix
- you can deploy production fix
#### Delete completed hotfix branch
- Delete (safe when merged)
- `git branch -d hotfix`
- Rationale
- master already contains the hotfix work
#### Return to feature branch (iss53) and continue
- Switch back
- `git checkout iss53`
- Continue work and commit
- `git commit -a -m "Finish the new footer [issue 53]"`
- Important note: hotfix isn’t in `iss53` automatically
- Options if needed
- merge master into iss53: `git merge master`
- or wait until iss53 is merged back into master
### Basic Merging (merge feature branch into master)
- When issue #53 is done
- `git checkout master`
- `git merge iss53`
- Why this merge differs from the hotfix merge
- histories diverged earlier
- master tip is not an ancestor of iss53 tip
- Git performs a three-way merge
- Inputs
- snapshot at master tip
- snapshot at iss53 tip
- snapshot at their common ancestor
- Output
- a new merged snapshot
- a new merge commit
- “special” because it has more than one parent
- Merge strategy note (as shown in output)
- merge made by the `recursive` strategy (typical default for two heads)
#### Clean up merged branch
- Delete iss53 after merge
- `git branch -d iss53`
### Basic Merge Conflicts (when Git cannot auto-merge)
- When conflicts occur
- both branches changed the same part of the same file differently
- What `git merge` does on conflict
- stops and reports conflict(s)
- does NOT create the merge commit yet
- requires manual resolution
#### Identify unmerged paths
- Use
- `git status`
- Status shows
- you are in a merging state
- list of “unmerged paths”
- hints to:
- fix conflicts
- `git add` files to mark resolution
- then `git commit` to conclude merge
#### Conflict markers inserted into files
- Git writes markers like
- `<<<<<<<` (start of conflict block)
- `=======` (separator)
- `>>>>>>>` (end of block)
- Meaning
- Top section = HEAD version (current branch at merge time, e.g., master)
- Bottom section = incoming branch version (e.g., iss53)
#### Resolve and mark as resolved
- Manual resolution workflow
- edit file(s)
- choose one side or combine them
- remove all markers
- Mark resolution
- `git add <file>` for each conflicted file
- staging indicates conflict resolved in Git
#### Using a merge tool (optional)
- Run
- `git mergetool`
- Behavior
- opens a visual merge tool
- helps walk through conflict resolution
- If not configured
- Git warns `merge.tool` isn’t configured
- offers possible tool choices (platform dependent)
- you can specify an alternative tool name
#### Finalize the merge
- Verify state
- `git status`
- typically indicates “all conflicts fixed” but merge still in progress
- Conclude
- `git commit`
- Merge commit message details
- default message mentions merged branch
- often lists conflicts
- note in message references merge metadata (e.g., `.git/MERGE_HEAD`)
- you may edit message to explain how/why conflicts were resolved
- Reference for deeper conflict handling
- “Advanced Merging” (mentioned as later coverage)
## Branch Management (everyday utilities)
- `git branch` does more than create/delete
- provides multiple views and filters of branch state
### Listing branches
- `git branch`
- lists local branches
- `*` shows current branch (HEAD points here)
### See last commit on each branch
- `git branch -v`
- shows branch tip commit SHA + message summary
### Filter by merge status
- `git branch --merged`
- branches already merged into current branch
- usually safe to delete those (except the current `*` branch)
- `git branch --no-merged`
- branches not merged into current branch
- deletion safety
- `git branch -d <branch>` fails if not fully merged
- `git branch -D <branch>` forces deletion (discarding unmerged work)
#### Note: merge-status filters are relative to a base
- Default base
- current branch (if no argument given)
- You can compare relative to a different branch without checking it out
- `git branch --no-merged master`
## Changing a branch name (rename)
- Safety warning
- do not rename branches still used by other collaborators
- do not rename default branches (master/main/etc.) without reading next section
### Rename locally
- `git branch --move bad-branch-name corrected-branch-name`
- Effect
- preserves history
- changes only your local ref name initially
### Publish the renamed branch and set upstream
- `git push --set-upstream origin corrected-branch-name`
- Effect
- creates the new remote branch name
- configures tracking
### Remove the old remote branch name
- `git push origin --delete bad-branch-name`
- Effect
- fully replaces the bad remote name with the corrected one
### Verification
- `git branch --all`
- shows local branches and `remotes/origin/...` remote-tracking refs
## Changing the master branch name (e.g., `master` → `main`)
- High-impact warning
- renaming default branch can break
- integrations/services
- helper utilities
- build/release scripts
- any references in code, configs, docs
- consult collaborators
- search/update all references to the old name
### Local rename
- `git branch --move master main`
- Result
- local `master` ref no longer exists
- local `main` points to the same commit tip
### Push and set upstream
- `git push --set-upstream origin main`
- Result
- remote now has `main`
- remote may still have `master`
- remote HEAD may still point to `origin/master` until host settings change
### Migration checklist (must update external references)
- Dependent projects
- update code/config referencing old branch
- Test runner configs
- update any branch-name assumptions
- Build/release scripts
- update target branch names
- Repo host settings
- default branch
- merge rules / protections
- other branch-name-based settings
- Documentation
- update old references
- Pull requests
- close/merge/retarget PRs aimed at old branch
### Delete old remote branch after transition
- `git push origin --delete master`
## Branching Workflows (patterns enabled by lightweight branches)
- Goal
- choose a branching strategy that matches team/release needs
- Key enabler
- easy repeated three-way merges over time
### Long-Running Branches (progressive stability)
- Concept
- keep multiple always-open branches for different stability levels
- merge “upwards” as code becomes stable
- Common pattern
- `master`: only stable/released (or release-candidate) code
- `develop` / `next`: integration/testing branch; can be unstable
- topic branches merged into develop/next for testing before master
- How to think about “stability”
- linear commit history view
- stable branches are “behind” (older, tested commits)
- bleeding-edge branches are “ahead” (newer, less proven commits)
- “silo” view
- commits graduate to more stable silos once fully tested
- Multi-level stability in large projects
- additional branches like `proposed` / `pu` (proposed updates)
- idea: not everything is ready for `next` or `master` immediately
- Note
- not required, but often helpful for large/complex projects
### Topic Branches (short-lived branches)
- Definition
- branch created for a single feature/bugfix/experiment
- typically merged and deleted after completion
- Why Git makes this common
- branch creation/merging is cheap → can do it many times a day
- Benefits
- clean context switching (work isolated by topic)
- easier code review (topic’s commits grouped)
- flexible integration timing (minutes, days, months later)
- can merge in any order regardless of creation order
- Example topology from the chapter
- work on `master`
- branch `iss91` (issue work)
- branch `iss91v2` off `iss91` (alternate approach)
- return to `master` and continue other work
- branch `dumbidea` off `master` (experimental idea)
- outcome
- discard `iss91` if inferior
- merge `iss91v2` and `dumbidea` if chosen
- Reminder: local operations
- branching/merging is local-only until you fetch/push/pull
- Reference mention
- more workflow discussion later in “Distributed Git”
## Remote Branches (remote references + remote-tracking branches)
### Remote references overview
- Remote repos contain references (pointers) to
- branches
- tags
- other refs
- Ways to inspect
- `git ls-remote <remote>` (full list of remote refs)
- `git remote show <remote>` (focus on remote branches + info)
### Remote-tracking branches
- Definition
- local references that record the state of remote branches
- you can’t move them yourself
- Git updates them during network communication
- Naming
- `<remote>/<branch>`
- Examples
- `origin/master`
- `origin/iss53`
- Mental model
- bookmarks showing where a remote branch was last time you connected
### Clone example (how origin/master appears)
- When cloning from a server
- Git names the remote `origin` by default
- downloads data
- creates `origin/master` (remote-tracking)
- creates your local `master` starting at same commit as origin’s master
#### Note: “origin” is not special
- It’s just the default name created by `git clone`
- You can rename the default remote at clone time
- `git clone -o booyah ...` → remote-tracking branch becomes `booyah/master`
### Divergence between local and remote
- If you commit locally and someone else pushes to the remote
- histories diverge
- `origin/master` does not move until you communicate
### Fetching updates remote-tracking branches
- `git fetch origin`
- contacts remote
- downloads objects you don’t have
- updates pointers like `origin/master` to newer commits
### Multiple remotes
- Add another remote
- `git remote add teamone <url>`
- Fetch it
- `git fetch teamone`
- Possible outcome
- if teamone has only a subset of commits you already have from origin:
- fetch downloads no new objects
- still updates `teamone/master` pointer to match teamone’s master tip
## Pushing (sharing branches)
### Why pushing is explicit
- Local branches do not automatically sync to remotes
- Benefit
- you can keep private local branches
- push only branches you intend to share/collaborate on
### Push a branch
- Pattern
- `git push <remote> <branch>`
- Example
- `git push origin serverfix`
- What Git expands it to (conceptual)
- `refs/heads/serverfix:refs/heads/serverfix`
- Push local branch to a different remote branch name
- `git push origin serverfix:awesomebranch`
### Authentication convenience (HTTPS)
- HTTPS push commonly prompts for username/password
- To avoid typing credentials repeatedly
- credential cache example:
- `git config --global credential.helper cache`
- reference mentioned: “Credential Storage” (for other options)
### After someone else fetches
- Fetching a pushed branch
- `git fetch origin`
- Result
- creates/updates a remote-tracking ref (e.g., `origin/serverfix`)
- does NOT create a local editable branch automatically
### Using fetched remote-tracking branch work
- Merge directly into current branch
- `git merge origin/serverfix`
- Create a local branch based on it (editable) and track it
- `git checkout -b serverfix origin/serverfix`
## Tracking Branches (local branches that track upstream)
### Definitions
- Tracking branch
- local branch tied to a remote-tracking branch
- Upstream branch
- remote-tracking branch the local branch tracks
### Why tracking matters
- On a tracking branch, `git pull` can automatically
- fetch from the right remote
- merge the right branch
### How tracking branches are created
- Common creation form
- `git checkout -b <branch> <remote>/<branch>`
- Shorthand
- `git checkout --track origin/serverfix`
- Extra shortcut
- `git checkout serverfix`
- works if
- local `serverfix` doesn’t exist, and
- exactly one remote has `serverfix`
- Different local name than remote branch
- `git checkout -b sf origin/serverfix`
- local `sf` tracks `origin/serverfix`
### Set or change upstream later
- `git branch -u origin/serverfix`
- also available as `--set-upstream-to`
### Upstream shorthand in commands
- `@{upstream}` or `@{u}`
- references the upstream branch of the current branch
- Example
- `git merge @{u}` instead of `git merge origin/master` (when master tracks origin/master)
### Inspect tracking status and ahead/behind
- `git branch -vv`
- shows local branches
- indicates upstream tracking target
- shows ahead/behind counts
- Interpreting counts
- ahead N → N local commits not pushed
- behind N → N remote commits not merged locally
- Cache caveat
- ahead/behind shown is from last fetch; command doesn’t contact server
- To refresh counts
- `git fetch --all; git branch -vv`
## Pulling (fetch + merge convenience)
- `git fetch`
- downloads new data
- does not modify working directory
- leaves integration to you (merge/rebase)
- `git pull`
- in most cases = `fetch` immediately followed by `merge`
- uses tracking (upstream) info to pick remote + branch
- Guidance from the chapter
- explicit `fetch` + `merge` is often clearer than the “magic” of `pull`
## Deleting Remote Branches
- When a remote branch is no longer needed
- merged into mainline/stable branch on the server
- Delete remote branch pointer
- `git push origin --delete serverfix`
- Effect
- removes the branch pointer on the server
- server may keep underlying objects until garbage collection
- accidental deletions can often be recovered before GC runs
## Rebasing (the other integration strategy)
- Two main ways to integrate changes between branches
- `merge`
- `rebase`
### The Basic Rebase (replaying commits)
- Starting situation
- branches diverged; each has unique commits
- Merge recap (already covered earlier)
- three-way merge of:
- tip snapshot A
- tip snapshot B
- common ancestor snapshot
- creates a new snapshot + merge commit
- Rebase concept
- take the patch introduced by commits on one branch
- reapply them on top of another branch’s tip
- Example commands
- `git checkout experiment`
- `git rebase master`
- Internal steps (conceptual)
- find common ancestor between current branch and target branch
- compute diffs for each commit on current branch since ancestor
- save diffs temporarily
- reset current branch to target tip
- apply diffs sequentially (creating new commits with new SHAs)
- After rebase
- integrate by fast-forward merge
- `git checkout master`
- `git merge experiment`
- Result comparison
- final snapshot content is the same as with merge
- history is different
- rebase → linear-looking history
- merge → preserves the true parallel shape
- Common use case (contributing workflow)
- rebase your work onto `origin/master` before submitting patches
- maintainer can integrate via fast-forward / clean apply
- Core conceptual distinction
- rebase: replay changes in order introduced
- merge: combine endpoints and record a merge
### More Interesting Rebases (rebasing a branch off another topic branch)
- Scenario
- topic branch `server` created from master; commits added
- topic branch `client` created from `server`; commits added
- later additional commits added to `server`
- Goal
- ship client changes now (merge into master)
- delay server changes until tested
- Use `--onto`
- `git rebase --onto master server client`
- Meaning
- take commits on `client` that are not on `server`
- replay them as if `client` started from `master`
- Integrate client quickly
- `git checkout master`
- `git merge client` (fast-forward)
- Integrate server later without manual checkout
- `git rebase master server`
- checks out `server` and replays onto master
- `git checkout master`
- `git merge server` (fast-forward)
- Cleanup
- delete topic branches once integrated
- `git branch -d client`
- `git branch -d server`
### The Perils of Rebasing (rewriting published history)
- The one-line rule
- Do not rebase commits that exist outside your repository and that people may have based work on
- Why rebasing public commits is dangerous
- rebase abandons existing commits and creates new ones
- new commits have different SHAs
- collaborators who based work on old SHAs must reconcile mismatched history
- Example failure pattern (from the chapter)
- you clone and do work
- someone else pushes a merge to the central server
- later they rebase their work and `push --force` (rewriting server history)
- you fetch new commits
- if you `git pull` normally, you may create a merge combining old + new lines
- can lead to duplicate-looking commits (same message/author/date) with different IDs
- pushing that back can reintroduce commits the other dev tried to eliminate
- Social consequence emphasized
- if you rewrite shared history, teammates will have to re-merge and untangle confusion
### Rebase When You Rebase (recovering after a force-push)
- Problem after force-push
- determine which commits are uniquely yours vs rewritten copies
- Patch-id concept
- besides commit SHA-1, Git can compute a checksum based on the patch content (“patch-id”)
- How rebase helps
- rebasing onto the updated target can let Git:
- identify which commits are already represented (same patch)
- replay only the unique commits
- Example approach
- `git rebase teamone/master`
- What Git may compute during this recovery rebase (as described)
- determine commits unique to your branch
- exclude merge commits from replay
- detect commits that were rewritten but represent the same patch in the target
- apply remaining unique commits on top of the updated branch
- Limitation noted
- works best if rewritten commits are almost the same patch
- otherwise Git may not detect duplication and may reapply a similar patch (possibly failing)
- Convenience options
- `git pull --rebase` instead of normal pull
- or manual: `git fetch` then `git rebase <remote>/<branch>`
- configure default:
- `git config --global pull.rebase true`
- Safety guideline recap
- safe: rebase commits that never left your machine
- generally ok: rebase pushed commits if nobody based work on them
- risky: rebase publicly shared commits → coordinate + warn others to use `pull --rebase`
### Rebase vs. Merge (choosing based on what “history” means)
- Two viewpoints on commit history
- History as a factual record
- commit history documents what actually happened
- rewriting is “lying” about events
- merge commits reflect real parallel work
- History as a curated story
- raw development includes missteps and dead ends
- before mainline, rewrite history to tell a clearer story
- tools mentioned: `rebase`, `filter-branch`
- Conclusion
- no universal best choice; depends on team/project
- Practical “best of both worlds” guideline
- rebase local changes before pushing (clean up)
- never rebase anything you’ve pushed somewhere shared/public
## Summary (skills this chapter expects you to have now)
- Branch creation and switching
- create branches, move between them
- understand HEAD as “current branch pointer”
- Merging
- fast-forward merges
- three-way merges and merge commits (multiple parents)
- resolve conflicts (markers, `status`, `add`, `mergetool`, final `commit`)
- Branch management
- list branches and identify current branch
- inspect branch tips (`-v`)
- find merged/unmerged branches (`--merged`, `--no-merged`)
- delete safely (`-d`) or forcibly (`-D`)
- rename branches (local + remote cleanup)
- rename default branch (master/main) with ecosystem updates
- Collaboration with remotes
- remote-tracking branches, fetch/push/pull behaviors
- create tracking branches and set upstream
- delete remote branches
- Rebasing
- what rebase does and why it can make history linear
- advanced rebase (`--onto`)
- when rebasing is dangerous and how to mitigate with `pull --rebase`
- Next topic preview (mentioned)
- how to run your own Git repository-hosting server