Full Code of jvns/git-workflow for AI

main 935d073cd5a6 cached
17 files
19.9 KB
6.0k tokens
20 symbols
1 requests
Download .txt
Repository: jvns/git-workflow
Branch: main
Commit: 935d073cd5a6
Files: 17
Total size: 19.9 KB

Directory structure:
gitextract_a3ambk43/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── fly-deploy.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.py
├── commands.txt
├── fly.toml
├── git-workflow.py
├── requirements.txt
├── schema.sql
├── static/
│   └── style.css
└── templates/
    ├── base.html
    ├── display_graph.html
    ├── index.html
    └── review_commands.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
venv
*.db


================================================
FILE: .github/workflows/fly-deploy.yml
================================================
# From https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy
on:
  push:
    branches:
      - main
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    concurrency: deploy-group
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}


================================================
FILE: .gitignore
================================================
*.pyc
git_workflow.db


================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1

ARG PYTHON_VERSION=3.13.6

FROM python:${PYTHON_VERSION}-slim

LABEL fly_launch_runtime="flask"

RUN apt-get update && apt-get install -y graphviz && apt-get install -y fonts-inconsolata

WORKDIR /code

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .

EXPOSE 8080

CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]


================================================
FILE: LICENSE
================================================
Copyright (c) 2013 Julia Evans

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
Visualize your Git!
===================

Source for http://visualize-your-git.herokuapp.com

It makes pictures that look like this:

![](http://i.imgur.com/ezMmOXv.png)


Stand-alone version
-------------------

```
pip install -r requirements.local.txt
history | python git-workflow.py > graph.svg
```

See ``templates/index.html`` or the Website for an explanation of how it works.


================================================
FILE: app.py
================================================
from flask import (
    Flask,
    render_template,
    jsonify,
    request,
    g,
    redirect,
    url_for,
    Response,
)
import pandas as pd
import graphviz
import numpy as np
from io import StringIO
import sqlite3
import os
from nanoid import generate


app = Flask(__name__)
app.config["DEBUG"] = True


def load_valid_commands():
    with open("commands.txt", "r") as f:
        return set(line.strip() for line in f)


VALID_GIT_COMMANDS = load_valid_commands()


@app.route("/")
def hello():
    return render_template("index.html")


@app.route("/review/<log_id>")
def review_commands(log_id):
    cursor = g.conn.cursor()
    cursor.execute(
        "SELECT DISTINCT command FROM entries WHERE log_id = ? AND valid = 0", (log_id,)
    )
    invalid_commands = [row[0] for row in cursor.fetchall()]
    return render_template(
        "review_commands.html", log_id=log_id, invalid_commands=invalid_commands
    )


@app.route("/review/<log_id>/clean", methods=["POST"])
def clean_commands(log_id):
    cursor = g.conn.cursor()
    cursor.execute("DELETE FROM entries WHERE log_id = ? AND valid = 0", (log_id,))
    g.conn.commit()
    return redirect(url_for("display_graph", num=log_id))


@app.route("/review/<log_id>/keep", methods=["POST"])
def keep_commands(log_id):
    return redirect(url_for("display_graph", num=log_id))


@app.route("/display/<num>")
def display_graph(num=None):
    return render_template("display_graph.html", num=num)


@app.route("/image/<num>/git-workflow.png")
def serve_image(num=None):
    return serve_image_inner(num)

@app.route("/image/sparse/<num>/git-workflow.png")
def serve_image_sparse(num=None):
    return serve_image_inner(num, sparse=True)

def serve_image_inner(num, sparse=False):
    cursor = g.conn.cursor()
    cursor.execute(
        "SELECT row_number, command FROM entries WHERE log_id = ? ORDER BY row_number",
        (num,),
    )
    entries = cursor.fetchall()
    if not entries:
        return "Graph is empty!", 404
    image = create_image(entries, format='png', sparse=sparse)
    return Response(image, mimetype="image/png")

@app.route("/graph", methods=["POST"])
def get_image():
    history = request.form["history"]
    log_id = save_history(history)

    # Delete invalid commands that only appear once or start with dash
    # (probably typos or flags)
    cursor = g.conn.cursor()
    cursor.execute(
        """
        DELETE FROM entries
        WHERE log_id = ? AND valid = 0
        AND (
            command = 'git'
            OR command LIKE '-%'
            OR command IN (
                SELECT command
                FROM entries
                WHERE log_id = ? AND valid = 0
                GROUP BY command
                HAVING COUNT(*) = 1
            )
        )
    """,
        (log_id, log_id),
    )
    g.conn.commit()

    # Check if there are still invalid commands
    cursor.execute(
        "SELECT COUNT(*) FROM entries WHERE log_id = ? AND valid = 0", (log_id,)
    )
    invalid_count = cursor.fetchone()[0]

    if invalid_count > 0:
        return redirect(url_for("review_commands", log_id=log_id))
    else:
        return redirect(url_for("display_graph", num=log_id))


def save_history(history):
    cursor = g.conn.cursor()
    log_id = generate(
        alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
        size=10,
    )
    cursor.execute("INSERT INTO logs (id) VALUES (?);", (log_id,))

    lines = [l for l in history.split("\n") if len(l.strip()) > 0]
    for line in lines:
        parts = line.strip().split(" ", 1)
        if len(parts) != 2:
            continue
        row_number, command = parts
        cursor.execute(
            "INSERT INTO entries (log_id, row_number, command, valid) VALUES (?, ?, ?, ?);",
            (log_id, int(row_number), command, command in VALID_GIT_COMMANDS),
        )

    g.conn.commit()
    return log_id


def build_colorscheme(nodes):
    # colorbrewer Dark2 color scheme + lighter fill colours
    color_palette = [
        {"color": "#3b82f6", "fill": "#eff6ff"},
        {"color": "#f59e0b", "fill": "#fffbeb"},
        {"color": "#ec4899", "fill": "#fdf2f8"},
        {"color": "#8b5cf6", "fill": "#f5f3ff"},
        {"color": "#10b981", "fill": "#f0fdf4"},
        {"color": "#f97316", "fill": "#fff7ed"},
        {"color": "#64748b", "fill": "#f8fafc"},
        {"color": "#ef4444", "fill": "#fef2f2"},
    ]
    return {node: color_palette[i % len(color_palette)] for i, node in enumerate(nodes)}

def create_image(entries, format, sparse):
    pair_counts, node_totals = get_statistics(entries, sparse)
    return create_image_inner(pair_counts, node_totals, format)

def create_image_inner(pair_counts, node_totals, format="svg"):
    dot = graphviz.Digraph()
    dot.attr(
        rankdir="TB",
        bgcolor="#fef5e7",
        pad="0.2",
        fontname="Arial,Helvetica,system-ui,sans-serif",
        fontsize="12",
        fontcolor="#656d76",
        label="Visualize Your Git: gitviz.jvns.ca",
        labelloc="b",
        labeljust="r",
        dpi="200"
    )

    # Make a subgraph for aesthetics
    graph = graphviz.Digraph(name='cluster_main')
    graph.attr(
        style="filled,rounded",
        fillcolor="white",
        pencolor="#f6ad55",
        penwidth="1.5",
        margin="15",
        label=""
    )

    # Extract nodes
    nodes = set()
    for (frm, to), row in pair_counts.iterrows():
        nodes.add(frm)
        nodes.add(to)
    # Sort to make colorscheme deterministic
    nodes = list(sorted(nodes))

    node_colors = build_colorscheme(nodes)

    # Add nodes
    for node in nodes:
        size = np.sqrt(node_totals[node])
        size /= float(sum(np.sqrt(node_totals)))
        size *= 6
        size = max(size, 0.1)
        size = min(size, 4)
        width = max(size, 0.7)

        percentage = int(node_totals[node] / float(sum(node_totals)) * 100)

        graph.node(
            node,
            shape="box",
            style="filled,rounded",
            fontname="Inconsolata, monospace",
            fillcolor=node_colors[node]["fill"],
            color=node_colors[node]["color"],
            label=f"{node}\\n{percentage}%",
            width=str(width),
            height=str(size),
            fontsize="12",
            penwidth="1.5",
            margin="0.15"
        )

    # Add edges
    total_count = np.sum(pair_counts["count"])
    for (frm, to), row in pair_counts.iterrows():
        size = row["count"]
        penwidth = min(float(size) / total_count * 60, 10)
        arrowsize = 1.0
        if penwidth  > 5:
            arrowsize = 0.1
        graph.edge(
            frm,
            to,
            penwidth=str(penwidth),
            color=node_colors[frm]["color"],
            arrowsize=str(arrowsize),
        )

    # Add the cluster to the main graph
    dot.subgraph(graph)

    return dot.pipe(format=format, encoding="utf-8" if format == "svg" else None)


def get_statistics(entries, sparse=False):
    df = pd.DataFrame(entries, columns=["row", "command"]).set_index("row")
    pairs = pd.DataFrame(index=range(len(df) - 1))
    pairs["dist"] = df.index[1:].values - df.index[:-1].values
    pairs["from"] = df["command"][:-1].values
    pairs["to"] = df["command"][1:].values
    node_totals = df["command"].value_counts()
    close_pairs = pairs[pairs.dist == 1]
    pair_counts = (
        close_pairs.groupby(["from", "to"])
        .aggregate(len)
        .rename(columns={"dist": "count"})
    )

    pair_counts = pair_counts.sort_values("count", ascending=False)
    total_count = float(np.sum(pair_counts["count"]))

    # In sparse mode, only include transitions that happen at
    # least 1% of the time
    min_count = 1
    if sparse:
        min_count = total_count / 100
    elif total_count >= 1000:
        min_count = 5
    pair_counts = pair_counts[pair_counts["count"] >= min_count]

    return pair_counts, node_totals


@app.before_request
def before():
    g.conn = db_connect()


@app.teardown_request
def teardown_request(exception):
    db = getattr(g, "db", None)
    if db is not None:
        db.close()


def db_connect():
    db_path = os.environ.get("DATABASE_PATH", "git_workflow.db")
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row

    # Create tables if they don't exist
    with open("schema.sql", "r") as f:
        schema = f.read()
    conn.executescript(schema)
    conn.commit()

    return conn


if __name__ == "__main__":
    app.run(port=5001)


================================================
FILE: commands.txt
================================================
add
am
archive
backfill
bisect
branch
bundle
checkout
cherry-pick
citool
clean
clone
commit
describe
diff
fetch
format-patch
gc
gitk
grep
gui
init
log
maintenance
merge
mv
notes
pull
push
range-diff
rebase
reset
restore
revert
rm
scalar
shortlog
show
sparse-checkout
stash
status
submodule
switch
tag
worktree
config
fast-export
fast-import
filter-branch
mergetool
pack-refs
prune
reflog
refs
remote
repack
replace
annotate
blame
bugreport
count-objects
diagnose
difftool
fsck
gitweb
help
instaweb
merge-tree
rerere
show-branch
verify-commit
verify-tag
version
whatchanged
archimport
cvsexportcommit
cvsimport
cvsserver
imap-send
p4
quiltimport
request-pull
send-email
svn
apply
checkout-index
commit-graph
commit-tree
hash-object
index-pack
merge-file
merge-index
mktag
mktree
multi-pack-index
pack-objects
prune-packed
read-tree
replay
symbolic-ref
unpack-objects
update-index
update-ref
write-tree

Low-level
cat-file
cherry
diff-files
diff-index
diff-pairs
diff-tree
for-each-ref
for-each-repo
get-tar-commit-id
ls-files
ls-remote
ls-tree
merge-base
name-rev
pack-redundant
rev-list
rev-parse
show-index
show-ref
unpack-file
var
verify-pack

Low-level
daemon
fetch-pack
http-backend
send-pack
update-server-info

Low-level
check-attr
check-ignore
check-mailmap
check-ref-format
column
credential
credential-cache
credential-store
fmt-merge-msg
hook
interpret-trailers
mailinfo
mailsplit
merge-one-file
patch-id
sh-i18n
sh-setup
stripspace

User-facing
attributes
cli
hooks
ignore
mailmap
modules
repository-layout
revisions

Developer-facing
format-bundle
format-chunk
format-commit-graph
format-index
format-pack
format-signature
protocol-capabilities
protocol-common
protocol-http
protocol-pack
protocol-v2
absorb
commit-folders
lb
oops
preview-pr
remote-update-time
review
sw
vee
ci
co
lg


================================================
FILE: fly.toml
================================================
# fly.toml app configuration file generated for git-workflow on 2025-08-29T16:14:36-04:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'git-workflow'
primary_region = 'ewr'

[build]

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

[mounts]
  source = 'visualize_git_db'
  destination = '/data'

[env]
  DATABASE_PATH = '/data/git_workflow.db'


================================================
FILE: git-workflow.py
================================================
#!/usr/bin/env python3

import sys
from app import create_image

if __name__ == "__main__":
    entries = []
    for line in sys.stdin:
        if 'git ' in line:
            parts = line.strip().split()
            if len(parts) >= 3 and parts[1] == 'git':
                entries.append((int(parts[0]), parts[2]))

    if entries:
        svg = create_image(entries, format='svg', sparse=False)
        print(svg if svg else "<!-- No data to visualize -->")


================================================
FILE: requirements.txt
================================================
Flask==3.1.2
graphviz==0.21
gunicorn==23.0.0
numpy==2.3.2
pandas==2.3.2
pydot==4.0.1
nanoid==2.0.0


================================================
FILE: schema.sql
================================================
CREATE TABLE IF NOT EXISTS logs (
    id TEXT PRIMARY KEY,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS entries (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    log_id TEXT NOT NULL,
    row_number INTEGER NOT NULL,
    command TEXT NOT NULL,
    valid INTEGER NOT NULL,
    FOREIGN KEY (log_id) REFERENCES logs (id)
);


================================================
FILE: static/style.css
================================================
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
html,
body,
p,
ol,
ul,
li,
dl,
dt,
dd,
blockquote,
figure,
fieldset,
legend,
textarea,
pre,
iframe,
hr,
h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 0;
  padding: 0
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: 100%;
  font-weight: normal
}

ul {
  list-style: none
}

button,
input,
select {
  margin: 0
}

html {
  box-sizing: border-box
}

*,
*::before,
*::after {
  box-sizing: inherit
}

img,
video {
  height: auto;
  max-width: 100%
}

iframe {
  border: 0
}

table {
  border-collapse: collapse;
  border-spacing: 0
}

td,
th {
  padding: 0
}

/* done with reset */

:root {
  --bg-color: #ed8936;
  --link-color: #bd7335;
  /* better contrast */
}

html {
  font-family: sans-serif;
  font-size: 18px;
}

nav {
  padding: 1.5rem;
  background-color: var(--bg-color);
  display: flex;
  justify-content: space-between;
  color: white;
  box-shadow: 2px 2px black;
}

nav a {
  color: white;
  text-decoration: none;
}

nav h1 {
  font-size: 1.2rem;
  font-weight: bold;
}

nav ul {
  display: flex;
  align-items: center;
}

nav img {
  width: 1rem;
}

nav li {
  margin: 0 .5rem;
}

main {
  max-width: 800px;
  margin: 2rem auto;
}

p, .tabs, form, div, button {
  margin: 1rem 0;
}

body {
  margin: .5rem;
}

pre {
  background: #eee;
  padding: .75rem;
  border: 2px solid black;
  white-space: pre-wrap;
  word-break: keep-all;
}

button {
  background-color: var(--bg-color);
  color: white;
  padding: .5rem 1rem;
  box-shadow: 2px 2px black;
  border: none;
  outline: none;
  font-size: 1.2rem;
  margin-right: 1rem;
  cursor: pointer;
}

button:focus {
  box-shadow: none;
}

textarea {
  width: 100%;
  height: 200px;
  padding: .5rem;
  font-family: sans-serif;
}

h2 {
  margin-top: 2rem;
  font-size: 1.5rem;
}

.image {
  text-align: center;
}

.sparse img {
  max-width: 400px;
}

a {
  color: var(--link-color);
}

ul.invalid {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem;
}

ul.invalid li {
  margin: .25rem;
  padding: .25rem;
  border-radius: 5px;
  background-color: #eee;
}

/* tabs */
/* https://dev.to/joxx/building-a-tab-component-with-pure-css-using-radio-and-label-tags-200b */

.tabs {
  display: flex;
  flex-wrap: wrap;
}

.tabs label {
    background: #eee;

  cursor: pointer;
  display: block;
  font-weight: 600;
  order: initial;
  padding: .5rem 1rem;
  transition: background ease 0.3s;
  width: 100%;
  border: 2px solid black;
  border-bottom: 0;
}

.tabs .tab-content {
  display: none;
  flex-grow: 1;
  padding: 1rem;
  width: 100%;
}

.tabs input[type="radio"] {
  display: none;
}

.tabs input[type="radio"]:checked+label {
  background: var(--bg-color);
  color: white;
}

.tabs input[type="radio"]:checked+label+.tab-content {
  display: block;
}

.tabs .tab-content {
  order: 99
}

.tabs label {
  order: 1;
}

.tabs label {
  margin-right: 0.3rem;
  margin-top: 0;
  width: auto;
}


================================================
FILE: templates/base.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Visualize Your Git!</title>
    <link rel="stylesheet" href="/static/style.css">
    {% block meta %}{% endblock %}
</head>
<body>
    <header>
        <nav>
            <h1><a href="/">Visualize Your Git!</a></h1>
            <ul>
                <li><a href="http://jvns.ca">By Julia Evans</a></li>
                <li><a href="http://github.com/jvns/git-workflow"><img src="/static/images/GitHub-Mark-32px.png" alt="GitHub"></a></li>
            </ul>
        </nav>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>


================================================
FILE: templates/display_graph.html
================================================
{% extends "base.html" %}

{% block meta %}
    <meta property="og:image" content="https://gitviz.jvns.ca{{ url_for('serve_image_sparse', num=num) }}">
    <meta property="og:type" content="website">
    <meta property="og:description" content="A visualization of you use Git">
{% endblock %}

{% block content %}
<p>
  An arrow from 'commit' to 'push' means that you did 'git commit' right before
  'git push'. Thicker arrows happened more times.
</p>

<h2>
  Sparse version <small>(only has the most used arrows)</small>
</h2>

<div class="image sparse">
  <img src="/image/sparse/{{ num }}/git-workflow.png">
</div>

<h2> Full version </h2>
<div class="image">
  <img src="/image/{{ num }}/git-workflow.png">
</div>
{% endblock %}


================================================
FILE: templates/index.html
================================================
{% extends "base.html" %}

{% block content %}
<p>
  Hello! You can use this tool to visualize how you use Git!
</p>

<p>
  First, run this command. If you have
  bash aliases, try replacing <code>history</code> with <a href="https://gist.github.com/mwhite/7509467">this
    script</a>.
</p>

<div class="tabs">
    <input type="radio" name="tab" id="bash" checked>
    <label for="bash">Bash</label>
    <pre class="tab-content">history | awk '$2 == "git" {print $1 " " $3}' > history.txt</pre>

    <input type="radio" name="tab" id="zsh">
    <label for="zsh">ZSH</label>
    <pre class="tab-content">history 0 | awk '$2 == "git" {print $1 " " $3}' > history.txt</pre>

    <input type="radio" name="tab" id="fish">
    <label for="fish">Fish</label>
    <pre class="tab-content">history | nl -w1 -s' ' | awk '$2 == "git" {print $1 " " $3}' > history.txt</pre>
</div>



<p>
  Next, paste the results! You'll get a graph of which commands you use
  after other commands. For example, if you always push after committing,
  there will be a big arrow from 'commit' to 'push'.
</p>

<form action="/graph" method="POST">
  <textarea name="history" placeholder="Paste your git history here..."></textarea>
  <button type="submit">Submit</button>
</form>
{% endblock %}


================================================
FILE: templates/review_commands.html
================================================
{% extends "base.html" %}

{% block content %}
<h2>Are these typos or aliases?</h2>

<p>
  There are a few Git subcommands that don't appear to be valid:
</p>

<ul class="invalid">
  {% for command in invalid_commands %}
  <li><code>{{ command }}</code></li>
  {% endfor %}
</ul>

<p>
  Do you want to remove them from your visualization, or keep them?
</p>

<form action="/review/{{ log_id }}/clean" method="POST" style="display: inline;">
  <button type="submit">Remove them</button>
</form>

<form action="/review/{{ log_id }}/keep" method="POST" style="display: inline;">
  <button type="submit">Keep them</button>
</form>
{% endblock %}
Download .txt
gitextract_a3ambk43/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── fly-deploy.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.py
├── commands.txt
├── fly.toml
├── git-workflow.py
├── requirements.txt
├── schema.sql
├── static/
│   └── style.css
└── templates/
    ├── base.html
    ├── display_graph.html
    ├── index.html
    └── review_commands.html
Download .txt
SYMBOL INDEX (20 symbols across 2 files)

FILE: app.py
  function load_valid_commands (line 24) | def load_valid_commands():
  function hello (line 33) | def hello():
  function review_commands (line 38) | def review_commands(log_id):
  function clean_commands (line 50) | def clean_commands(log_id):
  function keep_commands (line 58) | def keep_commands(log_id):
  function display_graph (line 63) | def display_graph(num=None):
  function serve_image (line 68) | def serve_image(num=None):
  function serve_image_sparse (line 72) | def serve_image_sparse(num=None):
  function serve_image_inner (line 75) | def serve_image_inner(num, sparse=False):
  function get_image (line 88) | def get_image():
  function save_history (line 127) | def save_history(history):
  function build_colorscheme (line 150) | def build_colorscheme(nodes):
  function create_image (line 164) | def create_image(entries, format, sparse):
  function create_image_inner (line 168) | def create_image_inner(pair_counts, node_totals, format="svg"):
  function get_statistics (line 252) | def get_statistics(entries, sparse=False):
  function before (line 282) | def before():
  function teardown_request (line 287) | def teardown_request(exception):
  function db_connect (line 293) | def db_connect():

FILE: schema.sql
  type logs (line 1) | CREATE TABLE IF NOT EXISTS logs (
  type entries (line 6) | CREATE TABLE IF NOT EXISTS entries (
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (23K chars).
[
  {
    "path": ".dockerignore",
    "chars": 10,
    "preview": "venv\n*.db\n"
  },
  {
    "path": ".github/workflows/fly-deploy.yml",
    "chars": 442,
    "preview": "# From https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/\nname: Fly Deploy\non:\n  push:\n    branch"
  },
  {
    "path": ".gitignore",
    "chars": 22,
    "preview": "*.pyc\ngit_workflow.db\n"
  },
  {
    "path": "Dockerfile",
    "chars": 387,
    "preview": "# syntax=docker/dockerfile:1\n\nARG PYTHON_VERSION=3.13.6\n\nFROM python:${PYTHON_VERSION}-slim\n\nLABEL fly_launch_runtime=\"f"
  },
  {
    "path": "LICENSE",
    "chars": 1055,
    "preview": "Copyright (c) 2013 Julia Evans\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis sof"
  },
  {
    "path": "README.md",
    "chars": 384,
    "preview": "Visualize your Git!\n===================\n\nSource for http://visualize-your-git.herokuapp.com\n\nIt makes pictures that look"
  },
  {
    "path": "app.py",
    "chars": 8527,
    "preview": "from flask import (\n    Flask,\n    render_template,\n    jsonify,\n    request,\n    g,\n    redirect,\n    url_for,\n    Resp"
  },
  {
    "path": "commands.txt",
    "chars": 1796,
    "preview": "add\nam\narchive\nbackfill\nbisect\nbranch\nbundle\ncheckout\ncherry-pick\ncitool\nclean\nclone\ncommit\ndescribe\ndiff\nfetch\nformat-p"
  },
  {
    "path": "fly.toml",
    "chars": 582,
    "preview": "# fly.toml app configuration file generated for git-workflow on 2025-08-29T16:14:36-04:00\n#\n# See https://fly.io/docs/re"
  },
  {
    "path": "git-workflow.py",
    "chars": 460,
    "preview": "#!/usr/bin/env python3\n\nimport sys\nfrom app import create_image\n\nif __name__ == \"__main__\":\n    entries = []\n    for lin"
  },
  {
    "path": "requirements.txt",
    "chars": 99,
    "preview": "Flask==3.1.2\ngraphviz==0.21\ngunicorn==23.0.0\nnumpy==2.3.2\npandas==2.3.2\npydot==4.0.1\nnanoid==2.0.0\n"
  },
  {
    "path": "schema.sql",
    "chars": 365,
    "preview": "CREATE TABLE IF NOT EXISTS logs (\n    id TEXT PRIMARY KEY,\n    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n)"
  },
  {
    "path": "static/style.css",
    "chars": 2907,
    "preview": "/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */\nhtml,\nbody,\np,\nol,\nul,\nli,\ndl,\ndt,\ndd,\nblock"
  },
  {
    "path": "templates/base.html",
    "chars": 717,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "templates/display_graph.html",
    "chars": 734,
    "preview": "{% extends \"base.html\" %}\n\n{% block meta %}\n    <meta property=\"og:image\" content=\"https://gitviz.jvns.ca{{ url_for('ser"
  },
  {
    "path": "templates/index.html",
    "chars": 1267,
    "preview": "{% extends \"base.html\" %}\n\n{% block content %}\n<p>\n  Hello! You can use this tool to visualize how you use Git!\n</p>\n\n<p"
  },
  {
    "path": "templates/review_commands.html",
    "chars": 642,
    "preview": "{% extends \"base.html\" %}\n\n{% block content %}\n<h2>Are these typos or aliases?</h2>\n\n<p>\n  There are a few Git subcomman"
  }
]

About this extraction

This page contains the full source code of the jvns/git-workflow GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (19.9 KB), approximately 6.0k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!