Skip to content

Instantly share code, notes, and snippets.

@lvh

lvh/README.md Secret

Last active June 7, 2024 23:04
Show Gist options
  • Save lvh/9e765f8999cb6d7f5a93a5abbd994588 to your computer and use it in GitHub Desktop.
Save lvh/9e765f8999cb6d7f5a93a5abbd994588 to your computer and use it in GitHub Desktop.
Bisecting a dependency of a deps.edn project

Sonmetimes, your dependencies have changing behavior resulting in bugs in your program (which may or may not be bugs upstream), and you want to find out why. This leverages deps.edn/clj's excellent local library support to bisect the dependency, even if it's (in my case, martian) a library with (1) multiple internal sub-projects (2) most of which leiningen (so you can't just :local/root and call it a day).

Here's an example run:

martian on  HEAD (e5fc36d) via :coffee: v17.0.6 
❯ git bisect start
status: waiting for both good and bad commits

We declare the current commit good:

martian on  HEAD (e5fc36d) (BISECTING) via :coffee: v17.0.6 
❯ git bisect good 
status: waiting for bad commit, 1 good commit known

We declare HEAD bad:

martian on  HEAD (e5fc36d) (BISECTING) via :coffee: v17.0.6 
❯ git bisect bad master
Bisecting: 13 revisions left to test after this (roughly 4 steps)
[20883947e630725ff1dcb0df514c017dfc4e13e0] Adding London Clojurians video link

Finally we run the below script to find the offending commit automatically:

martian on  HEAD (2088394) (BISECTING) via :coffee: v17.0.6 
❯ git bisect run -- bb --file /Users/lvh/Projects/steak-knives/bisect_dep.clj
....
51 tests, 217 assertions, 0 failures.
bec567941ef73a8e8b1444019ee54034271a1242 is the first bad commit
commit bec567941ef73a8e8b1444019ee54034271a1242
Author: Oliver Hine <git@oliy.co.uk>
Date:   Sat Jul 22 15:00:48 2023 +0100

    Terminate when vcr playback finds a response, closes #181

 vcr/src/martian/vcr.cljc | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
bisect found first bad commit%      
(ns bisect-dep
"A small thing for git bisecting a dependency."
(:require
[babashka.fs :as fs]
[babashka.process :as sh]
[babashka.deps :as deps]
[clojure.edn :as edn]))
(deps/add-deps
'{:deps {com.rpl/specter {:mvn/version "RELEASE"}}})
(require '[com.rpl.specter :as sr])
;; The dependency we're bisecting can either be a lein project or a deps.edn project.
;; If it is a lein project, we use `lein install` to build a local jar, and point this project (always a deps.edn project) to that jar. If it's a deps.edn project we can just point directly to the dependency on local disk.
(defn lein-install!
"Runs `lein install` in the given directory and returns the path to the jar file that was created."
[dir]
(let [{:keys [out]} (sh/sh {:dir (str dir)} "lein" "install")]
(when-let [jar-path (re-find #"Created (.+\.jar)" out)]
(second jar-path))))
(defn get-new-deps!
"Returns a map with the dependency spec for the given project directory."
[bisected-project]
(let [{:keys [repo-path deps]} bisected-project]
(->>
(for [{:keys [coordinate subdirectory]} deps
:let [dir (if (some? subdirectory)
(fs/path repo-path subdirectory)
repo-path)]]
[coordinate
{:local/root
(cond
(fs/exists? (fs/path dir "project.clj"))
(lein-install! dir)
(fs/exists? (fs/path dir "deps.edn"))
dir)}])
(into {}))))
(defn update-deps-edn!
[opts]
(let [deps-edn-path (-> opts :dependent-path (fs/path "deps.edn") str)
new-deps (get-new-deps! opts)]
(->>
deps-edn-path
slurp
edn/read-string
;; This will create duplicate deps entries but I think that's OK; alternatively we can first crop the new-deps with the keys from the old deps...
(sr/transform*
[(sr/multi-path
:deps
[:aliases sr/MAP-VALS
(sr/multi-path :deps :extra-deps)])
some?]
(fn [deps] (merge deps new-deps)))
pr-str
(spit deps-edn-path))))
(def opts
{:repo-path (fs/expand-home "~/Projects/martian")
:dependent-path (fs/parent *file*)
:reproducer-cmd ["bb" "test"]
:deps
[{:coordinate 'com.github.oliyh/martian
:subdirectory "core"}
{:coordinate 'com.github.oliyh/martian-hato
:subdirectory "hato"}
{:coordinate 'com.github.oliyh/martian-vcr
:subdirectory "vcr"}]})
(update-deps-edn! opts)
(apply sh/shell {:dir (opts :dependent-path)} (opts :reproducer-cmd))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment