gomodgraph
A short example that loads the output of go mod graph into gograph and answers a few
questions about a module's requirements. It only uses gograph and the standard library.
go mod graph | go run github.com/hmdsefi/gograph/examples/gomodgraph@latest cycles
Each line of go mod graph is from to, meaning from requires to. The example adds an
edge from -> to for each line, so the dependencies of a module are the vertices it can
reach, and the modules that depend on it are the vertices that can reach it. That's the
opposite of a build graph, where an edge A -> B means A has to happen before B.
Lines for the go and toolchain versions are skipped.
Commands
A <module> is either a module path, which matches every version, or path@version. The
outputs below come from testdata/modgraph.txt.
cycles prints the module versions that require each other, one group per line. It uses
connectivity.Tarjan:
$ gomodgraph -f testdata/modgraph.txt cycles
example.com/codec@v0.5.0 example.com/util@v0.3.1
dependents <module> prints every module that requires a version of <module>, directly
or indirectly. It uses dag.Ancestors: the modules that can reach a module are the ones
that require it.
$ gomodgraph -f testdata/modgraph.txt dependents example.com/util@v0.2.0
example.com/app
example.com/log@v1.1.0
why <module> prints the shortest requirement path from the main module, in the format of
go mod why -m:
$ gomodgraph -f testdata/modgraph.txt why example.com/codec
# example.com/codec
example.com/app
example.com/lib@v1.4.0
example.com/util@v0.3.1
example.com/codec@v0.5.0
versions [module] groups the module versions by path and prints each path that the
graph has at more than one version, or only the path of <module>. go mod graph
lists every version that some module requires, and minimal version selection builds the
highest one, so that's the version marked with *. Each version lists the modules that
require it directly, which shows the requirement that sets it:
$ gomodgraph -f testdata/modgraph.txt versions
example.com/util
v0.2.0 required by example.com/log@v1.1.0
* v0.3.1 required by example.com/codec@v0.5.0, example.com/lib@v1.4.0
A main module builds from its own code. When a dependency requires a version of it, its
path is printed with (main module), and none of those versions is marked.
diff <old> <new> compares two saved outputs of go mod graph and prints the module
versions and requirements that were removed (-) or added (+):
$ gomodgraph diff testdata/modgraph.txt testdata/modgraph_new.txt
- module example.com/lib@v1.4.0
- module example.com/log@v1.1.0
- module example.com/util@v0.2.0
+ module example.com/lib@v1.5.0
+ module example.com/metrics@v0.1.0
- require example.com/app example.com/lib@v1.4.0
...
diff -selected <old> <new> compares the version of each module path that builds:
- for a module that left the build, + for a new one, and ~ for a version that
changed. It matches what go list -m all reports on the two commits:
$ gomodgraph diff -selected testdata/modgraph.txt testdata/modgraph_new.txt
- example.com/log v1.1.0
+ example.com/metrics v0.1.0
~ example.com/lib v1.4.0 -> v1.5.0
The plain diff lists - module example.com/util@v0.2.0 for the same change, while
example.com/util builds at v0.3.1 before and after. On a large module the gap is
wider: between Prometheus v3.14.0 and v3.15.0, diff prints 3,488 lines and
diff -selected prints 171.
mermaid <module> draws the module, the modules it requires directly, and the modules
that require it directly. encoding/mermaid writes the picture as a flowchart, so it can
sit in a pull request or a doc. The command leaves out the rest of the graph, which is
usually too large for a diagram.
$ gomodgraph -f testdata/modgraph.txt mermaid example.com/app
flowchart TD
n0["example.com/app"]
n1["example.com/lib@v1.4.0"]
n2["example.com/log@v1.1.0"]
n0 --> n1
n0 --> n2
flowchart TD
n0["example.com/app"]
n1["example.com/lib@v1.4.0"]
n2["example.com/log@v1.1.0"]
n0 --> n1
n0 --> n2
This workflow runs go mod graph on the base and head commits of a pull request and posts
the diff -selected output as a comment, so the comment lists the modules whose version
in the build changed. Pull requests from forks get a read-only token, so the comment step
only works for branches in the same repository.
name: Module graph
on:
pull_request:
paths: [go.mod, go.sum]
permissions:
contents: read
pull-requests: write
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Compare module graphs
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
git checkout -q "$BASE" && go mod graph > "$RUNNER_TEMP/old.txt"
git checkout -q "$HEAD" && go mod graph > "$RUNNER_TEMP/new.txt"
{
echo '```diff'
go run github.com/hmdsefi/gograph/examples/gomodgraph@latest \
diff -selected "$RUNNER_TEMP/old.txt" "$RUNNER_TEMP/new.txt"
echo '```'
} > "$RUNNER_TEMP/comment.md"
- name: Comment
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: gh pr comment "$PR" --body-file "$RUNNER_TEMP/comment.md"