I have a pretty standardized workflow at my day job. I log on, check Jira, and start banging away. I fork a new branch off of our development branch (conveniently named develop
) for every new ticket I'm assigned. I like doing things this way, because it keeps the history clean and makes merges and code review easier. (If you've ever had to pull out a feature that's been merged in piecemeal, I'm sure you'll agree.)
This may strike you as a fairly rote series of steps; it certainly struck me as such, so I decided to try writing a custom Git command to help automate it. You can add a custom command to Git by creating an executable file on your $PATH
that's named git-$COMMAND_NAME
. Git will strip off the git-
prefix when you invoke it, e.g. git-my-command
will be runnable as git my-command
.
My first attempt was a simple Bash script:
#! /usr/bin/sh
new_branch() {
git switch develop
git pull origin develop
git switch -c "$1"
}
new_branch "$@"
I made a .git-commands
folder in my home directory and saved this as git-new-branch
. I made the file executable and added ~/.git-commands
to my $PATH
.
It worked. Pretty well.
There were two pain points. First, my editor (Neovim + CoC) didn't pick up on the fact that it was a Bash script, so I didn't get any linting/syntax highlighting/debugging help. I have coc-sh
and shellcheck
set up, but they were silent. (It is here that I must admit that what I shared above is not the original version, which didn't actually work.) The solution to this issue was to rename the file to git-new-branch.sh
. Fine, but then Git thought the command name was new-branch.sh
, which sucks. The solution to that was a simple ln -s ~/.git-commands/git-new-branch.sh ~/.git-commands/git-new-branch
. Easy enough.
The second problem was trickier. You see, I'm not a very good developer. As such, tickets I'm assigned are frequently re-opened in the course of the QA process. (A hearty shout-out to QA people the world over, and especially at my company, for their tireless diligence and infinite patience.) It's my practice in such situations to fork a new branch off of develop
with the same ticket name plus a letter. So, should ticket 1234
be re-opened (after the original was merged into develop
and deployed to our development server for QA), I'd open 1234-b
, 1234-c
, etc. My new command didn't handle this. The issue wasn't disastrous—I'd just get a fresh pull of develop
and then a chiding from Git about the pre-existing branch name. I could probably just remember that it was a reopened ticket and add the letters myself, but again, tickets are sometimes re-opened multiple times, and the whole point of this exercise is making my life easier.
There's got to be a better way!
I tried fiddling around with sed
and managed to isolate the suffix, but then ran into some limitations, both around Bash's character-handling deficiencies and (more saliently) my own Bash-handling deficiencies, specifically around how to "increment" a character—i.e. going from b
to c
.
Python to the rescue:
#! /usr/bin/env python3
import re
from subprocess import run
from sys import argv, exit
def branches_list():
return run(
["git", "--no-pager", "branch", "--list"], capture_output=True, text=True
).stdout
def next_branch(branch_name):
p = f"{branch_name}(?:-(\\w))?"
matches = re.findall(p, branches_list())
if len(matches) == 0:
return branch_name
if len(matches) == 1 and matches[0] == "":
return f"{branch_name}-b"
return f"{branch_name}-{chr(ord(matches[-1]) + 1)}"
def main():
if len(argv) != 2:
print("Please supply a branch name")
exit(1)
branch_name = argv[1]
run(["git", "switch", "develop"])
run(["git", "pull", "origin", "develop"])
run(["git", "switch", "-c", next_branch(branch_name)])
exit(0)
if __name__ == "__main__":
main()
(N.B.: subprocess.run
was introduced in 3.5; if you're using an older version you'll need to fall back to subprocess.check_output
; see the docs for more.)
I threw this into new_branch.py
, marked it as executable, deleted the symbolic link to the Bash version and created a new one to this & voilà!
As much as it can feel (to me, at least) like a crutch sometimes, Python legitimately rules for this kind of thing. It was born as a "glue language," and has all the necessary batteries included. I could have reached for Haskell or Rust (Haskell's Ord
instance for Char
especially would've made some of this nicer) but I didn't want to mess with compiling a "production" build and then figuring out where Stack or Cargo (respectively) put it and so on. (Yes, I could've written a Stack script but I don't really know how to do that and didn't want to bother.)
I guess the lessons here are:
- Custom Git commands aren't that hard to write, and we (I) should probably be writing more of them.
- While professional development and personal growth are worthwhile goals, sometimes the easy path is the right one.
(One last unsolved irritant: some not-terribly-thorough Googling failed to turn up a way to add Git commands per-repo, as opposed to globally. If anyone has a solution for this, please let me know.)
If anyone's got any custom Git commands they're particularly proud of, feel free to drop them in the comments!
Top comments (0)