git home / emma home
logo

sgw

Static git web. Emma's fork of stagit.
git clone https://git.y1.nz/archives/sgw.tar.gz
README | Files | Log | Refs | LICENSE

commit 05e2ac6f921499950adde93f972d0f147b57ceb9
parent 84155dca25baf5e67288cc67a653511e555f7773
Author: Emma Weaver <emma@waeaves.com>
Date:   Tue, 26 May 2026 12:25:29 -0400

Updated README, configs, and args

Diffstat:
MREADME227+++++++++++++++----------------------------------------------------------------
MTODO7++++---
Mconfig.h35++++++++++++++++-------------------
Mindex.c5+----
Mmain.c130++++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mopt.c9+++++----
Mopt.h23+++++++++++------------
7 files changed, 153 insertions(+), 283 deletions(-)

diff --git a/README b/README @@ -1,186 +1,41 @@ -stagit ------- - -static git page generator. - -It generates static HTML pages for a git repository. - - -Usage ------ - -Make files per repository: - - $ mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1 - $ stagit path/to/gitrepo1 - repeat for other repositories - $ ... - -Make index file for repositories: - - $ cd htmlroot - $ stagit-index path/to/gitrepo1 \ - path/to/gitrepo2 \ - path/to/gitrepo3 > index.html - - -Build and install ------------------ - -$ make -# make install - - -Dependencies ------------- - -- C compiler (C99). -- libc (tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl). -- libgit2 (v0.22+). -- POSIX make (optional). - - -Documentation -------------- - -See man pages: stagit(1) and stagit-index(1). - - -Building a static binary ------------------------- - -It may be useful to build static binaries, for example to run in a chroot. - -It can be done like this at the time of writing (v0.24): - -cd libgit2-src - -# change the options in the CMake file: CMakeLists.txt -BUILD_SHARED_LIBS to OFF (static) -CURL to OFF (not needed) -USE_SSH OFF (not needed) -THREADSAFE OFF (not needed) -USE_OPENSSL OFF (not needed, use builtin) - -mkdir -p build && cd build -cmake ../ -make -make install - - -Extract owner field from git config ------------------------------------ - -A way to extract the gitweb owner for example in the format: - - [gitweb] - owner = Name here - -Script: - - #!/bin/sh - awk '/^[ ]*owner[ ]=/ { - sub(/^[^=]*=[ ]*/, ""); - print $0; - }' - - -Set clone URL for a directory of repos --------------------------------------- - #!/bin/sh - cd "$dir" - for i in *; do - test -d "$i" && echo "git://git.codemadness.org/$i" > "$i/url" - done - - -Update files on git push ------------------------- - -Using a post-receive hook the static files can be automatically updated. -Keep in mind git push -f can change the history and the commits may need -to be recreated. This is because stagit checks if a commit file already -exists. It also has a cache (-c) option which can conflict with the new -history. See stagit(1). - -git post-receive hook (repo/.git/hooks/post-receive): - - #!/bin/sh - # detect git push -f - force=0 - while read -r old new ref; do - hasrevs=$(git rev-list "$old" "^$new" | sed 1q) - if test -n "$hasrevs"; then - force=1 - break - fi - done - - # remove commits and .cache on git push -f - #if test "$force" = "1"; then - # ... - #fi - - # see example_create.sh for normal creation of the files. - - -Create .tar.gz archives by tag ------------------------------- - #!/bin/sh - name="stagit" - mkdir -p archives - git tag -l | while read -r t; do - f="archives/${name}-$(echo "${t}" | tr '/' '_').tar.gz" - test -f "${f}" && continue - git archive \ - --format tar.gz \ - --prefix "${t}/" \ - -o "${f}" \ - -- \ - "${t}" - done - - -Features --------- - -- Log of all commits from HEAD. -- Log and diffstat per commit. -- Show file tree with linkable line numbers. -- Show references: local branches and tags. -- Detect README and LICENSE file from HEAD and link it as a webpage. -- Detect submodules (.gitmodules file) from HEAD and link it as a webpage. -- Atom feed of the commit log (atom.xml). -- Atom feed of the tags/refs (tags.xml). -- Make index page for multiple repositories with stagit-index. -- After generating the pages (relatively slow) serving the files is very fast, - simple and requires little resources (because the content is static), only - a HTTP file server is required. -- Usable with text-browsers such as dillo, links, lynx and w3m. - - -Cons ----- - -- Not suitable for large repositories (2000+ commits), because diffstats are - an expensive operation, the cache (-c flag) is a workaround for this in - some cases. -- Not suitable for large repositories with many files, because all files are - written for each execution of stagit. This is because stagit shows the lines - of textfiles and there is no "cache" for file metadata (this would add more - complexity to the code). -- Not suitable for repositories with many branches, a quite linear history is - assumed (from HEAD). - - In these cases it is better to just use cgit or possibly change stagit to - run as a CGI program. - -- Relatively slow to run the first time (about 3 seconds for sbase, - 1500+ commits), incremental updates are faster. -- Does not support some of the dynamic features cgit has, like: - - Snapshot tarballs per commit. - - File tree per commit. - - History log of branches diverged from HEAD. - - Stats (git shortlog -s). - - This is by design, just use git locally. +┌─────┐ +│~sgw~│ +└─────┘ + +Static Git Webpage (Generator). + +Build: + Run "make" to create the gitsta binary. Then, run "make install" as root + to install, if that's what you want. + +Documentation/Usage: + Calling gitsta with the --help option will print a usage guide. For + further info, you can read the man page via "mandoc gitsta.1" or simply + "man gitsta" (if you installed it). + +Dependencies: + - C compiler (C99) + - libc (tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl) + - libgit2 (v0.22+) + - POSIX make (optional) + +Tool Features: + - Allows specifying output directory + - Can generate repository pages, index page, or both. + - Minimal options at runtime + +Webpage Features: + - customizable via config.h: + - header/footer + - webpage structure: link destinations, page locations, etc. + - nested directory pages (or not), whether to show backlinks + - Presents log of all commits from HEAD + - Presents file tree at HEAD + - Text files have linkable line numbers + - Binary files have preview (via object tag) and download link. + - Links README, LICENSE, and submodules in headers. + - Atom feeds for the commit log (atom.xml) and tags/refs (tags.xml). + - Compatible with text-browsers such as dillo, links, lynx and w3m. + +TODO: + See TODO. diff --git a/TODO b/TODO @@ -4,19 +4,19 @@ [ ] Separate out stagit.c into clean, standalone modules. - but how to handle includes/headers... -[ ] Redesign options / I/O +[x] Redesign options / I/O [x] create config.h file [x] reused document chunks (header, footer, logo, etc) [ ] nested (whether to show all files on files.html) [x] backlinks (whether to show .. in directories) - [ ] commit depth limit + [x] commit depth limit ---NEW FEATURES--- [x] include binary files in static site + link to them [x] add support for projects with folders in them [x] backlink on subdirectory pages -[ ] add lines of code totals on files page +[x] add lines of code totals on files page [x] add lines of code on each file's page [x] show line/byte totals for each directory [x] tweak paths that start with dots, so that they don't get hidden or 403'd @@ -30,6 +30,7 @@ [x] Correctly link to repo pages from index page, based on configuration/baseurl [ ] Separate repo page generation out of main.c [ ] Remove globals / replace them with passed structs +[ ] Update man page [ ] Redo caching, to minimally repeat work - but it needs to also not increase the complexity. diff --git a/config.h b/config.h @@ -1,27 +1,24 @@ -/* Only macros! Nice and clean :) */ +/* Only macros! :) */ -/* CONFIG FOR INDEX PAGE */ -// TODO uniformly expose repo data via struct +#define INDEX_DEST "index.html" #define INDEX_LINK readme == NULL ? "files.html" : "files/%s.html", readme -#define INDEX_LOGO_SRC "/logo.png" +// TODO index header/footer? +#define INDEX_STYLE_HREF "/style.css" // TODO wire +#define INDEX_LOGO_SRC "/logo.webp" #define INDEX_LOGO_HREF "" -/* CONFIG FOR REPOSITORY PAGES */ -#define HEADER "<p>bonjour!</p>" -#define FOOTER "<p>ciao!</p>" +#define PAGE_DEST "%s", repo_name +#define PAGE_HEADER "<p>bonjour!</p>" +#define PAGE_FOOTER "<p><i>This webpage is intended to be an accessible " \ +"preview of this repository. To get a fuller picture, clone it and use the " \ +"git CLI." +#define PAGE_STYLE_HREF "/style.css" +#define PAGE_LOGO_SRC "/logo.webp" +#define PAGE_LOGO_HREF "/index.html" -/* Relative to the output directory (see -o). */ -// TODO make this being relative EXPLICIT here (but make sure the slashes still work out) -#define PAGE_DEST "%s", repo_name /* where pages get saved */ - -/* Absolute within the webpage */ -#define STYLE_HREF "/style.css" -#define LOGO_SRC "/logo.png" -#define LOGO_HREF "/index.html" - -#define DIR_PAGES 1 /* generate pages for each directory */ -#define BACKLINKS 0 /* include ".." in directories */ -#define MAX_COMMITS 100 +#define DIR_PAGES 0 /* generate a page for each dir in a repo */ +#define BACKLINKS 1 /* include ".." in directories */ +#define MAX_COMMITS 10 #define LICENSE_FILES "HEAD:LICENSE", "HEAD:LICENSE.md", \ "HEAD:LICENSE.txt", "HEAD:COPYING" diff --git a/index.c b/index.c @@ -76,7 +76,6 @@ int put_index_log( FILE *fp, git_repository *repo, - const char *owner, const char *description, const char *repo_name, const char *readme @@ -125,15 +124,13 @@ find_owner() { FILE *fp; char path[PATH_MAX]; - - // TODO use git_fopen fp = git_fopen(path, sizeof(path), ".git/owner"); owner[0] = '\0'; if (fp) { if (!fgets(owner, sizeof(owner), fp)) owner[0] = '\0'; - check_file_error(fp, "owner", 'r'); + check_file_error(fp, path, 'r'); fclose(fp); owner[strcspn(owner, "\n")] = '\0'; } diff --git a/main.c b/main.c @@ -78,21 +78,19 @@ static git_repository *repo; /* flags + arguments */ static Opt opt; -static const char *repodir; +static const char *repo_dir; static const char *license_files[] = { LICENSE_FILES }; static const char *readme_files[] = { README_FILES }; // TODO extract these into a struct. LMFAO static char *name = ""; -static char owner[255]; static char *repo_name = ""; static char description[255]; static char clone_url[1024]; static char *submodules; static const char *license; static const char *readme; -static long long nlogcommits = -1; /* -1 indicates not used */ /* bad globals for index.... */ // TODO move this stuff to index.h? @@ -100,7 +98,7 @@ static FILE *index_fp; static char index_fname[255] = "index.html"; void find_owner(); -int put_index_log(FILE *fp, git_repository *repo, const char *owner, const char *description, const char *repo_name, const char *readme); +int put_index_log(FILE *fp, git_repository *repo, const char *description, const char *repo_name, const char *readme); void put_index_header(FILE *fp); void put_index_footer(FILE *fp); @@ -162,11 +160,11 @@ git_fopen(char *path, int path_size, const char *file) /* try dir/(whatever), */ /* (+5 removes the leading '.git/'.) */ - join_path(path, path_size, repodir, file + 5); + join_path(path, path_size, repo_dir, file + 5); if ((ret = fopen(path, "r"))) return ret; /* then try dir/.git/(whatever). */ - join_path(path, path_size, repodir, file); + join_path(path, path_size, repo_dir, file); return fopen(path, "r"); } @@ -632,18 +630,18 @@ put_header(FILE *fp, const char *title) SPUTF(fp, "\" />" "\n"); SPUTF(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\""); - fprintf(fp, STYLE_HREF); + fprintf(fp, PAGE_STYLE_HREF); SPUTF(fp, "\" />" "\n"); SPUTF(fp, "</head>" "\n" "<body>" "\n"); - fprintf(fp, HEADER); + fprintf(fp, PAGE_HEADER); SPUTF(fp, "<table>" "<tr><td>"); SPUTF(fp, "<a href=\""); - fprintf(fp, LOGO_HREF); - SPUTF(fp, "\"><img alt=\"logo\" src=\""); - fprintf(fp, LOGO_SRC); + fprintf(fp, PAGE_LOGO_HREF); + SPUTF(fp, "\"><img alt=\"logo\" width=\"32\" height=\"32\" src=\""); + fprintf(fp, PAGE_LOGO_SRC); SPUTF(fp, "\"></a>"); SPUTF(fp, "</td><td>" "<h1>"); @@ -703,7 +701,7 @@ void put_footer(FILE *fp) { SPUTF(fp, "</div>" "\n"); - fprintf(fp, FOOTER); + fprintf(fp, PAGE_FOOTER); SPUTF(fp, "</body>" "\n" "</html>" "\n"); } @@ -967,10 +965,12 @@ put_log(FILE *fp, const git_oid *oid) char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; size_t remcommits = 0; int r; + int commits_left = MAX_COMMITS; git_revwalk_new(&w, repo); git_revwalk_push(w, oid); + // TODO avoid processing very large commits while (!git_revwalk_next(&id, w)) { git_oid_tostr(oidstr, sizeof(oidstr), &id); r = snprintf(path, sizeof(path), "commits/%s.html", oidstr); @@ -980,7 +980,7 @@ put_log(FILE *fp, const git_oid *oid) /* optimization: if there are no log lines to write and the commit file already exists: skip the diffstat */ - if (!nlogcommits) { + if (!commits_left) { remcommits++; if (!r) continue; @@ -992,10 +992,10 @@ put_log(FILE *fp, const git_oid *oid) if (commit_info_get_stats(ci) == -1) goto err; - if (nlogcommits != 0) { + if (commits_left != 0) { put_log_line(fp, ci); - if (nlogcommits > 0) - nlogcommits--; + if (commits_left > 0) + commits_left--; } /* check if file exists if so skip it */ @@ -1006,7 +1006,7 @@ err: } git_revwalk_free(w); - if (nlogcommits == 0 && remcommits != 0) + if (commits_left == 0 && remcommits != 0) fprintf( fp, "<tr><td></td><td colspan=\"5\">" @@ -1253,7 +1253,7 @@ write_blob( struct Weight ret; ret.lines = 0; - ret.bytes = git_blob_rawsize(blob); + ret.bytes = 0; get_page_path(fname, sizeof(fname), opt.out_dir, filename); fp = fopen_w(fname); @@ -1264,6 +1264,7 @@ write_blob( if (git_blob_is_binary(blob)) { put_binary_blob_stub(fp, entrypath); // name); write_binary_file(entrypath, blob); + ret.bytes = git_blob_rawsize(blob); } else { ret.lines = put_file_html(fp, blob); } @@ -1471,10 +1472,6 @@ put_file_list(FILE *fp, git_tree *tree, const char *path) ) return ret; - if (name[0] == '.' && opt.hide_hidden) - continue; - - /* entrypath = path + name */ combine_paths(entrypath, path, name); if (!git_tree_entry_to_object(&obj, repo, entry)) { @@ -1528,11 +1525,13 @@ write_filetree(const char *path, const char *filename, git_tree *tree) SPUTF(fp, "</tbody></table>"); // TODO how can this get printed before the contents... + /* SPUTF(fp, "<p>Totals: "); put_size(fp, ret.lines, 0); SPUTF(fp, "L "); put_size(fp, ret.bytes, 0); SPUTF(fp, "B</p>"); + */ put_footer(fp); check_file_error(fp, fname, 'w'); @@ -1722,19 +1721,37 @@ find_clone_url() } } +void +find_repo_name() +{ + char abs_dir[PATH_MAX + 1]; + char *p; + + /* set abs_dir */ + if (!realpath(repo_dir, abs_dir)) + err(1, "realpath"); + + if ((name = strrchr(abs_dir, '/'))) name++; + else name = ""; + + if (!(repo_name = strdup(name))) err(1, "strdup"); + if ((p = strrchr(repo_name, '.'))) + if (!strcmp(p, ".git")) + *p = '\0'; + + if (repo_name[0] == '\0') + repo_name = "untitled"; +} + int write_repo_pages(const char* dir) { - char abs_dir[PATH_MAX + 1]; /* absolute path of dir */ // TODO fully stop using globals like this.... - repodir = dir; - - if (!realpath(dir, abs_dir)) - err(1, "realpath"); + repo_dir = dir; #ifdef __OpenBSD__ - if (unveil(repodir, "r") == -1) err(1, "unveil: %s", repodir); + if (unveil(repo_dir, "r") == -1) err(1, "unveil: %s", repo_dir); if (unveil(".", "rwc") == -1) err(1, "unveil: ."); /* @@ -1749,7 +1766,7 @@ write_repo_pages(const char* dir) /* open the git repo */ int result = git_repository_open_ext( - &repo, repodir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL + &repo, repo_dir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL ); if (result < 0) { fprintf(stderr, "fatal: cannot open repository '%s'" "\n", dir); @@ -1763,20 +1780,9 @@ write_repo_pages(const char* dir) head = git_object_id(obj); git_object_free(obj); - /* use directory name as name */ - if ((name = strrchr(abs_dir, '/'))) name++; - else name = ""; - - /* strip .git suffix */ - char *p; - if (!(repo_name = strdup(name))) err(1, "strdup"); - if ((p = strrchr(repo_name, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - if (repo_name[0] == '\0') - repo_name = "untitled"; - /* set global vars... */ + // TODO repo = open_repo(); + find_repo_name(); find_description(); find_clone_url(); find_license(); @@ -1784,15 +1790,20 @@ write_repo_pages(const char* dir) find_submodules(); if (opt.index) find_owner(); - /* write pages! */ - write_log_page(head); - walk_git_tree(head); - write_refs_page(); - write_feed(); - write_releases_feed(); - if (opt.index) put_index_log(index_fp, repo, owner, description, repo_name, readme); + if (opt.index) { + put_index_log(index_fp, repo, description, repo_name, readme); + } + + if (opt.pages) { + write_log_page(head); + walk_git_tree(head); + write_refs_page(); + write_feed(); + write_releases_feed(); + } /* clean up */ + // TODO close_repo(); git_repository_free(repo); return 0; @@ -1801,8 +1812,12 @@ write_repo_pages(const char* dir) void begin_index() { - // TODO populate index_fname correctly based on configs - get_index_page_path(index_fname, sizeof(index_fname), opt.out_dir, "index.html"); + get_index_page_path( + index_fname, + sizeof(index_fname), + opt.out_dir, + INDEX_DEST + ); index_fp = fopen_w(index_fname); put_index_header(index_fp); } @@ -1833,20 +1848,25 @@ init_lib_git() int main(int argc, const char *argv[]) { - int i; + int i, result; if (parse_opts(&opt, argc, argv) == OPT_FAIL) { PRINT_USAGE(argv[0]); return 1; } + if (!opt.pages && !opt.index) { + printf("Nothing to do, use -i and/or -p." "\n\n"); + PRINT_USAGE(argv[0]); + return 1; + } + init_lib_git(); if (opt.index) begin_index(); for (i = 0; i < opt.repo_count; i++) { - printf("Writing repository: %s" "\n", opt.repo_dirs[i]); - if(write_repo_pages(opt.repo_dirs[i]) > 0) - return 1; + result = write_repo_pages(opt.repo_dirs[i]); + if(result > 0) return 1; } if (opt.index) end_index(); diff --git a/opt.c b/opt.c @@ -18,11 +18,11 @@ parse_opt(Opt *opt, const char *cur, const char *next) if (flag == '\0') break; switch (flag) { - case 'h': opt->hide_hidden = 1; break; - case 'i': opt->index = next; return OPT_SKIP; + case 'i': opt->index = 1; break; + case 'p': opt->pages = 1; break; case 'o': opt->out_dir = next; return OPT_SKIP; - case 'c': opt->cache = next; return OPT_SKIP; case 'b': opt->base_url = next; return OPT_SKIP; + case 'c': opt->cache = next; return OPT_SKIP; default: ERROR("Unrecognized flag: -%c." "\n\n", flag); } @@ -42,9 +42,10 @@ parse_opts(Opt *opt, int argc, const char * argv[]) if (argc < 2) ERROR(""); /* set default values */ + opt->index = 0; + opt->pages = 0; opt->base_url = ""; opt->out_dir = ""; - opt->index = NULL; for (index = 1;;) { cur = argv[index]; diff --git a/opt.h b/opt.h @@ -7,28 +7,27 @@ #define ERROR(...) { fprintf(stderr, __VA_ARGS__); return OPT_FAIL; } #define USAGE \ -"Usage: %s [-h] [-o OUT] [-b BASE] [-i INDEX] [-c CACHE] REPO [REPO2 [...]]" "\n" \ -" -h: omit files and directories beginning with '.'" "\n" \ -" -o: write output into dir OUT (default is pwd)" "\n" \ -" -b: use base url BASE when creating links" "\n" \ -" -i: generate index file at OUT/INDEX" "\n" \ -" -c: utilize dir CACHE for caching" "\n" \ +"Usage: %s [-i] [-p] [-o DIR] [-b URL] [-c DIR] REPO [REPO2 [...]]" "\n" \ +" -i: generate index file" "\n" \ +" -p: generate repository pages" "\n" \ +" -o: specify output dir" "\n" \ +" -b: specify base url" "\n" \ +" -c: specify cache dir" "\n" \ "\n" #define PRINT_USAGE(argv0) fprintf(stderr, USAGE, argv0); typedef struct _ { - int hide_hidden; - - int repo_count; - const char **repo_dirs; + int index; /* 0 or 1 */ + int pages; /* 0 or 1 */ const char *cache; const char *out_dir; - const char *index; - const char *base_url; + + int repo_count; + const char **repo_dirs; } Opt; int parse_opts(Opt *opt, int argc, const char *argv[]);

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.