This package provides scripts to support managing multiple branches of a git repository.
The scripts support a work style in which you have a separate working tree
for each branch that you work on. You never switch branches within a
working tree. I find that this reduces branch confusion.
This work style replaces mechanisms such as git stash.
The key principles of this work style are:
- Do not run git commands in the main branch, except
git pull. (Or, usemvc pull.) - Do not change branches in any working tree.
That is, never run
git checkout, and never rungit stash. - To create a new branch, run the command
gnborgit-new-branch. It creates a new directory holding a new branch that starts as a copy of the branch from which you rangnb. - To check out an existing branch, run
gcborgit-checkout-branch. It creates a new directory and checks out the branch there.
The commands are:
git-checkout-branchBRANCHNAME: Checks out the given branch of the repository in a new working tree directory. Run this command from within a working tree; the new directory is a sibling of the working tree. The new directory is a worktree (see below) that holds a fresh checkout of the branch: it does not inherit the current directory's uncommitted changes, untracked files, or build output, so the first build there is a full one. A branch can be checked out in only one worktree of a repository at a time, though separate clones can each check it out. A branch that has submodules gets a copy of the whole directory instead, which is an independent clone. Below is a definition for an aliasgcb.git-new-branch[--clean | --copy] BRANCHNAME: Creates and checks out the given branch of the repository in a new working tree directory. Run this command from within a working tree; the new directory is a sibling of the working tree. This command does not push the new branch. By default, the new directory is a worktree (see below) that shares the repository, and its contents are the same as the current directory's, including uncommitted changes and untracked files. With--clean, it is a worktree that holds a clean checkout and nothing else. With--copy, it is a copy of the whole directory,.gitincluded, and so an independent clone; a working tree that has submodules always gets this. Below is a definition for an aliasgnb.git-push-to[--nocompile] [--rebase | --squash] FROM_DIR TO_DIR ...: Pulls from FROM_DIR into TO_DIR, compiles TO_DIR, then pushes TO_DIR to its remote if compilation succeeds. Each directory must be the top level of a working tree and must have an upstream branch. You may also pass a list of directories: each is pushed into the subsequent one. Instead of directories, you may pass slugs of the formORG:BRANCH, such asgit-push-to upstream:main myname:feature. Each slug names the directoryPROJECT-fork-ORG-branch-BRANCH(orPROJECT-fork-ORG, if it has BRANCH checked out) in the current directory. Each PROJECT that has directories for two or more of the slugs gets its own chain. A slug may also be justBRANCH, as ingit-push-to main feature. It matches every ORG; each PROJECT and ORG gets its own chain. It also matchesPROJECT-branch-BRANCH, as created bygit-new-branchfrom a clonePROJECT, and it matches the clonePROJECTitself if that has BRANCH checked out and noPROJECT-branch-BRANCHexists; those directories form one more chain per PROJECT. With--rebaseor--squash, it then does whatgit-rebase-todoes.git-rebase-to[--nocompile] [--squash] FROM_DIR TO_DIR ...: Runsgit-push-toon the arguments, then rebases each branch onto the previous branch, to eliminate merge commits, and force-pushes each branch. This is useful for maintaining a stack of GitHub pull requests, each of whose base is the previous one. With--squash, each branch becomes a single commit on top of the previous branch. The arguments may be slugs, as forgit-push-to.git-pull-from[--nocompile] OTHER-REPO-DIR: Pulls from OTHER-REPO-DIR into the current directory, compiles the current directory, then pushes the current directory to its remote if compilation succeeds. OTHER-REPO-DIR and the current directory must both be the top level of a working tree and must each have an upstream branch.is-deleted-branchDIRECTORY: Tests whether the given directory is the top level of a deleted branch. It is also true of a directory that holds nothing but Eclipse.projectfiles, which a branch directory may leave behind when its working tree is gone.git-orphaned-branches[--print0] [--remove]: Lists directories named*-branch-*, below the current directory, that are working trees for branches that were deleted in the remote repository, or that hold nothing but Eclipse.projectfiles. With--remove, removes each of them instead of printing them. Typical usage isgit-orphaned-branches --removeorrmgob(see alias below).git-remove-branch-directory[--force] [--force-uncommitted] [--force-unpushed] [--repository DIR] DIRECTORY...: Removes a branch directory, unless the removal would lose work. Commits on a branch whose changes have landed in a remote's default branch, as after a squash-merge, do not count as work that would be lost. It removes a worktree withgit worktree removeand deletes its branch, ifgit-new-branchorgit-checkout-branchcreated that branch and no other worktree has it checked out, and it removes anything else withrm -rf. Given a worktree whose directory was already removed withrm -rf, it removes what that left behind: the worktree's registration and, under the same conditions, its branch, in the repository that--repositorynames (by default, the current directory's).git-outgoing[--only-branch BRANCH]... [--omitted | GIT-LOG-ARGS]: Shows the commits on local branches that have not been pushed. Unlikegit log --branches --not --remotes, it omits a branch whose changes are already in a remote's default branch, as after a squash-merge.compile-project[--clean] [DIRECTORY]: Runs a Gradle, Maven, or Make command to compile the project that contains the given directory, which defaults to the current directory. The command-line arguments for the compilation can be customized, using the environment variables described below.relative-pathBASE TARGET: Prints the pathname of TARGET, relative to BASE. This is a portable replacement for GNUrealpath --relative-to=BASE TARGET.
More documentation of each script appears at the top of the script. Click the command names above to see that documentation.
These environment variables customize the behavior of the commands. Each one takes effect when it is set to a non-empty value.
GRADLE_ASSEMBLE_FLAGS:compile-projectpasses this togradlew assembleorgradle assemble.MVN_COMPILE_FLAGS:compile-projectpasses this tomvnw compileormvn compile.MAKE_FLAGS:compile-projectpasses this tomake.ERR_IF_NO_BUILDFILE:compile-projectfails if it finds no buildfile. Otherwise,compile-projectsucceeds when it finds no buildfile.MANAGE_GIT_BRANCHES_SKIP_COMPILE_PROJECT:git-push-to,git-pull-from, andgit-rebase-toskip the compilation step, and push whenever the merge succeeds.DEBUG:git-orphaned-branchesprints a message about each directory that it examines.GIT_SSH_COMMANDandGIT_SSH:is-deleted-branch,git-orphaned-branches,git-new-branch,git-checkout-branch, andgit-push-touse these standard Git variables to select the SSH command when they query a remote repository.GIT_SSH_VARIANT:is-deleted-branch,git-orphaned-branches,git-new-branch,git-checkout-branch, andgit-push-touse this standard Git variable to determine whether the selected SSH command accepts OpenSSH options.
Clone this repository:
git clone https://github.com/mernst/manage-git-branches.gitFor convenience, add the following commands to your shell startup file,
such as ~/.profile.
export PATH="/path/to/manage-git-branches:${PATH}"
alias gcb=git-checkout-branch
alias gnb=git-new-branch
alias rmgob='git-orphaned-branches --remove'To run the tests, run tests/run-tests.sh. Each test creates its
repositories under a temporary directory and removes them afterward.
If you use git-checkout-branch and git-new-branch, then you will never
switch the branch for a working tree. That is, you will never run git checkout BRANCHNAME. Instead, you will have a separate working directory for each
branch that you work on. The directory's name will be
REPONAME-branch-BRANCHNAME.
This convention enables you to easily work on multiple branches at a time:
- No confusion about which fork or branch you are on: this information is evident in the directory name.
- No need to switch branches with
git checkout. Just use file system operations such ascdandln. - No need to commit or stash changes before switching branches, which would
otherwise be necessary to avoid accidentally mixing work between
branches. Never run
git stashagain! - Faster builds, because each branch has its own version of built executables, avoiding the need for your build system to recompile everything when switching branches.
- Easier non-git operations: comparing branches using your favorite tool, searching multiple branches, reconciling branches, copying text between them, etc.
- A disadvantage is greater disk usage than if you have a single clone and
you switch branches within that clone. The branch directories share one
repository, but each has its own working tree and build output, which is
usually most of a directory's size: sharing the repository saves 10-30%
of each branch directory. (
git-new-branch --cleansaves more, because the new directory holds no build output.) Usually, disk space is plentiful and the foregoing advantages are worthwhile. If you have very large repositories and limited disk space, however, this approach may be undesirable.
If other programs need a specific name for your repository, then you may wish to
make the directory REPONAME be a symbolic link to whichever branch you are
working on at the time, such as REPONAME-branch-main when you want to use the
main branch.
git-new-branch and git-checkout-branch make each branch directory a
linked worktree of the repository that you ran them in, made by
git worktree add. All the branch
directories of one repository share one object store, so the repository is
not copied. This has some consequences:
- A branch directory's
.gitis a file, not a directory. A tool that tests for a.gitdirectory to recognize a working tree does not recognize a branch directory; such a tool should ask git, withgit rev-parse --show-toplevel. - The branch directories share their configuration, hooks, and branches:
git configin one affects all of them. - A branch that has submodules is an exception. Git does not support a
submodule in more than one worktree, so
git-new-branchandgit-checkout-branchmake a full copy instead, which is an independent clone. The copy includes.git, so it can be made only from a directory whose.gitis a directory, such as a clone: run in a linked worktree, whose.gitis a file, the commands refuse, and say to run them in the main working tree instead.
Do not remove or move a worktree with file-system commands:
- Do not
rm -rfit. That leaves its branch, so the branch name stays taken, and a stale registration in the repository. Usegit-remove-branch-directoryinstead. - Do not
mvit. It keeps working, so nothing looks wrong, but its registration still names the old path, and thegit worktree prunethatgit gceventually runs then deletes the registration, after which the directory is no longer a working tree. Usegit worktree moveinstead, or rungit worktree repairin a directory that was already moved. - Do not remove or move the main working tree -- the clone that the
worktrees were made from -- while worktrees depend on it. Removing it
removes the repository that all of them use, and moving it breaks them
until you run
git worktree repairin it.
None of this applies to a directory made by git-new-branch --copy: each of
those is an independent clone, which you may remove or move like any other
directory.
git-new-branch, git-checkout-branch, git-remove-branch-directory, and
git-orphaned-branches require git 2.31 or later. The default mode of
git-new-branch requires git 2.36 or later in a sparse checkout or with
per-worktree configuration.
The multi-version-control program manages multiple clones, much as the scripts in this repository manage multiple branches.
This package is distributed under the MIT License.