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:
| M | README | 227 | +++++++++++++++---------------------------------------------------------------- |
| M | TODO | 7 | ++++--- |
| M | config.h | 35 | ++++++++++++++++------------------- |
| M | index.c | 5 | +---- |
| M | main.c | 130 | ++++++++++++++++++++++++++++++++++++++++++++++---------------------------------- |
| M | opt.c | 9 | +++++---- |
| M | opt.h | 23 | +++++++++++------------ |
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[]);