Used for three purposes:
- Getting relevant branches info: Alternative to
git branch. Provides useful information about local branches. - Keeping branches up to date: Outputs a list of
gitcommands that the user can choose to run to catch up their branches to the main branch. - Helping amend commits while keeping the tree structure intact: Outputs a list of
gitcommands that the user can choose to run to amend the last commit and keep the tree structure of all dependent branches intact.
/usr/bin/env bash -c "$(curl -fsSL https://raw.githubusercontent.com/santi-h/branches/main/scripts/install.sh)"(see installation script for details)
The installer only creates one persisted directory and a symbolic link pointing to a file in that persisted directory. So, to uninstall branches you can just remove both with:
rm "$HOME/.local/bin/branches"; rm -rf "$HOME/.local/opt/branches"In a git repo, just run branches
branches
branches --help- This script is mostly developed and tested on arm64 MacOS. Executables for arm64 Linux and amd64 Linux are created and should work but not as manually tested. The CI tests do run on Linux though.
gitis installed in the system.- The main remote is called
origin. If there is nooriginset the script will work but some features won't be available. originpoints to GitHub using a SSH shorthand URL. For example"git@github.com:santi-h/branches.git"(I.e. no HTTPS)- The envar
GITHUB_TOKENis set. This is needed for github to figure out whether there is a Pull Request for each branch. The token can be created at https://github.com/settings/tokens and needs thereposcope.
For each branch, branches outputs information that is usaully handy to have:
- Whether this branch is in
originor not - If the branch is in
origin, is it in sync or something needs to be pulled or pushed? - If the branch is in
origin, a handy link to the changes in github - Whether there's commits authored by someone else other than the current git user
- Branch's last commit authored age in days
- Commits ahead/behind with respect to the main branch. Main branch = branch referenced by
git symbolic-ref refs/remotes/origin/HEAD - Base branch, in case the current branch branched off another one
- Whether this branch has a pull request
- If there is a pull request, the status of it
- If there is a pull request, a link to it
The script does not run any write git command. It only outputs suggested git operations. The user can decide to run the suggested operations. The goal of these commands is to keep branches up to date. More specifically:
-
Clean up merged branches that are already on the main branch.
-
Get all branches up to date (via
rebase), respecting the tree structure.branches operations ↓ N > N <- branch3 / / K---L---M > K---L---M <- branch2 / / I---J > I---J <- branch1 / / A---B---C---E---F---G---H <- main -
Push the branches that are currently in sync with
originand the current user is the only author. -
Safely pull updates that exist in
originbut not locally
All suggestions are meant to be as safe as possible. For example, in order for it to suggest a branch to be deleted, all these conditions need to be satisfied:
- It is not the main branch
- A PR existed and was merged
- The merged sha is exactly the same as the local branch sha
- The branch was deleted in origin
The command that it outputs is a suggestion and the user makes the call whether to run it as is, change it and run it, or ignore it.
The update commands follow a no-merge approach. All update suggestions are rebase operations. If a branch contains merge commits, no rebase operation will ever be suggested for that branch.
Let's say we have a structure like this:
D---E <- branch2
/
C <- branch1
/
A---B <- main
branch2 depends on branch1.
Let's say you're on branch1 and realized a line should be moved 3 lines down. You don't want to create another commit for this, so you want to amend C with git commit --amend.
The process for this can be involved:
-
You make the code change while on
branch1and rungit add . && git commit --amend --no-edit. Now you have a structure like this:D---E <- branch2 / C G <- branch1 |/ A---B <- maini.e. you created a separate commit
Gandbranch2not only no longer depends onbranch1, but it also didn't get the update. So, -
You now have to rebase
branch2, but this rebase is not super trivial. If you rungit checkout branch2 && git rebase branch1there's a high likelyhood of a conflict. So, there's a manual step involved. You might be tempted to rungit rebase branch1 -X oursto automatically resolve the conflict by accepting the change inbranch1, but this not always results in what you expect. To understand why this sometimes doesn't work try the following in a throwaway new directory:git init && echo "line1\nline2\nline3\nline4\nline5\nline7\nline8" > testfile.txt && git add testfile.txt && git commit -m A && git checkout -b branch1 # ... Replace "line4" in testfile.txt with "line4 changed by branch1" git add testfile.txt && git commit -m C && git checkout -b branch2 # ... Replace "line2" in testfile.txt with "line2 changed by branch2" git add testfile.txt && git commit -m D # ... Replace "line2 changed by branch2" in testfile.txt with "line2 changed by branch2 again" git add testfile.txt && git commit -m E
At this point we have our
branch1andbranch2. This is whattestfile.txtlooks for each onetestfile.txt on branch1: testfile.txt on branch2: +--------------------------------+ +--------------------------------+ | line1 | | line1 | | line2 | | line2 changed by branch2 again | | line3 | | line3 | | line4 changed by branch1 | | line4 changed by branch1 | | line5 | | line5 | | line7 | | line7 | | line8 | | line8 | +--------------------------------+ +--------------------------------+ Our branch structure at the moment: D---E <- branch2 / C <- branch1 / A <- mainLet's amend our
testfile.txtinbranch1git checkout branch1 # ... Replace "line4 changed by branch1" in testfile.txt with "line4" # ... Replace "line7" in testfile.txt with "line7 changed by branch1" git add testfile.txt && git commit --amend -m "G" # `-m "G"` to show that this creates a separate commit. In practice I usually keep the message with `git commit --amend --no-edit`.
The current state now is
testfile.txt on branch1: testfile.txt on branch2: +--------------------------------+ +--------------------------------+ | line1 | | line1 | | line2 | | line2 changed by branch2 again | | line3 | | line3 | | line4 | | line4 changed by branch1 | | line5 | | line5 | | line7 changed by branch1 | | line7 | | line8 | | line8 | +--------------------------------+ +--------------------------------+ Our branch structure at the moment: D---E <- branch2 / C G <- branch1 |/ A <- mainIf we try to
git checkout branch2 && git rebase branch1this is what we would get:testfile.txt on branch1: testfile.txt on branch2: +--------------------------------+ +--------------------------------+ | line1 | | line1 | | line2 | | line2 changed by branch2 again | | line3 | | line3 | | line4 | | line4 changed by branch1 | | line5 | | line5 | | line7 changed by branch1 | | line7 changed by branch1 | | line8 | | line8 | +--------------------------------+ +--------------------------------+ Our branch structure: D---E <- branch2 / C | G <- branch1 / A <- mainDefinitely not what we wanted to accomplish
The problem is that doing git rebase branch1 is going to replay commit C on top of G. You don't want that. What you want is to ignore commit C and replay commits D and E on top of G, and let that be your new branch2. One way to do this is a bit more complicated, with git rebase --onto branch1 branch2~2. This replays commits D and E on top of G and the result is what you would expect:
testfile.txt on branch1: testfile.txt on branch2:
+--------------------------------+ +--------------------------------+
| line1 | | line1 |
| line2 | | line2 changed by branch2 again |
| line3 | | line3 |
| line4 | | line4 |
| line5 | | line5 |
| line7 changed by branch1 | | line7 changed by branch1 |
| line8 | | line8 |
+--------------------------------+ +--------------------------------+
Our branch structure:
D---E <- branch2
/
G <- branch1
/
A <- main
This is what branches helps with:
$ branches amend
Origin Local Age <- -> Branch Base PR
──────────────────────────────────────────────────────────
a4424 0 0 0 main
ab2b9 0 0 1 branch1
ed908 0 0 3 branch2 branch1
git add . && git commit --amend --no-edit && \
git checkout branch2 && git rebase --onto branch1 branch2~2 && \
git checkout branch1
Run update command? [y/N]
This is a trivial example. In practice it gets messier, there might be a lot more branches involved, and you might need to push the changes to origin.
I recommend setting up:
Steps:
- Create virtualenv:
pyenv virtualenv `cat .python-version` branches- Set direnv so every time you
cdinto this directory it uses the virtualenv created above:
echo 'export PYENV_VERSION=branches' > .envrc(allow if prompted)
-
Install
pip-toolswithpip install -U pip-tools. Seepip-toolsdocs. -
Install
pypidependencies:
pip-sync requirements-dev.txtTo upgrade all dependencies:
pip-compile --output-file=requirements.txt requirements.in --upgrade &&
pip-compile --output-file=requirements-dev.txt requirements-dev.in --upgradeWhen adding/removing dependencies to the .in files:
pip-compile --output-file=requirements.txt requirements.in &&
pip-compile --output-file=requirements-dev.txt requirements-dev.in &&
pip-sync requirements-dev.txtAssuming .envrc stays
pyenv virtualenv-delete -f branches &&
pyenv virtualenv `cat .python-version` branches &&
pip install -U pip-toolsNEW_PYTHON_VERSION=3.14.3
OLD_PYTHON_VERSION=$(cat .python-version)
pyenv virtualenv-delete -f branches &&
pyenv install $NEW_PYTHON_VERSION &&
echo $NEW_PYTHON_VERSION > .python-version &&
pyenv virtualenv `cat .python-version` branches &&
pip install -U pip-tools &&
pip-compile --output-file=requirements.txt requirements.in --upgrade &&
pip-compile --output-file=requirements-dev.txt requirements-dev.in --upgrade &&
pip-sync requirements-dev.txt &&
git add . && git commit -m "Upgrade python \`$OLD_PYTHON_VERSION\` -> \`$NEW_PYTHON_VERSION\` and dependencies" &&
echo "-------------------------------------------------------------" &&
echo "Remember to update .github/workflows/ci.yml" &&
echo "Remember to update https://github.com/santi-h/branches/settings/rules/11918879"To run all tests:
pytestTo run a specific file:
pytest -k test_branches.pyTo run a specific test:
pytest -k test_generate_update_commandsThis command will create all executables in dist/
scripts/build.shMake sure the VERSION in src/branches/__init__.py is updated, and then run:
VERSION=$(PYTHONPATH=src python -c "from branches import VERSION; print(VERSION)") && \
git tag v$VERSION && git push origin v$VERSION && \
open 'https://github.com/santi-h/branches/releases/new' && \
open ./dist/Then on GitHub, go to New Release and fill out the fields. Remember to upload the .tar.gz and .sha256 files.
For pre-releases, use versions with the following format examples:
VERSION=0.1.0-alphaVERSION=0.1.0-alpha.1VERSION=0.1.0-beta.3VERSION=0.1.0-rc.1
Remember the precedence:
0.1.0-alpha< 0.1.0-alpha.1< 0.1.0-beta.3< 0.1.0-rc.1 < 0.1.0
The first four are considered "pre-releases". For those, select the "Set as a pre-release" option in the Github UI.
