
Clean up outdated git branches with Babashaka
Problem to solve
The pull request is the de-facto way for code collaboration in the multi-member team nowadays.
It's based on git, as git is a distributed
version control system so you will not have only one place to manage your code, usually it involves at least two environments, the local repository and the remote one(github or bitbucket for example)
In my team the git flow is roughly like below, it's been proved to be solid enough for quite a lot cross team projects.
However I have a little annoyning task about branch clean up in the flow. If you are sharp-eyed, you will see after the step 6, the feature branches are automatically deleted once they are merge (by setting the Github preference), the feature branch remains in the local environment, certainly you can remove it with git branch --delete
but as programmers we are not good at remembering things, so if it's forgotten there will be outdated branches left in the local git repository.
Again, as programers we are good at automation, so I built a script for the clean up. Bash shall do the work but I haven't used bash for a long time, and sometimes the bash goes too complicated for a simple small task.
Solution
I chose to build a Babasha script, Babash is a lightweight Clojure interpreter for scripting, it exploits the powerful facet of data process from clojure.
The solution is quiet straightforward, getting both local and remote branches, figure out those only remain in local, they are the potential candiates to be removed.
Why I said
potential
? Because sometimes you will have a diverged branch in your local git environment, for instance, you probably have a feature testing branch that is diverged from the main branch, the script needs to keep these branches untouched.
So I add the commits diff check to the flow, i.e. checking the result git log branch_A..branch_B
is empyt or not.
Once I get all the outdated branch just iterate them with git branch --delete
.
Finally I can run the script like this,
# use `:m` to specify the name of main branch
$ gbd.clj :m master
Branch: [defer-publish] has been removed
Branch: [feature-close-connection] has been removed
Branch: [feature-produce-retry] has been removed
Branch: [feature-singleton-producer] has been removed
Branch: [fix-ci-build-error] has been removed
Code
The code of the script is as follows, you can also find it from Github Gist
#! /usr/bin/env bb
(require '[babashka.process :refer [shell]])
(require '[clojure.string :refer [includes? split-lines trim]])
(require '[babashka.cli :as cli])
(def cli-options {:main {:default "main" :alias :m :desc "the main branch name" }
:help {:coerce :boolean}})
(def cli-opt (cli/parse-opts *command-line-args* {:spec cli-options}))
(defn lazy-contains? [col key]
(boolean (some #(= % key) col)))
(defn behind? [target main]
( -> (shell {:out :string } (format "git log %s..%s" main target)) :out empty?))
(defn not_head [n]
(not (includes? n "HEAD")))
(defn git_rm [branch]
(shell {:out :string } (format "git branch -d %s" branch))
(println (format "Branch: [%s] has been removed" branch)))
(def locals (->> (shell {:out :string } "git branch") :out split-lines))
(def normalized (->> locals (map #(if (includes? % "*") (subs % (count "* ")) %))))
(if (not (lazy-contains? normalized (:main cli-opt)))
(println (format "Branch [%s] does not exit" (:main cli-opt) ))
(let [remotes_fullname ( -> (shell {:out :string } "git branch -r") :out split-lines )
remote_branches (->> remotes_fullname (filter not_head) (map #(subs (trim %) (count "origin/"))))
local_candidates (filter #(not (includes? % "*")) locals)
to_remove (filter #(and (not (lazy-contains? remote_branches %)) (behind? % (:main cli-opt))) (map #(trim %) local_candidates))
]
(doseq [to_rm to_remove]
(git_rm to_rm) )))