Repository navigation
Deprecate Git::Branch and Git::Branches (and Git::Repository#branch/#branches) in favor of the BranchInfo API #1639
Description
Summary
Deprecate the ActiveRecord-style branch objects and their facade entry points in favor of the Git::BranchInfo-based API, following the same pattern used for remotes.
Scope of deprecation:
Git::Branch(class)Git::Branches(collection class)Git::Repository#branch— returnsGit::BranchGit::Repository#branches— returnsGit::Branches
Not deprecated: the rest of Git::Repository::Branching. That module is the replacement — branch_list, checkout, branch_new, branch_delete, branch_contains, update_ref, current_branch, etc. all stay. Only the two methods that hand back AR objects (#branch, #branches) are in scope.
Proposed timeline: deprecate as early as 5.0.0, remove in 6.0.0.
Motivation
Git::BranchInfo (immutable, remote-aware) already exists and is returned by Git::Repository#branch_list. Git::Branch/Git::Branches are the legacy def initialize(base, …) rich objects that both hold data and perform operations. Steering users to branch_list + facade operations aligns branches with the *Info DAO direction (see umbrella #1636) and sidesteps the legacy string-constructor bug in #1631.
Capability audit — no gap
Every Git::Branch operation is already a thin delegate to an existing Git::Repository facade method that accepts a name/string:
Git::Branch method |
Facade equivalent |
|---|---|
checkout |
Git::Repository#checkout (see note on create-if-missing) |
create |
Git::Repository#branch_new |
delete |
Git::Repository#branch_delete |
current |
Git::Repository#current_branch |
contains? |
Git::Repository#branch_contains |
update_ref |
Git::Repository#update_ref |
merge |
Git::Repository#merge (see note on the merge(branch) overload) |
archive |
Git::Repository#archive |
gcommit |
Git::Repository#gcommit |
stashes |
Git::Repository#stashes_all (tracked separately in #1637) |
full / name / remote |
Git::BranchInfo#refname / #short_name / #remote_name |
Git::Branches (collection): each/[]/local/remote/size map onto Git::Repository#branch_list (which returns Array<Git::BranchInfo>) plus array operations.
Preconditions (do before deprecating the composites)
Two Git::Branch methods are multi-step conveniences with no single facade equivalent. Provide replacements first so the deprecation notes can point somewhere real:
in_branch(message) { … }— temp checkout → commit-or-reset → restore original branch. Re-home as aGit::Repository#in_branchfacade method (or document a recipe).merge(branch)overload — "merge X into branch Y without checking out Y first." The bare facade#mergemerges into the current branch, so add a facade path or document the recipe.
Semantic wrinkles to document in the deprecation notes
checkoutauto-creates:Git::Branch#checkoutcallscheck_if_createbefore checking out;Git::Repository#checkout(name)does not. Call this out so migrators aren't surprised.- Return-shape change:
#branchesreturns an enumerable ofGit::Branch;branch_listreturnsArray<Git::BranchInfo>. Access shifts frombranch.nametoinfo.short_name, etc. #stashesis a latent-bug method (ignores the branch, returns all repo stashes) — already split out to Deprecate Git::Branch#stashes in favor of Git::Repository#stashes_all #1637.
Per-method deprecation messages (draft)
Each warning should name the exact replacement, e.g.:
git.branch('x')→ "Userepo.branch_list/repo.branch_contains/ name-based facade methods instead."git.branch('x').delete→repo.branch_delete('x')git.branch('x').checkout→repo.checkout('x')(note: no auto-create)git.branches→repo.branch_listgit.branches['x']→repo.branch_list.find { |b| b.short_name == 'x' }
Use the existing Git::Deprecation.warn pattern (as in Git::Repository#stash_list).
Suggested sequencing
- Land the
in_branchandmerge-into-branch facade replacements. - Deprecate the 1:1-replaceable methods/accessors first (
#branch,#branches, and theGit::Branchoperation methods that already have exact facade equivalents). - Hold deprecation of
in_branch/merge(branch)until step 1 ships. - Ensure deprecation messages don't point at other about-to-be-deprecated APIs (e.g., don't route
#stashesusers throughGit::Stashes).
Blast radius
Git::Branch, Git::Branches, #branch, #branches are core, high-traffic, @api public. Expect significant deprecation-warning volume; phasing (above) keeps 5.0.0 warnings honest and each pointed at a real replacement.
Testing
- Unit specs asserting each deprecation warning fires (mirror the
stash_listdeprecation test). - Specs for the new
in_branch/merge-into-branch facade methods. UPGRADING.mdmigration section with the per-method mapping above.
Affected files (initial)
lib/git/branch.rb,lib/git/branches.rblib/git/repository/branching.rb(#branch,#branches; newin_branch/merge path)spec/unit/git/branch_spec.rb,spec/unit/git/branches_spec.rb,spec/unit/git/repository/branching_spec.rbUPGRADING.md
Related
- Resolve slash-containing remote names in Git::Branch string constructor path #1631 — slash-remote bug in
Git::Branchstring constructor (supports moving users to theBranchInfopath) - Deprecate Git::Branch#stashes in favor of Git::Repository#stashes_all #1637 — deprecate
Git::Branch#stashes(sub-case of this effort) - Migrate remaining ActiveRecord-style classes to *Info DAO objects (umbrella) #1636 — umbrella
*Info/DAO migration archive/v5-redesign/branch_parse_refactor_plan.md
Activity
jcouball commentedon Aug 21, 2026
Audit result and one semantic wrinkle
Two findings that were briefly drafted as ADRs. Neither is a decision, so both belong here instead.
Git::Branchhas no capability gapGit::Branchcan be deprecated without replicating its behavior somewhere first. Every operation on the class already delegates to aGit::Repositoryfacade method that takes a branch name or any string. Nothing onGit::Branchreaches into git in a way the facade cannot.Two composites are the exception, and both wrap existing calls rather than adding capability.
in_branchstashes, checks out, yields, and restores. Themerge(branch)overload merges the receiver into the current branch. Both are tracked in #1641 as preconditions for the deprecation.Worth recording because the opposite conclusion is the intuitive one. A class with twenty methods looks like it must do something the facade cannot, and the next person to pick this up will assume a capability audit is still owed. It is not. This is it.
#checkoutis not a drop-in replacementGit::Branch#checkoutcallscheck_if_createfirst, so it creates the branch when it does not exist and swallows any error from that attempt.Git::Repository#checkout(name)does not. Checking out a branch that does not exist fails.A caller who follows a deprecation warning from
branch('feature').checkouttocheckout('feature')gets a failure the first time the branch is new. The deprecation warning and the upgrade notes have to name this and point at the two-call replacement. Changing the semantics quietly under a deprecation notice is worse than not deprecating the method, because the warning tells the caller the migration is safe.jcouball commentedon Sep 3, 2026
Git::Repository#in_branchandGit::Repository#merge_intolanded in #1748 (refs #1641). When this issue deprecatesGit::Branch#in_branchand theGit::Branch#merge(branch)overload, the UPGRADING.md entry (following the pattern of theGit::Branch#stashesandGit::Repository#remotesentries) needs to call out these behavior differences between the deprecated methods and their replacements:g.branch('x').in_branch { ... }createdxif it did not exist.g.in_branch('x') { ... }raisesArgumentErrorunlessxis an existing local branch, so callers must callbranch_createfirst. A commit SHA, tag, or remote-tracking name is also rejected before any checkout.Git::Branch#in_branchrecorded the literalHEADand could not restore a detached HEAD to its original commit.g.in_branchandg.merge_intorecord the SHA and restore it.Git::Errorbefore checking anything out when HEAD is on a branch with no commits. The old methods failed later, mid-flow.g.branch('main').merge('feature')returned stdout from the final restore checkout and ran a hard reset after the merge.g.merge_into('main', 'feature')returns the merge's stdout and does no reset. It also rejects the:no_commitoption; callers who need--no-commitusecheckoutandmergedirectly.The deprecation messages for
Git::Branch#in_branchandGit::Branch#merge(branch)should point atGit::Repository#in_branchandGit::Repository#merge_intorespectively.feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…
feat(branch): deprecate Git::Branch and Git::Branches in favor of Git…