summary history branches tags files
commit:1951074e8fdd70eb72b3fa3b514a74e1eef3da19
author:Trevor Bentley
committer:Trevor Bentley
date:Tue Jan 10 01:13:35 2023 +0100
parents:9b64c36b0615dc2ab2382a59c37777e56c7ffe8f
functional prototype: TOML settings, CLI args, templated repo output
diff --git a/Cargo.lock b/Cargo.lock
line changes: +1094/-18
index d1f627c..e30c466
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,24 @@
 version = 3
 
 [[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "autocfg"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -15,6 +33,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[package]]
 name = "cc"
 version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -30,6 +73,202 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde"
+dependencies = [
+ "chrono",
+ "chrono-tz-build",
+ "phf",
+]
+
+[[package]]
+name = "chrono-tz-build"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c"
+dependencies = [
+ "parse-zoneinfo",
+ "phf",
+ "phf_codegen",
+]
+
+[[package]]
+name = "clap"
+version = "4.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39"
+dependencies = [
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deunicode"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
 name = "form_urlencoded"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -39,6 +278,27 @@ dependencies = [
 ]
 
 [[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
 name = "git2"
 version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -54,6 +314,75 @@ dependencies = [
 ]
 
 [[package]]
+name = "globset"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humansize"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
 name = "idna"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -64,10 +393,60 @@ dependencies = [
 ]
 
 [[package]]
+name = "ignore"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3"
+dependencies = [
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
+dependencies = [
+ "hermit-abi",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
+
+[[package]]
 name = "itsy-gitsy"
 version = "0.1.0"
 dependencies = [
+ "chrono",
+ "clap",
  "git2",
+ "serde",
+ "tera",
+ "toml",
 ]
 
 [[package]]
@@ -80,6 +459,21 @@ dependencies = [
 ]
 
 [[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
 name = "libc"
 version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -126,6 +520,21 @@ dependencies = [
 ]
 
 [[package]]
+name = "link-cplusplus"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
 name = "log"
 version = "0.4.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -135,6 +544,37 @@ dependencies = [
 ]
 
 [[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
 name = "openssl-probe"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -154,49 +594,514 @@ dependencies = [
 ]
 
 [[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
+
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
+dependencies = [
+ "regex",
+]
+
+[[package]]
 name = "percent-encoding"
 version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
 
 [[package]]
-name = "pkg-config"
-version = "0.3.26"
+name = "pest"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
 
 [[package]]
-name = "tinyvec"
-version = "1.6.0"
+name = "pest_derive"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603"
 dependencies = [
- "tinyvec_macros",
+ "pest",
+ "pest_generator",
 ]
 
 [[package]]
-name = "tinyvec_macros"
-version = "0.1.0"
+name = "pest_generator"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
 
 [[package]]
-name = "unicode-bidi"
-version = "0.3.8"
+name = "pest_meta"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha1",
+]
 
 [[package]]
-name = "unicode-normalization"
-version = "0.1.22"
+name = "phf"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
 dependencies = [
- "tinyvec",
+ "phf_shared",
 ]
 
 [[package]]
-name = "url"
+name = "phf_codegen"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
+dependencies = [
+ "siphasher",
+ "uncased",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustix"
+version = "0.36.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "slug"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+dependencies = [
+ "deunicode",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tera"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3df578c295f9ec044ff1c829daf31bb7581d5b3c2a7a3d87419afe1f2531438c"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "toml"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[package]]
+name = "uncased"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
+dependencies = [
+ "unic-ucd-segment",
+]
+
+[[package]]
+name = "unic-ucd-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
 version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
@@ -211,3 +1116,174 @@ name = "vcpkg"
 version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

diff --git a/Cargo.toml b/Cargo.toml
line changes: +5/-1
index dbff112..89f9634
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,11 @@ repository = "https://github.com/mrmekon/itsy-gitsy"
 readme = "README.md"
 license = "GPL-3.0-or-later"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+chrono = "0.4.23"
+clap = { version="4.0.32", features=["derive"] }
 git2 = "0.15.0"
+serde = { version = "1.0.152", features = ["derive"] }
+tera = "1.17.1"
+toml = "0.5.10"

diff --git a/settings.toml b/settings.toml
line changes: +12/-0
index 0000000..b90c15e
--- /dev/null
+++ b/settings.toml
@@ -0,0 +1,12 @@
+template_dir = "templates/"
+output_dir = "gen/"
+
+[a_repo]
+path = "repos/connectr"
+name = "connectr"
+
+[another_repo]
+path = "repos/fruitbasket"
+
+[extra]
+thingo = 1

diff --git a/src/main.rs b/src/main.rs
line changes: +766/-8
index 7fa12e6..7b8f275
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,16 +1,774 @@
-use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time};
-use git2::{DiffFormat, Error, Pathspec};
+use chrono::{
+    DateTime,
+    offset::FixedOffset,
+    naive::NaiveDateTime,
+};
+use clap::Parser;
+use git2::{DiffOptions, Repository, Error};
+use serde::{Serialize, Deserialize};
+use std::collections::{BTreeMap, HashMap};
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use tera::{Context, Filter, Function, Tera, Value, to_value, try_get_value};
+
+fn ts_to_date(ts: i64, offset: Option<i64>, format: Option<String>) -> String {
+    let offset = offset.unwrap_or(0);
+    let dt = NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("Invalid timestamp");
+    let dt_tz: DateTime<FixedOffset> = DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone"));
+    match format {
+        Some(f) => dt_tz.format(&f).to_string(),
+        None => dt_tz.format("%Y-%m-%d").to_string(),
+    }
+}
+
+fn ts_to_git_timestamp(ts: i64, offset: Option<i64>) -> String {
+    let offset = offset.unwrap_or(0);
+    let dt = chrono::naive::NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("invalid timestamp");
+    let dt_tz: DateTime<FixedOffset> = DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone"));
+    dt_tz.format("%a %b %e %T %Y %z").to_string()
+}
+
+fn first_line(msg: &[u8]) -> String {
+    let message = String::from_utf8_lossy(msg);
+    message.lines().next().unwrap_or("[no commit message]").to_owned()
+}
+
+#[derive(Serialize)]
+struct GitRepo {
+    name: String,
+    metadata: ItsyMetadata,
+    history: Vec<GitObject>,
+    branches: Vec<GitObject>,
+    tags: Vec<GitObject>,
+    root_files: Vec<GitFile>,
+    all_files: Vec<GitFile>,
+    commits: BTreeMap<String, GitObject>,
+}
+
+#[derive(Serialize, Default)]
+struct ItsyMetadata {
+    full_name: Option<String>,
+    description: Option<String>,
+    website: Option<String>,
+    clone: Option<String>,
+    attributes: BTreeMap<String, String>,
+}
+
+#[derive(Serialize, Default)]
+struct GitAuthor {
+    name: Option<String>,
+    email: Option<String>,
+}
+
+#[derive(Serialize, Default)]
+struct GitObject {
+    full_hash: String,
+    short_hash: String,
+    ts_utc: i64,
+    ts_offset: i64,
+    author: GitAuthor,
+    committer: GitAuthor,
+    parents: Vec<String>,
+    ref_name: Option<String>,
+    alt_refs: Vec<String>,
+    tagged_id: Option<String>,
+    tree_id: Option<String>,
+    summary: Option<String>,
+    message: Option<String>,
+    stats: Option<GitStats>,
+    diff: Option<GitDiffCommit>,
+}
+
+#[derive(Serialize, Default)]
+struct GitStats {
+    files: usize,
+    additions: usize,
+    deletions: usize,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct GitFile {
+    id: String,
+    name: String,
+    path: String,
+    mode: i32,
+    kind: String,
+    is_binary: bool,
+    size: usize,
+    contents: Option<String>,
+}
+
+#[derive(Serialize, Default)]
+struct GitDiffCommit {
+    files: Vec<GitDiffFile>,
+    file_count: usize,
+    additions: usize,
+    deletions: usize,
+}
+
+#[derive(Serialize, Default)]
+struct GitDiffFile {
+    oldfile: String,
+    newfile: String,
+    basefile: String,
+    oldid: String,
+    newid: String,
+    extra: String,
+    additions: usize,
+    deletions: usize,
+    hunks: Vec<GitDiffHunk>
+}
+
+#[derive(Serialize, Default)]
+struct GitDiffHunk {
+    context: String,
+    lines: Vec<GitDiffLine>,
+}
+
+#[derive(Serialize)]
+struct GitDiffLine {
+    kind: &'static str,
+    prefix: &'static str,
+    text: String,
+}
+
+fn walk_file_tree(repo: &git2::Repository, rev: &str, files: &mut Vec<GitFile>,
+                  depth: usize, recurse: bool, prefix: &str) -> Result<(), Error> {
+    let obj = repo.revparse_single(rev)?;
+    let tree = obj.peel_to_tree()?;
+    for entry in tree.iter() {
+        let name = prefix.to_string() + entry.name().unwrap_or_default();
+        let kind = match entry.kind() {
+            Some(git2::ObjectType::Tree) => "dir",
+            Some(git2::ObjectType::Blob) => "file",
+            Some(git2::ObjectType::Commit) => "submodule",
+            _ => "unknown",
+        };
+        let mut is_binary = false;
+        let mut size = 0;
+
+        if let Ok(blob) = repo.find_blob(entry.id()) {
+            is_binary = blob.is_binary();
+            size = blob.content().len();
+        }
+        files.push(GitFile {
+            id: entry.id().to_string(),
+            name: name.clone(),
+            path: match depth {
+                0 => name.to_string(),
+                _ => format!("{}/{}", prefix, name),
+            },
+            kind: kind.to_string(),
+            mode: entry.filemode(),
+            is_binary,
+            size,
+            contents: None,
+        });
+        if recurse && entry.kind() == Some(git2::ObjectType::Tree) {
+            let prefix = name + "/";
+            walk_file_tree(repo, &entry.id().to_string(), files, depth+1, true, &prefix)?;
+        }
+    }
+    Ok(())
+}
+
+fn parse_repo(repo: &Repository, name: &str) -> Result<GitRepo, Error> {
+    let mut history: Vec<GitObject> = vec!();
+    let mut branches: Vec<GitObject> = vec!();
+    let mut tags: Vec<GitObject> = vec!();
+    let mut commits: BTreeMap<String, GitObject> = BTreeMap::new();
 
-fn run() -> Result<(), Error> {
-    let path = ".";
-    let repo = Repository::open(path)?;
     let mut revwalk = repo.revwalk()?;
     revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
     revwalk.push_head()?;
-    Ok(())
+    for oid in revwalk {
+        let oid = oid?;
+        commits.insert(oid.to_string(), parse_commit(repo, &oid.to_string())?);
+        let commit = repo.find_commit(oid)?;
+        let obj = repo.revparse_single(&commit.id().to_string())?;
+        let full_hash = commit.id().to_string();
+        let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string();
+
+        let mut parents: Vec<String> = vec!();
+        let a = if commit.parents().len() == 1 {
+            let parent = commit.parent(0)?;
+            parents.push(parent.id().to_string());
+            Some(parent.tree()?)
+        } else {
+            None
+        };
+        let b = commit.tree()?;
+        let mut diffopts = DiffOptions::new();
+        let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts))?;
+        let stats = diff.stats()?;
+        let stats = GitStats {
+            files: stats.files_changed(),
+            additions: stats.insertions(),
+            deletions: stats.deletions(),
+        };
+
+        // TODO: is it acceptable to iterate over all references for
+        // every commit?  Is there another way?  Should probably cache
+        // all of the ref IDs in memory.
+        let mut alt_refs = vec!();
+        for refr in repo.references()? {
+            let refr = refr?;
+            if let Some(target) = refr.target() {
+                if target == commit.id() {
+                    // TODO: save these
+                    if let Some(name) = refr.shorthand() {
+                        alt_refs.push(name.to_string());
+                    }
+                }
+            }
+        }
+
+        history.push(GitObject {
+            full_hash,
+            short_hash,
+            ts_utc: commit.author().when().seconds(),
+            ts_offset: (commit.author().when().offset_minutes() as i64) * 60,
+            parents,
+            ref_name: None,
+            alt_refs,
+            author: GitAuthor {
+                name:  commit.author().name().map(|x| x.to_owned()),
+                email: commit.author().email().map(|x| x.to_owned()),
+            },
+            summary: Some(first_line(commit.message_bytes())),
+            stats: Some(stats),
+            ..Default::default()
+        });
+    }
+
+    for branch in repo.branches(None)? {
+        let (branch, _branch_type) = branch?;
+        let refr = branch.get();
+        let name = branch.name()?.unwrap_or("[unnamed]");
+        let obj = repo.revparse_single(name)?;
+        // Only show direct references, skip symbolic aliases.  Maybe
+        // this is a bad idea?
+        match refr.kind() {
+            Some(k) if k == git2::ReferenceType::Symbolic => continue,
+            _ => {},
+        }
+        let commit = repo.find_commit(obj.id())?;
+        let full_hash = obj.id().to_string();
+        let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string();
+        branches.push(GitObject {
+            full_hash,
+            short_hash,
+            ts_utc: commit.author().when().seconds(),
+            ts_offset: (commit.author().when().offset_minutes() as i64) * 60,
+            parents: vec!(),
+            ref_name: Some(name.to_string()),
+            author: GitAuthor {
+                name: commit.author().name().map(|x| x.to_owned()),
+                email: commit.author().email().map(|x| x.to_owned()),
+            },
+            committer: GitAuthor {
+                name: commit.committer().name().map(|x| x.to_owned()),
+                email: commit.committer().email().map(|x| x.to_owned()),
+            },
+            summary: Some(first_line(commit.message_bytes())),
+            message: commit.message().map(|x| x.to_string()),
+            ..Default::default()
+        });
+    }
+    for tag in repo.tag_names(None)?.iter() {
+        let tag = tag.unwrap_or("[unnamed]");
+        let obj = repo.revparse_single(tag)?;
+        let commit = repo.find_tag(obj.id())?;
+        let full_hash = obj.id().to_string();
+        let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string();
+        let (ts, tz) = match commit.tagger() {
+            Some(t) => (t.when().seconds(), (t.when().offset_minutes() as i64) * 60),
+            _ => (0, 0),
+        };
+        let (author,email) = match commit.tagger() {
+            Some(t) => (t.name().map(|x| x.to_owned()),
+                        t.email().map(|x| x.to_owned())),
+            _ => (None, None),
+        };
+        let summary = match commit.message_bytes() {
+            Some(m) => Some(first_line(m)),
+            _ => None,
+        };
+        tags.push(GitObject {
+            full_hash,
+            short_hash,
+            ts_utc: ts,
+            ts_offset: tz,
+            ref_name: Some(tag.to_string()),
+            author: GitAuthor {
+                name: author,
+                email,
+            },
+            tagged_id: Some(commit.target_id().to_string()),
+            message: commit.message().map(|x| x.to_string()),
+            summary,
+            ..Default::default()
+        });
+    }
+
+    let mut root_files: Vec<GitFile> = vec!();
+    let mut all_files: Vec<GitFile> = vec!();
+    walk_file_tree(&repo, "origin/HEAD", &mut root_files, 0, false, "")?;
+    // TODO: maybe this should be optional?  Walking the whole tree
+    // could be slow on huge repos.
+    walk_file_tree(&repo, "origin/HEAD", &mut all_files, 0, true, "")?;
+
+    Ok(GitRepo {
+        name: name.to_string(),
+        metadata: Default::default(),
+        history,
+        branches,
+        tags,
+        root_files,
+        all_files,
+        commits,
+    })
+}
+
+fn parse_commit(repo: &Repository, refr: &str) -> Result<GitObject, Error> {
+    let obj = repo.revparse_single(refr)?;
+    let commit = repo.find_commit(obj.id())?;
+    let mut parents: Vec<String> = vec!();
+
+    let a = match commit.parents().len() {
+        x if x == 1 => {
+            let parent = commit.parent(0).unwrap();
+            parents.push(parent.id().to_string());
+            Some(parent.tree()?)
+        },
+        x if x > 1 => {
+            for parent in commit.parents() {
+                parents.push(parent.id().to_string());
+            }
+            None
+        },
+        _ => {
+            None
+        },
+    };
+    let b = commit.tree()?;
+    let mut diffopts = DiffOptions::new();
+    let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts))?;
+    let stats = diff.stats()?;
+
+    let mut commit_diff: GitDiffCommit = GitDiffCommit {
+        file_count: stats.files_changed(),
+        additions: stats.insertions(),
+        deletions: stats.deletions(),
+        ..Default::default()
+    };
+    let files: std::rc::Rc<std::cell::RefCell<Vec<GitDiffFile>>> = std::rc::Rc::new(std::cell::RefCell::new(vec!()));
+
+    diff.foreach(
+        &mut |file, _progress| {
+            let mut file_diff: GitDiffFile = Default::default();
+            file_diff.newfile = match file.status() {
+                git2::Delta::Deleted => "/dev/null".to_owned(),
+                _ => file.new_file().path().map(|x| "b/".to_string() + &x.to_string_lossy()).unwrap_or("/dev/null".to_string()),
+            };
+            file_diff.oldfile = match file.status() {
+                git2::Delta::Added => "/dev/null".to_owned(),
+                _ => file.old_file().path().map(|x| "a/".to_string() + &x.to_string_lossy()).unwrap_or("/dev/null".to_string()),
+            };
+            file_diff.basefile = match file.status() {
+                git2::Delta::Added => file.new_file().path().map(|x| x.to_string_lossy().to_string()).unwrap_or("/dev/null".to_string()),
+                _ => file.old_file().path().map(|x| x.to_string_lossy().to_string()).unwrap_or("/dev/null".to_string()),
+            };
+            file_diff.oldid = file.old_file().id().to_string();
+            file_diff.newid = file.new_file().id().to_string();
+            files.borrow_mut().push(file_diff);
+            true
+        },
+        None, // TODO: handle binary files?
+        Some(&mut |_file, hunk| {
+            let mut files = files.borrow_mut();
+            let file_diff: &mut GitDiffFile = files.last_mut().expect("Diff hunk not associated with a file!");
+            let mut hunk_diff: GitDiffHunk = Default::default();
+            hunk_diff.context = String::from_utf8_lossy(hunk.header()).to_string();
+            file_diff.hunks.push(hunk_diff);
+            true
+        }),
+        Some(&mut |_file, _hunk, line| {
+            let mut files = files.borrow_mut();
+            let file_diff: &mut GitDiffFile = files.last_mut().expect("Diff hunk not associated with a file!");
+            let hunk_diff: &mut GitDiffHunk = file_diff.hunks.last_mut().expect("Diff line not associated with a hunk!");
+            let (kind, prefix) = match line.origin() {
+                ' ' => ("ctx", " "),
+                '-' => ("del", "-"),
+                '+' => ("add", "+"),
+                _ => ("other", " "),
+            };
+            match line.origin() {
+                '-' => file_diff.deletions += 1,
+                '+' => file_diff.additions += 1,
+                _ => {},
+            }
+            let line_diff = GitDiffLine {
+                text: String::from_utf8_lossy(line.content()).to_string(),
+                kind,
+                prefix,
+            };
+            hunk_diff.lines.push(line_diff);
+            true
+        })
+    )?;
+
+    match std::rc::Rc::try_unwrap(files) {
+        Ok(files) => {
+            let files: Vec<GitDiffFile> = files.into_inner();
+            commit_diff.files = files;
+        },
+        Err(_) => {},
+    }
+
+    let tree = obj.peel_to_tree()?;
+    let summary = GitObject {
+        full_hash: obj.id().to_string(),
+        short_hash: obj.short_id()?.as_str().unwrap_or_default().to_string(),
+        ts_utc: commit.author().when().seconds(),
+        ts_offset: (commit.author().when().offset_minutes() as i64) * 60,
+        tagged_id: None,
+        tree_id: Some(tree.id().to_string()),
+        parents,
+        ref_name: None,
+        alt_refs: vec!(),
+        author: GitAuthor {
+            name: commit.author().name().map(|x| x.to_string()),
+            email: commit.author().email().map(|x| x.to_string()),
+        },
+        committer: GitAuthor {
+            name: commit.committer().name().map(|x| x.to_string()),
+            email: commit.committer().email().map(|x| x.to_string()),
+        },
+        summary: Some(first_line(commit.message_bytes())),
+        message: commit.message().map(|x| x.to_string()),
+        stats: None,
+        diff: Some(commit_diff),
+    };
+
+    Ok(summary)
+}
+
+fn fill_file_contents(repo: &Repository, file: &GitFile) -> Result<GitFile, Error> {
+    let mut file = file.clone();
+    if file.kind == "file" {
+        let blob = repo.find_blob(git2::Oid::from_str(&file.id)?)?;
+        file.contents = match blob.is_binary() {
+            false => Some(String::from_utf8_lossy(blob.content()).to_string()),
+            true => Some(format!("[Binary data ({} bytes)]", blob.content().len())),
+        };
+    }
+    Ok(file)
+}
+
+fn dir_listing(repo: &Repository, file: &GitFile) -> Result<Vec<GitFile>, Error> {
+    let mut files: Vec<GitFile> = vec!();
+    walk_file_tree(&repo, &file.id, &mut files, 0, false, "")?;
+    Ok(files)
+}
+
+struct FileFilter;
+impl Filter for FileFilter {
+    fn filter(&self, value: &Value, _args: &HashMap<String, Value>
+    ) -> Result<Value, tera::Error> {
+        let file_list: Vec<GitFile> = try_get_value!("only_files", "value", Vec<GitFile>, value);
+        let file_list: Vec<GitFile> = file_list.iter().filter_map(|x| match x.kind.as_str() {
+            "file" => Some(x.clone()),
+            _ => None,
+        }).collect();
+        Ok(to_value(file_list).unwrap())
+    }
+}
+
+struct DirFilter;
+impl Filter for DirFilter {
+    fn filter(&self, value: &Value, _args: &HashMap<String, Value>
+    ) -> Result<Value, tera::Error> {
+        let file_list: Vec<GitFile> = try_get_value!("only_dirs", "value", Vec<GitFile>, value);
+        let file_list: Vec<GitFile> = file_list.iter().filter_map(|x| match x.kind.as_str() {
+            "dir" => Some(x.clone()),
+            _ => None,
+        }).collect();
+        Ok(to_value(file_list).unwrap())
+    }
+}
+
+struct TsDateFn;
+impl Function for TsDateFn {
+    fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
+        let ts: Option<i64> = match args.get("ts") {
+            Some(ts) => match tera::from_value(ts.clone()) {
+                Ok(ts) => Some(ts),
+                _ => None,
+            },
+            _ => None,
+        };
+        let ts = ts.expect("ts_to_date missing a `ts` argument");
+
+        let tz: Option<i64> = match args.get("tz") {
+            Some(tz) => match tera::from_value(tz.clone()) {
+                Ok(tz) => Some(tz),
+                _ => None,
+            },
+            _ => None,
+        };
+
+        let fmt: Option<String> = match args.get("fmt") {
+            Some(fmt) => match tera::from_value(fmt.clone()) {
+                Ok(fmt) => Some(fmt),
+                _ => None,
+            },
+            _ => None,
+        };
+        Ok(to_value(ts_to_date(ts, tz, fmt)).unwrap())
+    }
+}
+
+struct TsTimestampFn;
+impl Function for TsTimestampFn {
+    fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
+        let ts: Option<i64> = match args.get("ts") {
+            Some(ts) => match tera::from_value(ts.clone()) {
+                Ok(ts) => Some(ts),
+                _ => None,
+            },
+            _ => None,
+        };
+        let ts = ts.expect("ts_to_git_timestamp missing a `ts` argument");
+
+        let tz: Option<i64> = match args.get("tz") {
+            Some(tz) => match tera::from_value(tz.clone()) {
+                Ok(tz) => Some(tz),
+                _ => None,
+            },
+            _ => None,
+        };
+        Ok(to_value(ts_to_git_timestamp(ts, tz)).unwrap())
+    }
+}
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct CliArgs {
+    #[arg(short, long, value_name = "FILE")]
+    config: Option<PathBuf>,
+}
+
+#[derive(Deserialize)]
+#[allow(dead_code)]
+struct ItsySettings {
+    template_dir: PathBuf,
+    output_dir: PathBuf,
+    recursive_repo_dirs: Option<Vec<PathBuf>>,
+    extra: HashMap<String, toml::Value>,
+}
+#[derive(Deserialize)]
+#[allow(dead_code)]
+struct ItsySettingsRepo {
+    path: PathBuf,
+    name: Option<String>,
+    description: Option<String>,
+    website: Option<String>,
 }
 
 fn main() {
-    println!("Hello, world!");
-    run().expect("run failed");
+    let cli = CliArgs::parse();
+    let config_path = cli.config.as_deref().unwrap_or(Path::new("config.toml"));
+
+    // Parse the known settings directly into their struct
+    let toml = std::fs::read_to_string(config_path).expect(&format!("Configuration file not found: {}", config_path.display()));
+    let settings: ItsySettings = toml::from_str(&toml).expect("Configuration file is invalid.");
+
+    // Get a list of all remaining TOML "tables" in the file.
+    // These are the user-supplied individual repositories.
+    let reserved_keys = vec!("repos","extra");
+    let settings_raw: HashMap<String, toml::Value> = toml::from_str(&toml).expect("blah");
+    let table_keys: Vec<String> = settings_raw.iter().filter_map(|x| match x.1.is_table() {
+        true => match reserved_keys.contains(&x.0.as_str()) {
+            false => Some(x.0.clone()),
+            true => None,
+        },
+        false => None
+    }).collect();
+
+    // Try to convert each unknown "table" into a repo struct, and
+    // save the ones that are successful.  If no repo name is
+    // specified, use the TOML table name.
+    let mut repos: Vec<ItsySettingsRepo> = vec!();
+    for k in &table_keys {
+        let v = settings_raw.get(k).unwrap();
+        match toml::from_str::<ItsySettingsRepo>(&v.to_string()) {
+            Ok(mut repo) => {
+                if repo.name.is_none() {
+                    repo.name = Some(k.clone());
+                }
+                repos.push(repo);
+            },
+            _ => {},
+        }
+    }
+
+    for repo in &repos {
+        println!("Parse repo: {}", repo.name.as_ref().unwrap());
+    }
+
+    let mut template_path = settings.template_dir.clone();
+    template_path.push("**");
+    template_path.push("*.html");
+    let mut tera = match Tera::new(template_path.to_str().expect("No template path set!")) {
+        Ok(t) => t,
+        Err(e) => {
+            println!("Parsing error(s): {}", e);
+            ::std::process::exit(1);
+        }
+    };
+    tera.register_filter("only_files", FileFilter{});
+    tera.register_filter("only_dirs", DirFilter{});
+    tera.register_function("ts_to_date", TsDateFn{});
+    tera.register_function("ts_to_git_timestamp", TsTimestampFn{});
+
+    // Create output directory
+    let _ = std::fs::create_dir(settings.output_dir.to_str().expect("Output path not set!"));
+
+    let mut repos: Vec<GitRepo> = vec!();
+    for dir in std::fs::read_dir(std::path::Path::new("repos")).expect("Repo directory not found.") {
+        let dir = dir.expect("Repo contains invalid entries");
+        match dir.metadata() {
+            Ok(m) if m.is_dir() => {},
+            _ => continue,
+        }
+        let path: String = dir.path().to_string_lossy().to_string();
+        let name: String = dir.file_name().to_string_lossy().to_string();
+        let repo = Repository::open(path).expect("Unable to find git repository.");
+        let summary = parse_repo(&repo, &name).expect("Failed to analyze repo HEAD.");
+
+        let mut local_ctx = Context::from_serialize(&summary).unwrap();
+        match tera.render("summary.html", &local_ctx) {
+            Ok(rendered) => {
+                let mut output_path = settings.output_dir.clone();
+                output_path.push(&name);
+                let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
+                output_path.push("summary.html");
+                let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+                file.write(rendered.as_bytes()).expect("failed to save rendered html");
+            },
+            Err(x) => println!("ERROR: {:?}", x),
+        }
+
+        for branch in &summary.branches {
+            local_ctx.insert("branch", branch);
+            match tera.render("branch.html", &local_ctx) {
+                Ok(rendered) => {
+                    let mut output_path = settings.output_dir.clone();
+                    output_path.push(&summary.name);
+                    output_path.push("branch");
+                    let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
+                    output_path.push(format!("{}.html", branch.full_hash));
+                    let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+                    file.write(rendered.as_bytes()).expect("failed to save rendered html");
+                },
+                Err(x) => match x.kind {
+                    tera::ErrorKind::TemplateNotFound(_) => {},
+                    _ => println!("ERROR: {:?}", x),
+                },
+            }
+            local_ctx.remove("branch");
+        }
+
+        for tag in &summary.tags {
+            local_ctx.insert("tag", tag);
+            if let Some(commit) = summary.commits.get(tag.tagged_id.as_ref().unwrap()) {
+                local_ctx.insert("commit", &commit);
+            }
+            match tera.render("tag.html", &local_ctx) {
+                Ok(rendered) => {
+                    let mut output_path = settings.output_dir.clone();
+                    output_path.push(&summary.name);
+                    output_path.push("tag");
+                    let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
+                    output_path.push(format!("{}.html", tag.full_hash));
+                    let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+                    file.write(rendered.as_bytes()).expect("failed to save rendered html");
+                },
+                Err(x) => match x.kind {
+                    tera::ErrorKind::TemplateNotFound(_) => {},
+                    _ => println!("ERROR: {:?}", x),
+                },
+            }
+            local_ctx.remove("tag");
+            local_ctx.remove("commit");
+        }
+
+        for (_id, commit) in &summary.commits {
+            local_ctx.try_insert("commit", &commit).expect("Failed to add commit to template engine.");
+            match tera.render("commit.html", &local_ctx) {
+                Ok(rendered) => {
+                    let mut output_path = settings.output_dir.clone();
+                    output_path.push(&summary.name);
+                    output_path.push("commit");
+                    let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
+                    output_path.push(format!("{}.html", commit.full_hash));
+                    let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+                    file.write(rendered.as_bytes()).expect("failed to save rendered html");
+                },
+                Err(x) => println!("ERROR: {:?}", x),
+            }
+            local_ctx.remove("commit");
+        }
+
+        for file in summary.all_files.iter().filter(|x| x.kind == "file") {
+            let file = fill_file_contents(&repo, &file).expect("Failed to parse file.");
+            local_ctx.try_insert("file", &file).expect("Failed to add file to template engine.");
+            match tera.render("file.html", &local_ctx) {
+                Ok(rendered) => {
+                    let mut output_path = settings.output_dir.clone();
+                    output_path.push(&summary.name);
+                    output_path.push("file");
+                    let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
+                    output_path.push(format!("{}.html", file.id));
+                    let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+                    file.write(rendered.as_bytes()).expect("failed to save rendered html");
+                },
+                Err(x) => println!("ERROR: {:?}", x),
+            }
+            local_ctx.remove("file");
+        }
+
+        for dir in summary.all_files.iter().filter(|x| x.kind == "dir") {
+            let listing = dir_listing(&repo, &dir).expect("Failed to parse file.");
+            local_ctx.try_insert("files", &listing).expect("Failed to add dir to template engine.");
+            match tera.render("dir.html", &local_ctx) {
+                Ok(rendered) => {
+                    let mut output_path = settings.output_dir.clone();
+                    output_path.push(&summary.name);
+                    output_path.push("dir");
+                    let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
+                    output_path.push(format!("{}.html", dir.id));
+                    let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+                    file.write(rendered.as_bytes()).expect("failed to save rendered html");
+                },
+                Err(x) => println!("ERROR: {:?}", x),
+            }
+            local_ctx.remove("files");
+        }
+
+        repos.push(summary);
+    }
+
+    let mut global_ctx = Context::new();
+    global_ctx.try_insert("repos", &repos).expect("Failed to add repo to template engine.");
+    match tera.render("repos.html", &global_ctx) {
+        Ok(rendered) => {
+            let mut output_path = settings.output_dir.clone();
+            output_path.push("repos.html");
+            let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+            file.write(rendered.as_bytes()).expect("failed to save rendered html");
+        },
+        Err(x) => println!("ERROR: {:?}", x),
+    }
 }

diff --git a/templates/branch.html b/templates/branch.html
line changes: +11/-0
index 0000000..f979a62
--- /dev/null
+++ b/templates/branch.html
@@ -0,0 +1,11 @@
+<html>
+  <body style="font-family: monospace;">
+    branch: {{branch.ref_name}}<br/>
+    hash: {{branch.full_hash}} ({{branch.short_hash}})<br/>
+    author: {{branch.author.name}}<br/>
+    committer: {{branch.committer.name}}<br/>
+    date: {{ts_to_date(ts=branch.ts_utc, tz=branch.ts_offset)}}<br/>
+    summary: {{branch.summary}}<br/>
+    <pre>{{branch.message}}</pre>
+  </body>
+</html>

diff --git a/templates/commit.html b/templates/commit.html
line changes: +31/-0
index 0000000..c216f9c
--- /dev/null
+++ b/templates/commit.html
@@ -0,0 +1,31 @@
+<html>
+  <body style="font-family: monospace;">
+    <strong>
+    commit: {{commit.full_hash}}<br/>
+    author: {{commit.author.name}}<br/>
+    committer: {{commit.committer.name}}<br/>
+    parent: {% if commit.parents | length > 0 -%}<a href="{{commit.parents | first}}.html">{{commit.parents | first}}</a>{%-endif-%}<br/>
+    </strong>
+    <br/>
+    <pre style="margin: 0;">{{commit.message}}</pre>
+    {% for file in commit.diff.files -%}
+    <br/>
+    <strong>
+    diff --git a/{{file.basefile}} b/{{file.basefile}}<br/>
+    line changes: +{{file.additions}}/-{{file.deletions}}<br/>
+    index {{file.oldid | truncate(length=7,end="")}}..{{file.newid | truncate(length=7,end="")}}<br/>
+    --- {{file.oldfile}}<br/>
+    +++ {{file.newfile}}
+    </strong>
+    {% for hunk in file.hunks -%}
+    <pre><strong>{{hunk.context}}</strong>
+    {%- for line in hunk.lines -%}
+    {%- if line.kind in ["del","add"] -%}<span style="color: {%- if line.kind == "del" -%}bb0000{%- else -%}00aa00{% endif %}">{%- endif -%}
+    {{line.prefix}}{{line.text}}
+    {%- if line.kind in ["del","add"] -%}</span>{%- endif -%}
+    {%- endfor -%}
+    </pre>
+    {% endfor -%}
+    {% endfor -%}
+</body>
+</html>

diff --git a/templates/dir.html b/templates/dir.html
line changes: +21/-0
index 0000000..10a0eb3
--- /dev/null
+++ b/templates/dir.html
@@ -0,0 +1,21 @@
+<html>
+  <body style="font-family: monospace;">
+    <table class="files">
+      <tr>
+        <th>File</th>
+        <th>ID</th>
+        <th>Type</th>
+        <th>Mode</th>
+        <th>Size</th>
+      </tr>
+    {% for file in files -%}
+      <tr class="file">
+        <td class="file-name"><a href="../{{file.kind}}/{{file.id}}.html">{{file.name}}</a></td>
+        <td class="file-id">{{file.id}}</td>
+        <td class="file-type">{{file.kind}} ({{file.is_binary}})</td>
+        <td class="file-mode">{{file.mode}}</td>
+        <td class="file-size">{{file.size}}</td>
+      </tr>
+    {% endfor -%}
+  </body>
+</html>

diff --git a/templates/file.html b/templates/file.html
line changes: +7/-0
index 0000000..762aa06
--- /dev/null
+++ b/templates/file.html
@@ -0,0 +1,7 @@
+<html>
+  <body style="font-family: monospace;">
+    {{file.path}} ({{file.name}}) [{{file.id}}]<br/>
+    -------
+    <pre style="margin: 0">{{file.contents}}</pre>
+  </body>
+</html>

diff --git a/templates/repos.html b/templates/repos.html
line changes: +7/-0
index 0000000..1ce812d
--- /dev/null
+++ b/templates/repos.html
@@ -0,0 +1,7 @@
+<html>
+  <body style="font-family: monospace;">
+    {% for repo in repos -%}
+    <a href="{{repo.name}}/summary.html">{{ repo.name }}</a> ({{ts_to_date(ts=repo.history[0].ts_utc, tz=repo.history[0].ts_offset)}})<br/>
+    {% endfor -%}
+  </body>
+</html>

diff --git a/templates/summary.html b/templates/summary.html
line changes: +83/-0
index 0000000..70377f3
--- /dev/null
+++ b/templates/summary.html
@@ -0,0 +1,83 @@
+<html>
+  <body style="font-family: monospace;">
+    <table class="commits">
+      <tr>
+        <th>Commit ID</th>
+        <th>Message</th>
+        <th>Author</th>
+        <th>Date</th>
+        <th>Diff</th>
+        <th>Refs</th>
+      </tr>
+    {% for entry in history -%}
+    {% if loop.index0 < 250  -%}
+      <tr class="commit">
+        <td class="oid"><a href="commit/{{entry.full_hash}}.html">{{entry.short_hash}}</a></td>
+        <td class="commit-msg" style="font-family: sans-serif;">{{entry.summary}}</td>
+        <td class="author" style="font-family: sans-serif;">{{entry.author.name}}</td>
+        <td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
+        <td class="diff">{{entry.stats.files}} (+{{entry.stats.additions}}/-{{entry.stats.deletions}})</td>
+        <td class="refs">{%- for ref in entry.alt_refs -%}{%- if loop.index0 > 0 -%},&nbsp; {%- endif -%}<span class="commit-ref">{{ref}}</span>{%- endfor -%}</td>
+      </tr>
+    {% endif -%}
+    {% endfor -%}
+    </table>
+
+    <table class="branches">
+      <tr>
+        <th>Branch</th>
+        <th>Commit ID</th>
+        <th>Message</th>
+        <th>Author</th>
+        <th>Date</th>
+      </tr>
+    {% for entry in branches -%}
+      <tr class="branch">
+        <td class="name"><a href="branch/{{entry.full_hash}}.html">{{entry.ref_name}}</a></td>
+        <td class="oid">{{entry.short_hash}}</td>
+        <td class="commit-msg" style="font-family: sans-serif;">{{entry.summary}}</td>
+        <td class="author">{{entry.author.name}}</td>
+        <td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
+      </tr>
+    {% endfor -%}
+    </table>
+
+    <table class="tags">
+      <tr>
+        <th>Tag</th>
+        <th>Commit ID</th>
+        <th>Message</th>
+        <th>Author</th>
+        <th>Date</th>
+      </tr>
+    {% for entry in tags -%}
+      <tr class="tag">
+        <td class="name"><a href="tag/{{entry.full_hash}}.html">{{entry.ref_name}}</a></td>
+        <td class="oid">{{entry.short_hash}}</td>
+        <td class="commit-msg" style="font-family: sans-serif;">{{entry.summary}}</td>
+        <td class="author">{{entry.author.name}}</td>
+        <td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
+      </tr>
+    {% endfor -%}
+    </table>
+
+    <table class="files">
+      <tr>
+        <th>File</th>
+        <th>ID</th>
+        <th>Type</th>
+        <th>Mode</th>
+        <th>Size</th>
+      </tr>
+    {% for file in root_files -%}
+      <tr class="file">
+        <td class="name"><a href="{{file.kind}}/{{file.id}}.html">{{file.name}}</a></td>
+        <td class="id">{{file.id}}</td>
+        <td class="type">{{file.kind}} ({{file.is_binary}})</td>
+        <td class="mode">{{file.mode}}</td>
+        <td class="size">{{file.size}}</td>
+      </tr>
+    {% endfor -%}
+    </table>
+</body>
+</html>

diff --git a/templates/tag.html b/templates/tag.html
line changes: +15/-0
index 0000000..53bf17e
--- /dev/null
+++ b/templates/tag.html
@@ -0,0 +1,15 @@
+<html>
+  <body style="font-family: monospace;">
+    branch: {{tag.ref_name}}<br/>
+    hash: {{tag.full_hash}} ({{tag.short_hash}})<br/>
+    author: {{tag.author.name}}<br/>
+    committer: {{tag.committer.name}}<br/>
+    date: {{ts_to_date(ts=tag.ts_utc, tz=tag.ts_offset)}}<br/>
+    summary: {{tag.summary}}<br/>
+    <pre>{{tag.message}}</pre>
+    <br/>
+    commit: {%- if commit -%}<a href="../commit/{{commit.full_hash}}.html">{{commit.full_hash}}</a>
+    <pre>{{commit.message}}</pre>
+    {%-else-%}{{tag.tagged_id}}{%-endif-%}<br/>
+  </body>
+</html>