Synthesising Git Repos for Tutorial Walkthroughs

Over the last few years, I’ve run several scout camps called “Scout Hack”, during which we teach kids basic HTML, CSS, Bootstrap, and enough Python to build a very basic Flask website.

While trying to write the material for this course, I was reflecting on the best way to present the material such that:

  1. It was obvious and deterministic as to what each step was and why we did it
  2. We could show the resulting web page after each step
  3. I could easily make tweaks to earlier steps without having to re-write intermediate example code for demonstration purposes

I eventually settled on a synthesised git repository in which each commit was a step in the documentation. This meant that I could use the commit diff to show exactly what code changes participants had to make, and I could store documentation explaining each change in the commit message.

For bonus points, I wrote each commit message as a title with a body in Markdown format, and wrote a script to convert a git repo of this type into a Markdown-formatted document that could then be rendered as a HTML page or a RevealJS slide deck, both complete with code diffs.

Let’s take a look at how this works in practice.

First, we create a repository with a predictable, empty first commit:

$ git init
Initialized empty Git repository in /tmp/course/.git/
$ touch .gitignore
$ git add .gitignore
$ git commit -m "Initial commit"
[main (root-commit) 9b8af25] Initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 .gitignore

Then we have our first instructional commit:

$ cat index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

</body>
</html>
$ git commit -m 'Create a basic HTML document

Start by creating a text file called `index.html`.

This file contains a `DOCTYPE` to declare itself as a html document, a couple
of meta tags to declare its encoding and viewport, a placeholder `<title>` tag,
and an empty `<body>`.'
[main f25f07c] Create a basic HTML document
 1 file changed, 11 insertions(+)
 create mode 100644 index.html

OK, next instruction:

diff --git i/index.html w/index.html
index bd2bde6..0873343 100644
--- i/index.html
+++ w/index.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Document</title>
+    <title>My First Page</title>
 </head>
 <body>
$ git add index.html
$ git commit -m 'Set a page title

Edit the `<title>` element to choose your own title.
Make **sure** you save your file before testing this.'
[main a487f57] Set a page title
 1 file changed, 1 insertion(+), 1 deletion(-)

OK, let’s take a look at what we’ve got:

$ git log --reverse
commit 9b8af25
Author: Matthew Cengia <mattcen@mattcen.com>
Date:   Thu, 3 Oct 2024 19:02:17 +1000

    Initial commit

commit f25f07c
Author: Matthew Cengia <mattcen@mattcen.com>
Date:   Thu, 3 Oct 2024 19:15:14 +1000

    Create a basic HTML document

    Start by creating a text file called `index.html`.

    This file contains a `DOCTYPE` to declare itself as a html document, a couple
    of meta tags to declare its encoding and viewport, a placeholder `<title>` tag,
    and an empty `<body>`.

commit a487f57 (HEAD -> main)
Author: Matthew Cengia <mattcen@mattcen.com>
Date:   Thu, 3 Oct 2024 19:21:26 +1000

    Set a page title

    Edit the `<title>` element to choose your own title.
    Make **sure** you save your file before testing this.

Looks good! If we’d made any errors, we could use git commit --fixup <previous-commit-id> to add anything we’ve forgotten, and/or use git rebase -i --root to edit any historical code or commit messages and re-order/squash/fixup those commits. I’m not going to explain how to use git rebase -i here, but there is plenty of documentation online.

Once we’re satisfied with our commit history, we can format it as Markdown:

$ repo_path=https://git.mattcen.com/mattcen/synth-git-repo-tutorial
$ git log -p --reverse --format='```%n%n</details>%n%n# %s%n[%h]('"$repo_path"'/commit/%H)%n%n%b%n%n<details open><summary>diff</summary>%n%n```diff {.numberLines}' |
    sed \
        -e '1,4d;$a```\n\n</details>' \
        -e '/```diff/{N;s/\n//}' > walkthrough.md

Let’s step through that. We do a git log in reverse (to show the first commit at the top), with -p to show the diff, and with --format to customise the output format. The format string is interpreted as follows:



</details>

# COMMIT_MESSAGE_SUBJECT
[COMMIT_SHORT_HASH]($repo_path/commit/COMMIT_HASH)

COMMIT_MESSAGE_BODY

<details open><summary>diff</summary>

```diff {.numberLines}

Then, as a result of the -p, git outputs the diff for the commit, followed by, usually, the next commit message (which contains the closing </details> tag for the one opened before the previous diff).

We pipe all this through sed to remove the first (unnecessary) </details> and blank lines from the start, add an equivalent onto the very end to close off the final diff, and delete the undesired blank line at the top of each diff.

The whole $repo_path thing is so that each step can have a hyperlink to its respective git commit on GitHub or elsewhere.

If we instead wanted to generate Markdown suitable for use with Reveal.js, we could do this:

git log -p --reverse --format='```%n%n---%n%n# %s [*]('"$repo_path"'/commit/%H)%n%n%b%n%n```diff []' |
    sed \
        -e '1,4d;$a```' \
        -e '/```diff/{N;s/\n//}' > walkthrough_slides.md

Which translates to:



---

# COMMIT_MESSAGE_SUBJECT [*]($repo_path/commit/COMMIT_HASH)

COMMIT_MESSAGE_BODY

```diff []

And again with a similar sed to shuffle around the bits surrounding the code diff and remove extra blank line.

This would produce slides with a title equal to the commit subject, followed by an asterisk linking to the published git commit, a paragraph showing the commit message body, and finally a formatted diff. This wouldn’t all likely fit on one slide, so would need some manual tweaking.

And we’re done! It’s a little hacky, but it works rather well! See below for the resulting Markdown and rendered HTML.

Resulting Markdown

# Initial commit
[9b8af25](https://git.mattcen.com/mattcen/synth-git-repo-tutorial/commit/9b8af25720ce020bc1a2b39966113bd741c38f3e)



<details open><summary>diff</summary>

```diff {.numberLines}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e69de29
```

</details>

# Create a basic HTML document
[f25f07c](https://git.mattcen.com/mattcen/synth-git-repo-tutorial/commit/f25f07c77a0384044e0295db97f8dfc97a5b23bf)

Start by creating a text file called `index.html`.

This file contains a `DOCTYPE` to declare itself as a html document, a couple
of meta tags to declare its encoding and viewport, a placeholder `<title>` tag,
and an empty `<body>`.


<details open><summary>diff</summary>

```diff {.numberLines}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..bd2bde6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+</head>
+<body>
+    
+</body>
+</html>
```

</details>

# Set a page title
[a487f57](https://git.mattcen.com/mattcen/synth-git-repo-tutorial/commit/a487f57c35d64574416d073964849ebef0faa07e)

Edit the `<title>` element to choose your own title.
Make **sure** you save your file before testing this.


<details open><summary>diff</summary>

```diff {.numberLines}
diff --git a/index.html b/index.html
index bd2bde6..0873343 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Document</title>
+    <title>My First Page</title>
 </head>
 <body>
     
```

</details>

Which renders as follows:


Initial commit

9b8af25

diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e69de29

Create a basic HTML document

f25f07c

Start by creating a text file called index.html.

This file contains a DOCTYPE to declare itself as a html document, a couple of meta tags to declare its encoding and viewport, a placeholder <title> tag, and an empty <body>.

diff
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..bd2bde6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+</head>
+<body>
+    
+</body>
+</html>

Set a page title

a487f57

Edit the <title> element to choose your own title. Make sure you save your file before testing this.

diff
diff --git a/index.html b/index.html
index bd2bde6..0873343 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Document</title>
+    <title>My First Page</title>
 </head>
 <body>