so://phie

soso

My pretty neat website
git clone git://soophie.de/soso

feat: Initial implementation


commit: dc8c1848e143bc9c11abeaba2740de148a39c6ca
parent: 8c18da0792e9fa4730e244d5874bc9e246e434f1
author: Sophie <info@soophie.de>
time: 2026-10-08 22:30

diff --git a/.clangd b/.clangd

new file mode 100644

index 0000000..873a35e

--- /dev/null

+++ b/.clangd

1@@ -0,0 +1,12 @@

2+CompileFlags:

3+ Add:

4+ - -Wall

5+ - -Wextra

6+ - -Werror

7+ - -pedantic

8+ - -I/root/git/sock/src

9+ - -I/root/git/tpee/src

10+ - -I/root/git/tpee_ext/src

11+ - -I/root/git/buf/src

12+ - -I/root/git/orb/src

13+ - -I/root/git/md4c/src

14

15

diff --git a/.gitignore b/.gitignore

new file mode 100644

index 0000000..1cd86dc

--- /dev/null

+++ b/.gitignore

1@@ -0,0 +1,2 @@

2+src/templ

3+soso

4

5

diff --git a/Makefile b/Makefile

new file mode 100644

index 0000000..b20b1ec

--- /dev/null

+++ b/Makefile

1@@ -0,0 +1,71 @@

2+CC := cc

3+CFLAGS := -Wall -Wextra -Werror -pedantic

4+CFLAGS_RELEASE := -O2 -DNDEBUG

5+CFLAGS_DEBUG := -g3 -O0

6+# NOTE: exposes header impls into dynamically linked modules

7+LDFLAGS := -rdynamic

8+LDLIBS := \

9+ -lm \

10+ -lssl \

11+ -lgit2

12+DEPS := \

13+ -I$(HOME)/git/sock/src \

14+ -I$(HOME)/git/tpee/src \

15+ -I$(HOME)/git/tpee_ext/src \

16+ -I$(HOME)/git/buf/src \

17+ -I$(HOME)/git/orb/src \

18+ -I$(HOME)/git/md4c/src

19+SOURCE := \

20+ src/main.c \

21+ $(HOME)/git/md4c/src/*.c \

22+ src/module/page.c \

23+ src/module/default.c \

24+ src/module/index.c \

25+ src/module/git_list.c \

26+ src/module/git_repo.c \

27+ src/module/git_repo_log.c \

28+ src/module/git_repo_tree.c \

29+ src/module/git_repo_refs.c \

30+ src/module/git_repo_commit.c \

31+ src/module/git_repo_blob.c

32+VALGRIND := valgrind \

33+ --leak-check=full \

34+ --show-leak-kinds=all \

35+ --track-origins=yes

36+TARGET := soso

37+

38+export DUC_HEADERPATH := ./src/templ/

39+export DUC_BUILDPATH := ./src/templ/

40+export DUC_LIBPATH := $(CFLAGS) $(DEPS)

41+

42+all: build

43+

44+build-templ:

45+ mkdir -p ./src/templ

46+ifdef TEMPL

47+ ductape ./templ/$(TEMPL).html

48+else

49+ ductape ./templ/*.html

50+endif

51+

52+build: $(SOURCE)

53+ $(CC) -o $(TARGET) $(CFLAGS) $(DEPS) $(LDFLAGS) $^ $(LDLIBS)

54+

55+build-release: $(SOURCE)

56+ $(CC) -o $(TARGET) $(CFLAGS) $(DEPS) $(CFLAGS_RELEASE) $(LDFLAGS) $^ $(LDLIBS)

57+

58+build-debug: $(SOURCE)

59+ $(CC) -o $(TARGET) $(CFLAGS) $(DEPS) $(CFLAGS_DEBUG) $(LDFLAGS) $^ $(LDLIBS)

60+

61+run: build

62+ ./$(TARGET)

63+

64+run-release: build-release

65+ ./$(TARGET)

66+

67+run-debug: build

68+ $(VALGRIND) ./$(TARGET)

69+

70+clean:

71+ rm -f ./$(TARGET)

72+ rm -rf ./src/templ

73

74

diff --git a/asset/font/Inconsolata-Variable.ttf b/asset/font/Inconsolata-Variable.ttf

new file mode 100644

index 0000000..95ad718

Binary files /dev/null and b/asset/font/Inconsolata-Variable.ttf differ

diff --git a/asset/style/main.css b/asset/style/main.css

new file mode 100644

index 0000000..dd52159

--- /dev/null

+++ b/asset/style/main.css

1@@ -0,0 +1,298 @@

2+@font-face {

3+ font-family: 'Inconsolata';

4+ src: url('/asset/font/Inconsolata-Variable.ttf') format('truetype');

5+}

6+

7+:root {

8+ --ft-mono: 'Inconsolata', monospace;

9+ --ft-size: 14px;

10+ --ft-weight: 400;

11+}

12+

13+* {

14+ font-family: var(--ft-mono);

15+ box-sizing: border-box;

16+}

17+

18+html {

19+

20+ body {

21+ max-width: 900px;

22+ margin: 4rem auto;

23+ margin-bottom: 6rem;

24+ font-size: var(--ft-size);

25+ font-weight: var(--ft-weight);

26+ }

27+}

28+

29+nav {

30+ border-bottom: 1px solid #bbbbbb;

31+ padding-bottom: 1rem;

32+}

33+

34+pre {

35+ white-space: pre-wrap;

36+}

37+

38+ul.row-list {

39+ padding: 0px;

40+ width: 100%;

41+

42+ li.row {

43+ display: flex;

44+ gap: 0px 0.75rem;

45+ margin: 0px;

46+ padding: 0px;

47+ list-style-type: none;

48+

49+ &.row--head {

50+ margin-bottom: 0.5rem;

51+ }

52+

53+ span.col {

54+ display: inline-block;

55+

56+ &.col--oid {

57+ min-width: 7ch;

58+ max-width: 7ch;

59+ }

60+

61+ &.col--time {

62+ min-width: 10ch;

63+ max-width: 10ch;

64+ }

65+

66+ &.col--mode {

67+ min-width: 10ch;

68+ max-width: 10ch;

69+ }

70+

71+ &.col--owner {

72+ min-width: 20ch;

73+ max-width: 20ch;

74+ }

75+

76+ &.col--author {

77+ min-width: 20ch;

78+ max-width: 20ch;

79+ }

80+

81+ &.col--size {

82+ min-width: 20ch;

83+ max-width: 20ch;

84+ }

85+

86+ &.col--message {

87+ min-width: 50ch;

88+ flex: 1;

89+ }

90+

91+ &.col--description {

92+ min-width: 50ch;

93+ flex: 1;

94+ }

95+ }

96+ }

97+

98+ &.list--tree li.row span.col {

99+

100+ &.col--name {

101+ min-width: 50ch;

102+ flex: 1;

103+ }

104+ }

105+

106+ &.list--repo li.row span.col {

107+

108+ &.col--name {

109+ min-width: 30ch;

110+ max-width: 30ch;

111+ }

112+ }

113+

114+ &.list--refs li.row span.col {

115+

116+ &.col--name {

117+ min-width: 30ch;

118+ max-width: 30ch;

119+ }

120+ }

121+}

122+

123+.git--repo {

124+ margin-top: 2rem;

125+

126+ hr {

127+ border: none;

128+ border-top: 1px solid #bbbbbb;

129+ }

130+

131+ ul.link-list {

132+ display: flex;

133+ gap: 0px 0rem;

134+ padding: 0px;

135+ border-bottom: 1px solid #bbbbbb;

136+ margin-top: 2rem;

137+ margin-bottom: 0.5rem;

138+

139+ li.link {

140+ margin: 0px;

141+ margin-bottom: -1px;

142+ list-style-type: none;

143+ min-width: 12ch;

144+ border: 1px solid #bbbbbb;

145+

146+ &:not(:first-of-type) {

147+ margin-left: -1px;

148+ }

149+

150+ &.link--active {

151+ border-bottom: 1px solid #ffffff;

152+ z-index: 1;

153+ }

154+

155+ &:not(.link--active) {

156+ background-color: #eeeeee;

157+ }

158+

159+ a {

160+ display: block;

161+ padding: 0.4rem 1rem;

162+ padding-bottom: 0.5rem;

163+ text-align: center;

164+ }

165+ }

166+ }

167+

168+ select.repo--branch {

169+ margin: 0.5rem 0px;

170+ border: 1px solid #999999;

171+ padding: 0.25rem 0.5rem;

172+ cursor: pointer;

173+ min-width: 30ch;

174+

175+ &:hover {

176+ border-color: #777777;

177+ }

178+ }

179+

180+ .repo--tab {

181+ padding-top: 0.5rem;

182+ }

183+}

184+

185+.commit-diff {

186+ margin-top: 2rem;

187+

188+ pre {

189+ margin: 1rem 0px;

190+

191+ &.diff--meta {

192+ padding-top: 0.25rem;

193+ padding-bottom: 0.25rem;

194+ padding-left: 1ch;

195+ border: 1px solid #bbbbbb;

196+ background-color: #eeeeee;

197+

198+ &:has(+ pre.diff--code) {

199+ margin-bottom: -1rem;

200+ border-bottom: 0px;

201+ }

202+

203+ p {

204+ margin: 0px;

205+

206+ &:not(:first-of-type) {

207+ display: none;

208+ }

209+ }

210+ }

211+

212+ &.diff--code {

213+ padding-top: 0.5rem;

214+ padding-bottom: 0.5rem;

215+ border: 1px solid #bbbbbb;

216+

217+ p {

218+ display: flex;

219+ margin: 0px;

220+

221+ &:last-of-type {

222+ display: none;

223+ }

224+

225+ &.is-hunk {

226+ color: #5252ff;

227+ }

228+

229+ &.is-add {

230+

231+ span.ln {

232+ border-left: 3px solid #77d877;

233+ }

234+ }

235+

236+ &.is-rem {

237+

238+ span.ln {

239+ border-left: 3px solid #ff7f7f;

240+ }

241+ }

242+

243+ &:not(:hover) {

244+

245+ &.is-add {

246+ color: #2e862e;

247+ }

248+

249+ &.is-rem {

250+ color: #932323;

251+ }

252+

253+ &:target {

254+ background-color: #ffff9a !important;

255+

256+ span.ln {

257+

258+ a {

259+ color: #666666;

260+ }

261+ }

262+ }

263+ }

264+

265+ &:hover {

266+ background-color: #e1e1e1;

267+

268+ span.ln {

269+

270+ a {

271+ color: #666666;

272+ }

273+ }

274+ }

275+

276+ span.ln {

277+ min-width: max-content;

278+ user-select: none;

279+ padding-left: 1ch;

280+ margin-right: 2ch;

281+ border-left: 3px solid transparent;

282+

283+ a {

284+ text-decoration: none;

285+ color: #888888;

286+

287+ &:hover {

288+ text-decoration: underline;

289+ }

290+ }

291+ }

292+

293+ span.line {

294+

295+ }

296+ }

297+ }

298+ }

299+}

300

301

diff --git a/src/duc_macro.h b/src/duc_macro.h

new file mode 100644

index 0000000..5a0f426

--- /dev/null

+++ b/src/duc_macro.h

1@@ -0,0 +1,30 @@

2+#pragma once

3+

4+#include <buf.h>

5+#include <orb.h>

6+#include "util.h"

7+

8+#define DUC_OUT ((orb_ctx_t *) out)

9+#define DUC_WRITE(str) { buf_write(DUC_OUT->out, str); }

10+

11+#define HTML(str) DUC_WRITE(str ? str : "")

12+#define STRING(str) { \

13+ buf_t buf = BUF_NULL; \

14+ buf_write(&buf, str); \

15+ buf_repall(&buf, "&", "&amp;"); \

16+ buf_repall(&buf, "<", "&lt;"); \

17+ buf_repall(&buf, ">", "&gt;"); \

18+ buf_concat(DUC_OUT->out, buf); \

19+ buf_free(&buf); \

20+}

21+#define PRINTF(fmt, ...) { buf_writef(DUC_OUT->out, fmt, __VA_ARGS__); }

22+#define NUMBER(num) PRINTF("%d", num)

23+#define FORMAT_TIME(time, fmt) { \

24+ struct tm *tm = localtime(time); \

25+ char buffer[100]; \

26+ int len = strftime(buffer, 100, fmt, tm); \

27+ buffer[len] = '\0'; \

28+ STRING(buffer) \

29+}

30+

31+#define MARKDOWN(str) { util_md_html(str, DUC_OUT->out); }

32

33

diff --git a/src/main.c b/src/main.c

new file mode 100644

index 0000000..2aa0013

--- /dev/null

+++ b/src/main.c

1@@ -0,0 +1,227 @@

2+#include <unistd.h>

3+#include <signal.h>

4+#include <execinfo.h>

5+#include <git2.h>

6+

7+#define TPEE_IMPL

8+#include <tpee.h>

9+#define TPEE_EXT_IMPL

10+#include <tpee_ext.h>

11+#define BUF_IMPL

12+#include <buf.h>

13+#define ORB_IMPL

14+#include <orb.h>

15+#define UTIL_IMPL

16+#include "util.h" // IWYU pragma: keep

17+

18+#define PORT 8087

19+

20+#include "module/page.h"

21+#include "module/default.h"

22+#include "module/index.h"

23+#include "module/git_list.h"

24+#include "module/git_repo.h"

25+

26+TPEE_GET("/", index_page) {

27+ (void) req;

28+ buf_t out = BUF_NULL;

29+ orb_ctx_t ctx = orb_ctx_create(&out);

30+ orb_module_t index_module = orb_module_create(&INDEX_TYPE, NULL);

31+ orb_module_t page_module = orb_module_create(&PAGE_TYPE, &((page_param_list_t) {

32+ .title = "Index",

33+ .module = &index_module,

34+ }));

35+ orb_module_run(&page_module, &ctx);

36+ tpee_resp_t resp = tpee_resp_create(TPEE_STATUS_OK);

37+ tpee_resp_body(&resp, (char *) out.bytes, out.len);

38+ buf_free(&out);

39+ return resp;

40+}

41+

42+TPEE_GET("/git", git_list_page) {

43+ (void) req;

44+ buf_t out = BUF_NULL;

45+ orb_ctx_t ctx = orb_ctx_create(&out);

46+ orb_module_t git_list_module = orb_module_create(&GIT_LIST_TYPE, &((git_list_param_list_t) {

47+ .path = "/home/git",

48+ }));

49+ orb_module_t page_module = orb_module_create(&PAGE_TYPE, &((page_param_list_t) {

50+ .title = "Index",

51+ .module = &git_list_module,

52+ }));

53+ orb_module_run(&page_module, &ctx);

54+ tpee_resp_t resp = tpee_resp_create(TPEE_STATUS_OK);

55+ tpee_resp_body(&resp, (char *) out.bytes, out.len);

56+ buf_free(&out);

57+ return resp;

58+}

59+

60+TPEE_GET("/git/*", git_repo_page) {

61+ char *repo_name = tpee_ext_get_path_var(req->url, "/git/{repo_name}", "repo_name");

62+ if (!repo_name) return tpee_resp_create(TPEE_STATUS_NOT_FOUND);

63+ char *tab_mode = NULL;

64+ if (tpee_ext_is_path_match(req->url, "/git/{repo_name}/{tab_mode}")) {

65+ tab_mode = tpee_ext_get_path_var(req->url, "/git/{repo_name}/{tab_mode}", "tab_mode");

66+ }

67+ if (!tab_mode) {

68+ tab_mode = strdup("log");

69+ }

70+ const char *tab_mode_list[] = { "log", "tree", "refs", "commit", "blob" };

71+ size_t tab_mode_list_len = sizeof(tab_mode_list) / sizeof(tab_mode_list[0]);

72+ bool is_tab_mode_valid = false;

73+ for (size_t i = 0; i < tab_mode_list_len; i++) {

74+ if (strcmp(tab_mode, tab_mode_list[i]) == 0) {

75+ is_tab_mode_valid = true;

76+ break;

77+ }

78+ }

79+ if (!is_tab_mode_valid) {

80+ return tpee_resp_create(TPEE_STATUS_NOT_FOUND);

81+ }

82+ const char *git_base_path = "/home/git/";

83+ size_t repo_path_len = strlen(git_base_path) + strlen(repo_name) + 1;

84+ char repo_path[repo_path_len];

85+ snprintf(repo_path, repo_path_len, "%s%s", git_base_path, repo_name);

86+ tpee_pairs_t query_list = tpee_req_queries(req);

87+ buf_t out = BUF_NULL;

88+ orb_ctx_t ctx = orb_ctx_create(&out);

89+ orb_module_t git_repo_module = orb_module_create(&GIT_REPO_TYPE, &((git_repo_param_list_t) {

90+ .repo_name = repo_name,

91+ .repo_path = repo_path,

92+ .tab_mode = tab_mode,

93+ .ref = tpee_pairs_get(&query_list, "ref"),

94+ .oid = tpee_pairs_get(&query_list, "oid"),

95+ }));

96+ orb_module_t page_module = orb_module_create(&PAGE_TYPE, &((page_param_list_t) {

97+ .title = repo_name,

98+ .module = &git_repo_module,

99+ }));

100+ orb_module_run(&page_module, &ctx);

101+ tpee_pairs_free(&query_list);

102+ if (tab_mode) {

103+ free(tab_mode);

104+ }

105+ free(repo_name);

106+ if (ctx.rc.is_fatal) {

107+ tpee_resp_t resp = tpee_resp_create(TPEE_STATUS_NOT_FOUND);

108+ buf_free(&out);

109+ return resp;

110+ }

111+ tpee_resp_t resp = tpee_resp_create(TPEE_STATUS_OK);

112+ tpee_resp_body(&resp, (char *) out.bytes, out.len);

113+ buf_free(&out);

114+ return resp;

115+}

116+

117+// #include "registry.h"

118+

119+// TPEE_GET("/orb/module/*", orb_module_serve) {

120+// char *module_id = tpee_ext_get_path_var(req->url, "/orb/module/{module_id}", "module_id");

121+// tpee_resp_t resp = tpee_resp_create(TPEE_STATUS_NOT_FOUND);

122+// if (module_id) {

123+// const orb_module_type_t *module_type = NULL;

124+// for (size_t i = 0; i < REGISTRY_LEN; i++) {

125+// const registry_entry_t *entry = &REGISTRY[i];

126+// if (strcmp(module_id, entry->id) == 0) {

127+// module_type = entry->type;

128+// break;

129+// }

130+// }

131+// if (module_type) {

132+// buf_t out = BUF_NULL;

133+// orb_ctx_t ctx = orb_ctx_create(&out);

134+// void *param_list = NULL;

135+// tpee_pairs_t query_list = tpee_req_queries(req);

136+// if (module_type == &INDEX_TYPE) {

137+// param_list = calloc(1, sizeof(index_param_list_t));

138+// index_param_list_t *index_param_list = param_list;

139+// index_param_list->title = tpee_pairs_get(&query_list, "title");

140+// }

141+// if (module_type == &ABOUT_TYPE) {

142+// param_list = calloc(1, sizeof(about_param_list_t));

143+// about_param_list_t *about_param_list = param_list;

144+// about_param_list->headline = tpee_pairs_get(&query_list, "headline");

145+// about_param_list->subline = tpee_pairs_get(&query_list, "subline");

146+// }

147+// orb_module_t module = orb_module_create(module_type, param_list);

148+// orb_module_run(&module, &ctx);

149+// tpee_pairs_free(&query_list);

150+// if (param_list) {

151+// free(param_list);

152+// }

153+// resp.status = TPEE_STATUS_OK;

154+// tpee_resp_body(&resp, (char *) out.bytes, out.len);

155+// buf_free(&out);

156+// }

157+// free(module_id);

158+// }

159+// return resp;

160+// }

161+

162+TPEE_GET("/asset/*", asset_serve) {

163+ const char *prefix = "/asset/";

164+ const char *filepath = "./asset/";

165+ size_t filename_len = strlen(filepath) + strlen(req->url) - strlen(prefix) + 1;

166+ char filename[filename_len];

167+ snprintf(filename, filename_len, "%s%s", filepath, req->url + strlen(prefix));

168+ tpee_resp_t resp = tpee_ext_serve_file(filename);

169+ return resp;

170+}

171+

172+TPEE_DEFAULT(default_page) {

173+ (void) req;

174+ buf_t out = BUF_NULL;

175+ orb_ctx_t ctx = orb_ctx_create(&out);

176+ orb_module_t default_module = orb_module_create(&DEFAULT_TYPE, NULL);

177+ orb_module_t page_module = orb_module_create(&PAGE_TYPE, &((page_param_list_t) {

178+ .title = "404",

179+ .module = &default_module,

180+ }));

181+ orb_module_run(&page_module, &ctx);

182+ tpee_resp_t resp = tpee_resp_create(TPEE_STATUS_OK);

183+ tpee_resp_body(&resp, (char *) out.bytes, out.len);

184+ buf_free(&out);

185+ return resp;

186+}

187+

188+void on_signal(int signal) {

189+ (void) signal;

190+ // do nothing

191+}

192+

193+void on_segfault(int signal) {

194+ void *list[10];

195+ size_t size = backtrace(list, 10);

196+ fprintf(stderr, "ERROR: signal %d:\n", signal);

197+ backtrace_symbols_fd(list, size, STDOUT_FILENO);

198+ exit(1);

199+}

200+

201+int main(void) {

202+ struct sigaction action;

203+ action.sa_handler = on_signal;

204+ action.sa_flags = 0;

205+ sigemptyset(&action.sa_mask);

206+ sigaction(SIGINT, &action, NULL);

207+ struct sigaction sigint_action;

208+ sigint_action.sa_handler = on_segfault;

209+ sigint_action.sa_flags = 0;

210+ sigemptyset(&sigint_action.sa_mask);

211+ sigaction(SIGSEGV, &sigint_action, NULL);

212+ tpee_t *tpee = tpee_init();

213+ if (tpee_bind(tpee, "0.0.0.0", PORT, NULL, 0) != TPEE_ERR_OK) {

214+ printf("failed to bind port!\n");

215+ return 1;

216+ }

217+ git_libgit2_init();

218+ git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);

219+ if (tpee_listen(tpee) == TPEE_ERR_OK) {

220+ printf("ready and listening (pid: %d)\n", getpid());

221+ pause();

222+ }

223+ git_libgit2_shutdown();

224+ printf("stopped\n");

225+ tpee_close(tpee);

226+ tpee_cleanup(tpee);

227+ return 0;

228+}

229

230

diff --git a/src/module/default.c b/src/module/default.c

new file mode 100644

index 0000000..808ac0a

--- /dev/null

+++ b/src/module/default.c

1@@ -0,0 +1,18 @@

2+#include "default.h"

3+#include "../templ/duc_default.h"

4+

5+static orb_rc_t init(orb_module_t *self) {

6+ duc_default_t *data = (duc_default_t *) self->data;

7+ (void) data;

8+ return ORB_OK;

9+}

10+

11+static orb_rc_t render(orb_module_t *self, void *out) {

12+ return ORB_ERR(duc_default_render(out, (duc_default_t *) self->data));

13+}

14+

15+const orb_module_type_t DEFAULT_TYPE = {

16+ .size = sizeof(duc_default_t),

17+ .init = init,

18+ .render = render,

19+};

20

21

diff --git a/src/module/default.h b/src/module/default.h

new file mode 100644

index 0000000..41abaad

--- /dev/null

+++ b/src/module/default.h

1@@ -0,0 +1,5 @@

2+#pragma once

3+

4+#include <orb.h>

5+

6+extern const orb_module_type_t DEFAULT_TYPE;

7

8

diff --git a/src/module/git_list.c b/src/module/git_list.c

new file mode 100644

index 0000000..999bb06

--- /dev/null

+++ b/src/module/git_list.c

1@@ -0,0 +1,107 @@

2+#include <dirent.h>

3+#include "git_list.h"

4+

5+#include "../templ/duc_git_list.h"

6+

7+static orb_rc_t init(orb_module_t *self) {

8+ duc_git_list_t *data = self->data;

9+ (void) data;

10+ git_list_param_list_t *param_list = self->param_list;

11+ (void) param_list;

12+ return ORB_OK;

13+}

14+

15+static int _fetch_repo_meta_sort(const void *a, const void *b) {

16+ const git_repo_meta_t *repo_meta_a = a;

17+ const git_repo_meta_t *repo_meta_b = b;

18+ return strcmp(repo_meta_a->name, repo_meta_b->name);

19+}

20+

21+static int _fetch_read_text_file(char *filename, char **content) {

22+ FILE *fd = fopen(filename, "r");

23+ if (fd == NULL) return 1;

24+ fseek(fd, 0, SEEK_END);

25+ size_t len = ftell(fd);

26+ fseek(fd, 0, SEEK_SET);

27+ *content = calloc(1, sizeof(char) * (len + 1));

28+ fread(*content, sizeof(char), len, fd);

29+ (*content)[len] = '\0';

30+ fclose(fd);

31+ return 0;

32+}

33+

34+static int _fetch_read_git_file(char *repo_path, char *filename, char **content) {

35+ size_t file_path_len = strlen(repo_path) + strlen(filename) + 2;

36+ char file_path[file_path_len];

37+ snprintf(file_path, file_path_len, "%s/%s", repo_path, filename);

38+ return _fetch_read_text_file(file_path, content);

39+}

40+

41+static bool _fetch_git_file_exists(char *repo_path, char *filename) {

42+ size_t file_path_len = strlen(repo_path) + strlen(filename) + 2;

43+ char file_path[file_path_len];

44+ snprintf(file_path, file_path_len, "%s/%s", repo_path, filename);

45+ FILE *fp = fopen(file_path, "r");

46+ if (!fp) return false;

47+ fclose(fp);

48+ return true;

49+}

50+

51+static orb_rc_t fetch(orb_module_t *self) {

52+ duc_git_list_t *data = self->data;

53+ git_list_param_list_t *param_list = self->param_list;

54+ if (!param_list->path) return ORB_FATAL(1);

55+ DIR *dir = opendir(param_list->path);

56+ if (dir == NULL) return ORB_FATAL(2);

57+ struct dirent *entry;

58+ while ((entry = readdir(dir)) != NULL) {

59+ if (entry->d_type != DT_DIR) continue;

60+ if (strcmp(entry->d_name, ".") == 0) continue;

61+ if (strcmp(entry->d_name, "..") == 0) continue;

62+ size_t repo_path_len = strlen(param_list->path) + strlen(entry->d_name) + 2;

63+ char repo_path[repo_path_len];

64+ snprintf(repo_path, repo_path_len, "%s/%s", param_list->path, entry->d_name);

65+ bool is_public = _fetch_git_file_exists(repo_path, "git-daemon-export-ok");

66+ if (!is_public) continue;

67+ data->repo_meta_list = realloc(data->repo_meta_list, sizeof(git_repo_meta_t) * (data->repo_meta_list_len + 1));

68+ git_repo_meta_t *repo_meta = &data->repo_meta_list[data->repo_meta_list_len];

69+ memset(repo_meta, 0, sizeof(git_repo_meta_t));

70+ repo_meta->name = strdup(entry->d_name);

71+ _fetch_read_git_file(repo_path, "description", &repo_meta->description);

72+ _fetch_read_git_file(repo_path, "owner", &repo_meta->owner);

73+ repo_meta->is_public = is_public;

74+ repo_meta->is_archive = _fetch_git_file_exists(repo_path, ".archive");

75+ repo_meta->is_fork = _fetch_git_file_exists(repo_path, ".fork");

76+ data->repo_meta_list_len++;

77+ }

78+ closedir(dir);

79+ qsort(data->repo_meta_list, data->repo_meta_list_len, sizeof(git_repo_meta_t), _fetch_repo_meta_sort);

80+ return ORB_OK;

81+}

82+

83+static orb_rc_t render(orb_module_t *self, void *out) {

84+ return ORB_ERR(duc_git_list_render(out, (duc_git_list_t *) self->data));

85+}

86+

87+static void cleanup(orb_module_t *self) {

88+ duc_git_list_t *data = self->data;

89+ if (data->repo_meta_list && data->repo_meta_list_len > 0) {

90+ for (size_t i = 0; i < data->repo_meta_list_len; i++) {

91+ git_repo_meta_t *repo_meta = &data->repo_meta_list[i];

92+ free(repo_meta->name);

93+ if (repo_meta->description) free(repo_meta->description);

94+ if (repo_meta->owner) free(repo_meta->owner);

95+ }

96+ free(data->repo_meta_list);

97+ data->repo_meta_list = NULL;

98+ data->repo_meta_list_len = 0;

99+ }

100+}

101+

102+const orb_module_type_t GIT_LIST_TYPE = {

103+ .size = sizeof(duc_git_list_t),

104+ .init = init,

105+ .fetch = fetch,

106+ .render = render,

107+ .cleanup = cleanup,

108+};

109

110

diff --git a/src/module/git_list.h b/src/module/git_list.h

new file mode 100644

index 0000000..74d617f

--- /dev/null

+++ b/src/module/git_list.h

1@@ -0,0 +1,18 @@

2+#pragma once

3+

4+#include <orb.h>

5+

6+typedef struct {

7+ char *path;

8+} git_list_param_list_t;

9+

10+typedef struct {

11+ char *name;

12+ char *description;

13+ char *owner;

14+ bool is_public;

15+ bool is_archive;

16+ bool is_fork;

17+} git_repo_meta_t;

18+

19+extern const orb_module_type_t GIT_LIST_TYPE;

20

21

diff --git a/src/module/git_repo.c b/src/module/git_repo.c

new file mode 100644

index 0000000..6db8d8b

--- /dev/null

+++ b/src/module/git_repo.c

1@@ -0,0 +1,226 @@

2+#include <git2.h>

3+#include <git2/types.h>

4+#include "git_repo.h"

5+

6+#include "../templ/duc_git_repo.h"

7+

8+static orb_rc_t init(orb_module_t *self) {

9+ duc_git_repo_t *data = self->data;

10+ git_repo_param_list_t *param_list = self->param_list;

11+ if (param_list) {

12+ data->repo = (git_repo_t) {

13+ .path = param_list->repo_path,

14+ .meta = {0},

15+ .handle = NULL,

16+ .ref_oid = param_list->ref,

17+ .oid = param_list->oid,

18+ .ref_head = NULL,

19+ .ref = NULL,

20+ .ref_list = NULL,

21+ .ref_list_len = 0,

22+ };

23+ data->repo.meta.name = param_list->repo_name;

24+ data->tab_mode = param_list->tab_mode;

25+ }

26+ return ORB_OK;

27+}

28+

29+static int _fetch_read_text_file(char *filename, char **content) {

30+ FILE *fd = fopen(filename, "r");

31+ if (fd == NULL) return 1;

32+ fseek(fd, 0, SEEK_END);

33+ size_t len = ftell(fd);

34+ fseek(fd, 0, SEEK_SET);

35+ *content = calloc(1, sizeof(char) * (len + 1));

36+ fread(*content, sizeof(char), len, fd);

37+ (*content)[len] = '\0';

38+ fclose(fd);

39+ return 0;

40+}

41+

42+static int _fetch_read_git_file(char *repo_path, char *filename, char **content) {

43+ size_t file_path_len = strlen(repo_path) + strlen(filename) + 2;

44+ char file_path[file_path_len];

45+ snprintf(file_path, file_path_len, "%s/%s", repo_path, filename);

46+ return _fetch_read_text_file(file_path, content);

47+}

48+

49+static bool _fetch_git_file_exists(char *repo_path, char *filename) {

50+ size_t file_path_len = strlen(repo_path) + strlen(filename) + 2;

51+ char file_path[file_path_len];

52+ snprintf(file_path, file_path_len, "%s/%s", repo_path, filename);

53+ FILE *fp = fopen(file_path, "r");

54+ if (!fp) return false;

55+ fclose(fp);

56+ return true;

57+}

58+

59+static char *_fetch_git_file_oid(git_repository *repo, char *ref_name, char *filename) {

60+ size_t spec_len = strlen(ref_name) + strlen(filename) + 2;

61+ char spec[spec_len];

62+ snprintf(spec, spec_len, "%s:%s", ref_name, filename);

63+ git_object *obj_handle = NULL;

64+ if (git_revparse_single(&obj_handle, repo, spec) != 0) return NULL;

65+ char oid_str[GIT_OID_HEXSZ + 1];

66+ git_oid_tostr(oid_str, sizeof(oid_str), git_object_id(obj_handle));

67+ git_object_free(obj_handle);

68+ return strdup(oid_str);

69+}

70+

71+static orb_rc_t fetch(orb_module_t *self) {

72+ duc_git_repo_t *data = self->data;

73+ git_repo_param_list_t *param_list = self->param_list;

74+ if (!param_list->repo_path) return ORB_FATAL(1);

75+ bool is_public = _fetch_git_file_exists(param_list->repo_path, "git-daemon-export-ok");

76+ if (!is_public) return ORB_FATAL(2);

77+ git_repository *repo_handle = NULL;

78+ int code = git_repository_open(&repo_handle, param_list->repo_path);

79+ if (code < 0 || repo_handle == NULL) return ORB_FATAL(code);

80+ data->repo.handle = repo_handle;

81+ _fetch_read_git_file(data->repo.path, "description", &data->repo.meta.description);

82+ _fetch_read_git_file(data->repo.path, "owner", &data->repo.meta.owner);

83+ data->repo.meta.is_public = is_public;

84+ data->repo.meta.is_archive = _fetch_git_file_exists(data->repo.path, ".archive");

85+ data->repo.meta.is_fork = _fetch_git_file_exists(data->repo.path, ".fork");

86+ git_reference *ref_head_handle = NULL;

87+ git_repository_head(&ref_head_handle, data->repo.handle);

88+ if (ref_head_handle) {

89+ data->repo.ref_head = calloc(1, sizeof(git_repo_ref_t));

90+ git_repo_ref_t *ref = data->repo.ref_head;

91+ const git_oid *oid = git_reference_target(ref_head_handle);

92+ char oid_str[GIT_OID_HEXSZ + 1];

93+ git_oid_tostr(oid_str, sizeof(oid_str), oid);

94+ ref->oid = strdup(oid_str);

95+ const char *ref_name = git_reference_name(ref_head_handle);

96+ if (git_reference_is_branch(ref_head_handle)) {

97+ ref->name = strdup(ref_name + strlen("refs/heads/"));

98+ ref->type = "branch";

99+ }

100+ if (git_reference_is_tag(ref_head_handle)) {

101+ ref->name = strdup(ref_name + strlen("refs/tags/"));

102+ ref->type = "tag";

103+ }

104+ if (git_reference_is_remote(ref_head_handle)) {

105+ ref->name = strdup(ref_name + strlen("refs/remotes/"));

106+ ref->type = "remote";

107+ }

108+ if (!ref->name) {

109+ ref->name = strdup(ref_name);

110+ }

111+ git_reference_free(ref_head_handle);

112+ data->repo.readme_oid = _fetch_git_file_oid(repo_handle, ref->name, "README.md");

113+ data->repo.license_oid = _fetch_git_file_oid(repo_handle, ref->name, "LICENSE");

114+ git_reference_iterator *iter = NULL;

115+ git_reference *ref_handle = NULL;

116+ git_reference_iterator_new(&iter, data->repo.handle);

117+ while (git_reference_next(&ref_handle, iter) == 0) {

118+ data->repo.ref_list = realloc(data->repo.ref_list, sizeof(git_repo_ref_t) * (data->repo.ref_list_len + 1));

119+ git_repo_ref_t *ref = &data->repo.ref_list[data->repo.ref_list_len];

120+ memset(ref, 0, sizeof(git_repo_ref_t));

121+ const git_oid *oid = git_reference_target(ref_handle);

122+ char oid_str[GIT_OID_HEXSZ + 1];

123+ git_oid_tostr(oid_str, sizeof(oid_str), oid);

124+ ref->oid = strdup(oid_str);

125+ const char *ref_name = git_reference_name(ref_handle);

126+ if (git_reference_is_branch(ref_handle)) {

127+ ref->name = strdup(ref_name + strlen("refs/heads/"));

128+ ref->type = "branch";

129+ }

130+ if (git_reference_is_tag(ref_handle)) {

131+ ref->name = strdup(ref_name + strlen("refs/tags/"));

132+ ref->type = "tag";

133+ }

134+ if (git_reference_is_remote(ref_handle)) {

135+ ref->name = strdup(ref_name + strlen("refs/remotes/"));

136+ ref->type = "remote";

137+ }

138+ if (!ref->name) {

139+ ref->name = strdup(ref_name);

140+ }

141+ git_reference_free(ref_handle);

142+ data->repo.ref_list_len++;

143+ }

144+ git_reference_iterator_free(iter);

145+ }

146+ if (data->repo.ref_head && data->repo.ref_oid && strcmp(data->repo.ref_head->oid, data->repo.ref_oid) != 0) {

147+ git_oid ref_oid;

148+ if (git_oid_fromstr(&ref_oid, data->repo.ref_oid) == 0) {

149+ git_reference_iterator *iter = NULL;

150+ git_reference *ref_handle = NULL;

151+ git_reference_iterator_new(&iter, data->repo.handle);

152+ while (git_reference_next(&ref_handle, iter) == 0) {

153+ const git_oid *oid = git_reference_target(ref_handle);

154+ if (oid && git_oid_equal(oid, &ref_oid)) {

155+ data->repo.ref = calloc(1, sizeof(git_repo_ref_t));

156+ git_repo_ref_t *ref = data->repo.ref;

157+ ref->oid = strdup(data->repo.ref_oid);

158+ const char *ref_name = git_reference_name(ref_handle);

159+ if (git_reference_is_branch(ref_handle)) {

160+ ref->name = strdup(ref_name + strlen("refs/heads/"));

161+ ref->type = "branch";

162+ }

163+ if (git_reference_is_tag(ref_handle)) {

164+ ref->name = strdup(ref_name + strlen("refs/tags/"));

165+ ref->type = "tag";

166+ }

167+ if (git_reference_is_remote(ref_handle)) {

168+ ref->name = strdup(ref_name + strlen("refs/remotes/"));

169+ ref->type = "remote";

170+ }

171+ if (!ref->name) {

172+ ref->name = strdup(ref_name);

173+ }

174+ }

175+ git_reference_free(ref_handle);

176+ }

177+ git_reference_iterator_free(iter);

178+ }

179+ }

180+ return ORB_OK;

181+}

182+

183+static orb_rc_t render(orb_module_t *self, void *out) {

184+ return ORB_ERR(duc_git_repo_render(out, (duc_git_repo_t *) self->data));

185+}

186+

187+static void cleanup(orb_module_t *self) {

188+ duc_git_repo_t *data = self->data;

189+ if (data->repo.handle) {

190+ git_repository_free(data->repo.handle);

191+ data->repo.handle = NULL;

192+ }

193+ if (data->repo.meta.description) free(data->repo.meta.description);

194+ if (data->repo.meta.owner) free(data->repo.meta.owner);

195+ if (data->repo.ref_head) {

196+ free(data->repo.ref_head->oid);

197+ free(data->repo.ref_head->name);

198+ free(data->repo.ref_head);

199+ data->repo.ref_head = NULL;

200+ }

201+ if (data->repo.ref) {

202+ free(data->repo.ref->oid);

203+ free(data->repo.ref->name);

204+ free(data->repo.ref);

205+ data->repo.ref = NULL;

206+ }

207+ if (data->repo.readme_oid) free(data->repo.readme_oid);

208+ if (data->repo.license_oid) free(data->repo.license_oid);

209+ if (data->repo.ref_list && data->repo.ref_list_len > 0) {

210+ for (size_t i = 0; i < data->repo.ref_list_len; i++) {

211+ git_repo_ref_t *ref = &data->repo.ref_list[i];

212+ free(ref->oid);

213+ free(ref->name);

214+ }

215+ free(data->repo.ref_list);

216+ data->repo.ref_list = NULL;

217+ data->repo.ref_list_len = 0;

218+ }

219+}

220+

221+const orb_module_type_t GIT_REPO_TYPE = {

222+ .size = sizeof(duc_git_repo_t),

223+ .init = init,

224+ .fetch = fetch,

225+ .render = render,

226+ .cleanup = cleanup,

227+};

228

229

diff --git a/src/module/git_repo.h b/src/module/git_repo.h

new file mode 100644

index 0000000..b145385

--- /dev/null

+++ b/src/module/git_repo.h

1@@ -0,0 +1,58 @@

2+#pragma once

3+

4+#include <git2.h>

5+#include <time.h>

6+#include <orb.h>

7+#include "git_list.h"

8+

9+typedef struct {

10+ char *repo_name;

11+ char *repo_path;

12+ char *ref;

13+ char *oid;

14+ const char *tab_mode;

15+} git_repo_param_list_t;

16+

17+typedef struct {

18+ char *oid;

19+ char *name;

20+ const char *type;

21+} git_repo_ref_t;

22+

23+typedef struct {

24+ char *oid;

25+ char *message;

26+ char *author_name;

27+ char *author_email;

28+ time_t time;

29+} git_repo_commit_t;

30+

31+typedef struct {

32+ char *oid;

33+ char *name;

34+ char *content;

35+ size_t size;

36+} git_repo_blob_t;

37+

38+typedef struct {

39+ char *oid;

40+ char *name;

41+ char *mode;

42+ size_t size;

43+} git_repo_tree_entry_t;

44+

45+typedef struct {

46+ char *path;

47+ git_repo_meta_t meta;

48+ git_repository *handle;

49+ char *ref_oid;

50+ char *oid;

51+ git_repo_ref_t *ref_head;

52+ git_repo_ref_t *ref;

53+ git_repo_ref_t *ref_list;

54+ size_t ref_list_len;

55+ char *readme_oid;

56+ char *license_oid;

57+} git_repo_t;

58+

59+extern const orb_module_type_t GIT_REPO_TYPE;

60

61

diff --git a/src/module/git_repo_blob.c b/src/module/git_repo_blob.c

new file mode 100644

index 0000000..d3be875

--- /dev/null

+++ b/src/module/git_repo_blob.c

1@@ -0,0 +1,63 @@

2+#include "git_repo.h"

3+#include "git_repo_blob.h"

4+

5+#include "../templ/duc_git_repo_blob.h"

6+

7+static orb_rc_t init(orb_module_t *self) {

8+ duc_git_repo_blob_t *data = self->data;

9+ git_repo_blob_param_list_t *param_list = self->param_list;

10+ if (param_list) {

11+ data->is_markdown = param_list->is_markdown;

12+ }

13+ return ORB_OK;

14+}

15+

16+static orb_rc_t fetch(orb_module_t *self) {

17+ duc_git_repo_blob_t *data = self->data;

18+ git_repo_blob_param_list_t *param_list = self->param_list;

19+ if (!param_list) return ORB_FATAL(1);

20+ if (!param_list->repo) return ORB_FATAL(2);

21+ if (!param_list->repo->oid) return ORB_FATAL(3);

22+ data->repo = param_list->repo;

23+ git_oid blob_oid;

24+ if (git_oid_fromstr(&blob_oid, data->repo->oid) != 0) return ORB_FATAL(4);

25+ git_blob *blob_handle = NULL;

26+ if (git_blob_lookup(&blob_handle, data->repo->handle, &blob_oid) != 0) return ORB_FATAL(5);

27+ data->blob = calloc(1, sizeof(git_repo_blob_t));

28+ git_repo_blob_t *blob = data->blob;

29+ blob->oid = strdup(data->repo->oid);

30+ blob->name = NULL;

31+ size_t size = (size_t) git_blob_rawsize(blob_handle);

32+ const char *content = git_blob_rawcontent(blob_handle);

33+ if (content) {

34+ blob->content = malloc(size + 1);

35+ memcpy(blob->content, content, size);

36+ blob->content[size] = '\0';

37+ blob->size = size;

38+ }

39+ git_blob_free(blob_handle);

40+ return ORB_OK;

41+}

42+

43+static orb_rc_t render(orb_module_t *self, void *out) {

44+ return ORB_ERR(duc_git_repo_blob_render(out, (duc_git_repo_blob_t *) self->data));

45+}

46+

47+static void cleanup(orb_module_t *self) {

48+ duc_git_repo_blob_t *data = self->data;

49+ if (data->blob) {

50+ free(data->blob->oid);

51+ if (data->blob->name) free(data->blob->name);

52+ if (data->blob->content) free(data->blob->content);

53+ free(data->blob);

54+ data->blob = NULL;

55+ }

56+}

57+

58+const orb_module_type_t GIT_REPO_BLOB_TYPE = {

59+ .size = sizeof(duc_git_repo_blob_t),

60+ .init = init,

61+ .fetch = fetch,

62+ .render = render,

63+ .cleanup = cleanup,

64+};

65

66

diff --git a/src/module/git_repo_blob.h b/src/module/git_repo_blob.h

new file mode 100644

index 0000000..023d81b

--- /dev/null

+++ b/src/module/git_repo_blob.h

1@@ -0,0 +1,11 @@

2+#pragma once

3+

4+#include <orb.h>

5+#include "git_repo.h"

6+

7+typedef struct {

8+ git_repo_t *repo;

9+ bool is_markdown;

10+} git_repo_blob_param_list_t;

11+

12+extern const orb_module_type_t GIT_REPO_BLOB_TYPE;

13

14

diff --git a/src/module/git_repo_commit.c b/src/module/git_repo_commit.c

new file mode 100644

index 0000000..6dceb23

--- /dev/null

+++ b/src/module/git_repo_commit.c

1@@ -0,0 +1,104 @@

2+#include <buf.h>

3+#include <git2/diff.h>

4+#include "git_repo.h"

5+#include "git_repo_commit.h"

6+

7+#include "../templ/duc_git_repo_commit.h"

8+

9+static orb_rc_t fetch(orb_module_t *self) {

10+ duc_git_repo_commit_t *data = self->data;

11+ git_repo_commit_param_list_t *param_list = self->param_list;

12+ if (!param_list) return ORB_FATAL(1);

13+ if (!param_list->repo) return ORB_FATAL(2);

14+ if (!param_list->repo->oid) return ORB_FATAL(3);

15+ data->repo = param_list->repo;

16+ git_oid commit_oid;

17+ if (git_oid_fromstr(&commit_oid, data->repo->oid) != 0) return ORB_FATAL(4);

18+ git_commit *commit_handle = NULL;

19+ git_commit *parent_commit_handle = NULL;

20+ git_commit_lookup(&commit_handle, data->repo->handle, &commit_oid);

21+ data->commit = calloc(1, sizeof(git_repo_commit_t));

22+ git_repo_commit_t *commit = data->commit;

23+ commit->oid = strdup(data->repo->oid);

24+ commit->message = strdup(git_commit_message(commit_handle));

25+ commit->author_name = strdup(git_commit_author(commit_handle)->name);

26+ commit->author_email = strdup(git_commit_author(commit_handle)->email);

27+ commit->time = (time_t) git_commit_time(commit_handle);

28+ if (git_commit_parentcount(commit_handle) > 0) {

29+ git_commit_parent(&parent_commit_handle, commit_handle, 0);

30+ const git_oid *parent_oid = git_commit_id(parent_commit_handle);

31+ data->parent_commit_oid = malloc(sizeof(char) * (GIT_OID_HEXSZ + 1));

32+ git_oid_tostr(data->parent_commit_oid, GIT_OID_HEXSZ + 1, parent_oid);

33+ }

34+ git_tree *tree_handle = NULL;

35+ git_tree *parent_tree_handle = NULL;

36+ git_commit_tree(&tree_handle, commit_handle);

37+ if (parent_commit_handle != NULL) {

38+ git_commit_tree(&parent_tree_handle, parent_commit_handle);

39+ }

40+ git_diff *diff_handle = NULL;

41+ git_diff_tree_to_tree(&diff_handle, data->repo->handle, parent_tree_handle, tree_handle, NULL);

42+ size_t num_deltas = git_diff_num_deltas(diff_handle);

43+ buf_t out = BUF_NULL;

44+ for (size_t i = 0; i < num_deltas; i++) {

45+ git_patch *patch_handle = NULL;

46+ git_patch_from_diff(&patch_handle, diff_handle, i);

47+ git_buf buf_handle = GIT_BUF_INIT_CONST(NULL, 0);

48+ git_patch_to_buf(&buf_handle, patch_handle);

49+ buf_write_len(&out, buf_handle.ptr, buf_handle.size);

50+ buf_write(&out, "\n\n");

51+ git_buf_dispose(&buf_handle);

52+ git_patch_free(patch_handle);

53+ }

54+ buf_write_len(&out, "\0", 1);

55+ // git_diff_stats *stats_handle = NULL;

56+ // if (git_diff_get_stats(&stats_handle, diff_handle)) {

57+ // size_t files_changed = git_diff_stats_files_changed(stats_handle);

58+ // size_t insertions = git_diff_stats_insertions(stats_handle);

59+ // size_t deletions = git_diff_stats_deletions(stats_handle);

60+ // git_diff_stats_free(stats_handle);

61+ // }

62+ git_diff_free(diff_handle);

63+ git_tree_free(tree_handle);

64+ if (parent_tree_handle) {

65+ git_tree_free(parent_tree_handle);

66+ }

67+ git_commit_free(commit_handle);

68+ if (parent_commit_handle) {

69+ git_commit_free(parent_commit_handle);

70+ }

71+ data->diff = (char *) out.bytes;

72+ return ORB_OK;

73+}

74+

75+static orb_rc_t render(orb_module_t *self, void *out) {

76+ return ORB_ERR(duc_git_repo_commit_render(out, (duc_git_repo_commit_t *) self->data));

77+}

78+

79+static void cleanup(orb_module_t *self) {

80+ duc_git_repo_commit_t *data = self->data;

81+ if (data->commit) {

82+ git_repo_commit_t *commit = data->commit;

83+ free(commit->oid);

84+ free(commit->message);

85+ free(commit->author_name);

86+ free(commit->author_email);

87+ free(commit);

88+ commit = NULL;

89+ }

90+ if (data->parent_commit_oid) {

91+ free(data->parent_commit_oid);

92+ data->parent_commit_oid = NULL;

93+ }

94+ if (data->diff) {

95+ free(data->diff);

96+ data->diff = NULL;

97+ }

98+}

99+

100+const orb_module_type_t GIT_REPO_COMMIT_TYPE = {

101+ .size = sizeof(duc_git_repo_commit_t),

102+ .fetch = fetch,

103+ .render = render,

104+.cleanup = cleanup,

105+};

106

107

diff --git a/src/module/git_repo_commit.h b/src/module/git_repo_commit.h

new file mode 100644

index 0000000..d650633

--- /dev/null

+++ b/src/module/git_repo_commit.h

1@@ -0,0 +1,10 @@

2+#pragma once

3+

4+#include <orb.h>

5+#include "git_repo.h"

6+

7+typedef struct {

8+ git_repo_t *repo;

9+} git_repo_commit_param_list_t;

10+

11+extern const orb_module_type_t GIT_REPO_COMMIT_TYPE;

12

13

diff --git a/src/module/git_repo_log.c b/src/module/git_repo_log.c

new file mode 100644

index 0000000..021e998

--- /dev/null

+++ b/src/module/git_repo_log.c

1@@ -0,0 +1,73 @@

2+#include <git2.h>

3+#include "git_repo.h"

4+#include "git_repo_log.h"

5+

6+#include "../templ/duc_git_repo_log.h"

7+

8+static orb_rc_t fetch(orb_module_t *self) {

9+ duc_git_repo_log_t *data = self->data;

10+ git_repo_log_param_list_t *param_list = self->param_list;

11+ if (!param_list) return ORB_ERR(1);

12+ if (!param_list->repo) return ORB_ERR(2);

13+ data->repo = param_list->repo;

14+ git_revwalk *walker = NULL;

15+ git_revwalk_new(&walker, param_list->repo->handle);

16+ git_revwalk_sorting(walker, GIT_SORT_TIME);

17+ int code = 0;

18+ if (param_list->repo->ref) {

19+ git_oid ref_oid;

20+ git_oid_fromstr(&ref_oid, param_list->repo->ref->oid);

21+ code = git_revwalk_push(walker, &ref_oid);

22+ }

23+ else {

24+ code = git_revwalk_push_head(walker);

25+ }

26+ if (code != 0) {

27+ git_revwalk_free(walker);

28+ return ORB_ERR(code);

29+ }

30+ git_oid oid;

31+ while (git_revwalk_next(&oid, walker) == 0) {

32+ git_commit *commit_handle = NULL;

33+ git_commit_lookup(&commit_handle, param_list->repo->handle, &oid);

34+ data->commit_list = realloc(data->commit_list, sizeof(git_repo_commit_t) * (data->commit_list_len + 1));

35+ git_repo_commit_t *commit = &data->commit_list[data->commit_list_len];

36+ commit->oid = malloc(sizeof(char) * (GIT_OID_HEXSZ + 1));

37+ git_oid_tostr(commit->oid, GIT_OID_HEXSZ + 1, &oid);

38+ commit->message = strdup(git_commit_message(commit_handle));

39+ commit->author_name = strdup(git_commit_author(commit_handle)->name);

40+ commit->author_email = strdup(git_commit_author(commit_handle)->email);

41+ commit->time = (time_t) git_commit_time(commit_handle);

42+ git_commit_free(commit_handle);

43+ data->commit_list_len++;

44+ }

45+ git_revwalk_free(walker);

46+ return ORB_OK;

47+}

48+

49+static orb_rc_t render(orb_module_t *self, void *out) {

50+ return ORB_ERR(duc_git_repo_log_render(out, (duc_git_repo_log_t *) self->data));

51+}

52+

53+static void cleanup(orb_module_t *self) {

54+ duc_git_repo_log_t *data = self->data;

55+ if (data->commit_list && data->commit_list_len > 0) {

56+ for (size_t i = 0; i < data->commit_list_len; i++) {

57+ git_repo_commit_t *commit = &data->commit_list[i];

58+ free(commit->oid);

59+ free(commit->message);

60+ free(commit->author_name);

61+ free(commit->author_email);

62+ }

63+ free(data->commit_list);

64+ data->commit_list = NULL;

65+ data->commit_list_len = 0;

66+ }

67+}

68+

69+const orb_module_type_t GIT_REPO_LOG_TYPE = {

70+ .size = sizeof(duc_git_repo_log_t),

71+ .fetch = fetch,

72+ .render = render,

73+ .cleanup = cleanup,

74+};

75

76

diff --git a/src/module/git_repo_log.h b/src/module/git_repo_log.h

new file mode 100644

index 0000000..b47f0e3

--- /dev/null

+++ b/src/module/git_repo_log.h

1@@ -0,0 +1,10 @@

2+#pragma once

3+

4+#include <orb.h>

5+#include "git_repo.h"

6+

7+typedef struct {

8+ git_repo_t *repo;

9+} git_repo_log_param_list_t;

10+

11+extern const orb_module_type_t GIT_REPO_LOG_TYPE;

12

13

diff --git a/src/module/git_repo_refs.c b/src/module/git_repo_refs.c

new file mode 100644

index 0000000..2257907

--- /dev/null

+++ b/src/module/git_repo_refs.c

1@@ -0,0 +1,70 @@

2+#include <git2.h>

3+#include "git_repo.h"

4+#include "git_repo_refs.h"

5+

6+#include "../templ/duc_git_repo_refs.h"

7+

8+static orb_rc_t fetch(orb_module_t *self) {

9+ duc_git_repo_refs_t *data = self->data;

10+ git_repo_refs_param_list_t *param_list = self->param_list;

11+ if (!param_list) return ORB_FATAL(1);

12+ if (!param_list->repo) return ORB_FATAL(2);

13+ data->repo = param_list->repo;

14+ git_reference_iterator *iter = NULL;

15+ git_reference *ref_handle = NULL;

16+ git_reference_iterator_new(&iter, param_list->repo->handle);

17+ while (git_reference_next(&ref_handle, iter) == 0) {

18+ data->ref_list = realloc(data->ref_list, sizeof(git_repo_ref_t) * (data->ref_list_len + 1));

19+ git_repo_ref_t *ref = &data->ref_list[data->ref_list_len];

20+ memset(ref, 0, sizeof(git_repo_ref_t));

21+ const git_oid *oid = git_reference_target(ref_handle);

22+ char oid_str[GIT_OID_HEXSZ + 1];

23+ git_oid_tostr(oid_str, sizeof(oid_str), oid);

24+ ref->oid = strdup(oid_str);

25+ const char *ref_name = git_reference_name(ref_handle);

26+ if (git_reference_is_branch(ref_handle)) {

27+ ref->name = strdup(ref_name + strlen("refs/heads/"));

28+ ref->type = "branch";

29+ }

30+ if (git_reference_is_tag(ref_handle)) {

31+ ref->name = strdup(ref_name + strlen("refs/tags/"));

32+ ref->type = "tag";

33+ }

34+ if (git_reference_is_remote(ref_handle)) {

35+ ref->name = strdup(ref_name + strlen("refs/remotes/"));

36+ ref->type = "remote";

37+ }

38+ if (!ref->name) {

39+ ref->name = strdup(ref_name);

40+ }

41+ git_reference_free(ref_handle);

42+ data->ref_list_len++;

43+ }

44+ git_reference_iterator_free(iter);

45+ return ORB_OK;

46+}

47+

48+static orb_rc_t render(orb_module_t *self, void *out) {

49+ return ORB_ERR(duc_git_repo_refs_render(out, (duc_git_repo_refs_t *) self->data));

50+}

51+

52+static void cleanup(orb_module_t *self) {

53+ duc_git_repo_refs_t *data = self->data;

54+ if (data->ref_list && data->ref_list_len > 0) {

55+ for (size_t i = 0; i < data->ref_list_len; i++) {

56+ git_repo_ref_t *ref = &data->ref_list[i];

57+ free(ref->oid);

58+ free(ref->name);

59+ }

60+ free(data->ref_list);

61+ data->ref_list = NULL;

62+ data->ref_list_len = 0;

63+ }

64+}

65+

66+const orb_module_type_t GIT_REPO_REFS_TYPE = {

67+ .size = sizeof(duc_git_repo_refs_t),

68+ .fetch = fetch,

69+ .render = render,

70+ .cleanup = cleanup,

71+};

72

73

diff --git a/src/module/git_repo_refs.h b/src/module/git_repo_refs.h

new file mode 100644

index 0000000..47b9a7c

--- /dev/null

+++ b/src/module/git_repo_refs.h

1@@ -0,0 +1,10 @@

2+#pragma once

3+

4+#include <orb.h>

5+#include "git_repo.h"

6+

7+typedef struct {

8+ git_repo_t *repo;

9+} git_repo_refs_param_list_t;

10+

11+extern const orb_module_type_t GIT_REPO_REFS_TYPE;

12

13

diff --git a/src/module/git_repo_tree.c b/src/module/git_repo_tree.c

new file mode 100644

index 0000000..2ec8d50

--- /dev/null

+++ b/src/module/git_repo_tree.c

1@@ -0,0 +1,97 @@

2+#include <git2.h>

3+#include "git_repo.h"

4+#include "git_repo_tree.h"

5+

6+#include "../templ/duc_git_repo_tree.h"

7+

8+static int _fetch_tree_cb(const char *root, const git_tree_entry *entry_handle, void *data) {

9+ git_repo_tree_cb_t *handle = data;

10+ const char *name = git_tree_entry_name(entry_handle);

11+ git_filemode_t mode_handle = git_tree_entry_filemode(entry_handle);

12+ if (mode_handle != GIT_FILEMODE_TREE) {

13+ *handle->entry_list = realloc(*handle->entry_list, sizeof(git_repo_tree_entry_t) * (*handle->entry_list_len + 1));

14+ git_repo_tree_entry_t *entry = &(*handle->entry_list)[*handle->entry_list_len];

15+ memset(entry, 0, sizeof(git_repo_tree_entry_t));

16+ const git_oid *oid = git_tree_entry_id(entry_handle);

17+ char oid_str[GIT_OID_HEXSZ + 1];

18+ git_oid_tostr(oid_str, sizeof(oid_str), oid);

19+ entry->oid = strdup(oid_str);

20+ size_t filepath_len = strlen(root) + strlen(name);

21+ char filepath[filepath_len + 1];

22+ snprintf(filepath, sizeof(filepath), "%s%s", root, name);

23+ entry->name = strdup(filepath);

24+ entry->mode = strdup((mode_handle == GIT_FILEMODE_BLOB_EXECUTABLE) ? "-rwx-xr-x" : (mode_handle == GIT_FILEMODE_TREE) ? "d---------" : "-rw-r--r--");

25+ git_object_t obj_type = git_tree_entry_type(entry_handle);

26+ if (obj_type == GIT_OBJECT_BLOB) {

27+ git_object *obj_handle = NULL;

28+ git_tree_entry_to_object(&obj_handle, handle->repo_handle, entry_handle);

29+ git_blob *blob_handle = (git_blob *) obj_handle;

30+ entry->size = (size_t) git_blob_rawsize(blob_handle);

31+ git_object_free(obj_handle);

32+ }

33+ (*handle->entry_list_len)++;

34+ }

35+ return 0;

36+}

37+

38+static orb_rc_t fetch(orb_module_t *self) {

39+ duc_git_repo_tree_t *data = self->data;

40+ git_repo_tree_param_list_t *param_list = self->param_list;

41+ if (!param_list) return ORB_ERR(1);

42+ if (!param_list->repo) return ORB_ERR(2);

43+ data->repo = param_list->repo;

44+ git_object *obj = NULL;

45+ git_commit *commit_handle = NULL;

46+ char *ref_oid = NULL;

47+ if (param_list->repo->ref_head) {

48+ ref_oid = param_list->repo->ref_head->oid;

49+ }

50+ if (param_list->repo->ref) {

51+ ref_oid = param_list->repo->ref->oid;

52+ }

53+ if (!ref_oid) return ORB_ERR(3);

54+ const char *spec_suffix = "^{commit}";

55+ size_t spec_len = strlen(ref_oid) + strlen(spec_suffix) + 1;

56+ char spec[spec_len];

57+ snprintf(spec, spec_len, "%s%s", ref_oid, spec_suffix);

58+ if (git_revparse_single(&obj, param_list->repo->handle, spec) == 0) {

59+ commit_handle = (git_commit *) obj;

60+ git_tree *tree_handle = NULL;

61+ if (git_commit_tree(&tree_handle, commit_handle) == 0) {

62+ git_tree_walk(tree_handle, GIT_TREEWALK_PRE, _fetch_tree_cb, &((git_repo_tree_cb_t) {

63+ .repo_handle = param_list->repo->handle,

64+ .entry_list = &data->entry_list,

65+ .entry_list_len = &data->entry_list_len,

66+ }));

67+ git_tree_free(tree_handle);

68+ }

69+ git_object_free(obj);

70+ }

71+ return ORB_OK;

72+}

73+

74+static orb_rc_t render(orb_module_t *self, void *out) {

75+ return ORB_ERR(duc_git_repo_tree_render(out, (duc_git_repo_tree_t *) self->data));

76+}

77+

78+static void cleanup(orb_module_t *self) {

79+ duc_git_repo_tree_t *data = self->data;

80+ if (data->entry_list && data->entry_list_len > 0) {

81+ for (size_t i = 0; i < data->entry_list_len; i++) {

82+ git_repo_tree_entry_t *entry = &data->entry_list[i];

83+ free(entry->oid);

84+ free(entry->name);

85+ free(entry->mode);

86+ }

87+ free(data->entry_list);

88+ data->entry_list = NULL;

89+ data->entry_list_len = 0;

90+ }

91+}

92+

93+const orb_module_type_t GIT_REPO_TREE_TYPE = {

94+ .size = sizeof(duc_git_repo_tree_t),

95+ .fetch = fetch,

96+ .render = render,

97+ .cleanup = cleanup,

98+};

99

100

diff --git a/src/module/git_repo_tree.h b/src/module/git_repo_tree.h

new file mode 100644

index 0000000..1803a5d

--- /dev/null

+++ b/src/module/git_repo_tree.h

1@@ -0,0 +1,16 @@

2+#pragma once

3+

4+#include <orb.h>

5+#include "git_repo.h"

6+

7+typedef struct {

8+ git_repo_t *repo;

9+} git_repo_tree_param_list_t;

10+

11+typedef struct {

12+ git_repository *repo_handle;

13+ git_repo_tree_entry_t **entry_list;

14+ size_t *entry_list_len;

15+} git_repo_tree_cb_t;

16+

17+extern const orb_module_type_t GIT_REPO_TREE_TYPE;

18

19

diff --git a/src/module/index.c b/src/module/index.c

new file mode 100644

index 0000000..450e03e

--- /dev/null

+++ b/src/module/index.c

1@@ -0,0 +1,22 @@

2+#include "index.h"

3+#include "../templ/duc_index.h"

4+

5+static orb_rc_t init(orb_module_t *self) {

6+ duc_index_t *data = (duc_index_t *) self->data;

7+ (void) data;

8+ if (self->param_list) {

9+ index_param_list_t *param_list = (index_param_list_t *) self->param_list;

10+ (void) param_list;

11+ }

12+ return ORB_OK;

13+}

14+

15+static orb_rc_t render(orb_module_t *self, void *out) {

16+ return ORB_ERR(duc_index_render(out, (duc_index_t *) self->data));

17+}

18+

19+const orb_module_type_t INDEX_TYPE = {

20+ .size = sizeof(duc_index_t),

21+ .init = init,

22+ .render = render,

23+};

24

25

diff --git a/src/module/index.h b/src/module/index.h

new file mode 100644

index 0000000..5d881e7

--- /dev/null

+++ b/src/module/index.h

1@@ -0,0 +1,9 @@

2+#pragma once

3+

4+#include <orb.h>

5+

6+typedef struct {

7+ char *title;

8+} index_param_list_t;

9+

10+extern const orb_module_type_t INDEX_TYPE;

11

12

diff --git a/src/module/page.c b/src/module/page.c

new file mode 100644

index 0000000..c002658

--- /dev/null

+++ b/src/module/page.c

1@@ -0,0 +1,22 @@

2+#include "page.h"

3+#include "../templ/duc_page.h"

4+

5+static orb_rc_t init(orb_module_t *self) {

6+ duc_page_t *data = (duc_page_t *) self->data;

7+ if (self->param_list) {

8+ page_param_list_t *param_list = (page_param_list_t *) self->param_list;

9+ data->title = param_list->title;

10+ data->module = param_list->module;

11+ }

12+ return ORB_OK;

13+}

14+

15+static orb_rc_t render(orb_module_t *self, void *out) {

16+ return ORB_ERR(duc_page_render(out, (duc_page_t *) self->data));

17+}

18+

19+const orb_module_type_t PAGE_TYPE = {

20+ .size = sizeof(duc_page_t),

21+ .init = init,

22+ .render = render,

23+};

24

25

diff --git a/src/module/page.h b/src/module/page.h

new file mode 100644

index 0000000..25761d8

--- /dev/null

+++ b/src/module/page.h

1@@ -0,0 +1,10 @@

2+#pragma once

3+

4+#include <orb.h>

5+

6+typedef struct {

7+ char *title;

8+ orb_module_t *module;

9+} page_param_list_t;

10+

11+extern const orb_module_type_t PAGE_TYPE;

12

13

diff --git a/src/registry.h b/src/registry.h

new file mode 100644

index 0000000..d7e56c3

--- /dev/null

+++ b/src/registry.h

1@@ -0,0 +1,34 @@

2+#pragma once

3+

4+#include <tpee.h>

5+#include <orb.h>

6+#include "module/page.h"

7+#include "module/default.h"

8+#include "module/index.h"

9+#include "module/git_list.h"

10+#include "module/git_repo.h"

11+#include "module/git_repo_log.h"

12+#include "module/git_repo_tree.h"

13+#include "module/git_repo_refs.h"

14+#include "module/git_repo_commit.h"

15+#include "module/git_repo_blob.h"

16+

17+typedef struct {

18+ const char* id;

19+ const orb_module_type_t *type;

20+} registry_entry_t;

21+

22+static const registry_entry_t REGISTRY[] = {

23+ { "page", &PAGE_TYPE },

24+ { "default", &DEFAULT_TYPE },

25+ { "index", &INDEX_TYPE },

26+ { "git_list", &GIT_LIST_TYPE },

27+ { "git_repo", &GIT_REPO_TYPE },

28+ { "git_repo_log", &GIT_REPO_LOG_TYPE },

29+ { "git_repo_tree", &GIT_REPO_TREE_TYPE },

30+ { "git_repo_refs", &GIT_REPO_REFS_TYPE },

31+ { "git_repo_commit", &GIT_REPO_COMMIT_TYPE },

32+ { "git_repo_blob", &GIT_REPO_BLOB_TYPE },

33+};

34+

35+static const size_t REGISTRY_LEN = sizeof(REGISTRY) / sizeof(REGISTRY[0]);

36

37

diff --git a/src/util.h b/src/util.h

new file mode 100644

index 0000000..a40024b

--- /dev/null

+++ b/src/util.h

1@@ -0,0 +1,24 @@

2+#pragma once

3+

4+void util_md_html(const char *str, void *out);

5+

6+#ifdef UTIL_IMPL

7+

8+#include <md4c-html.h>

9+

10+static void _util_md_html_parse_cb(const MD_CHAR *str, MD_SIZE len, void *out) {

11+ buf_write_len((buf_t *) out, str, len);

12+}

13+

14+void util_md_html(const char *str, void *out) {

15+ if (!str || !out) return;

16+ unsigned parser_flags =

17+ MD_FLAG_STRIKETHROUGH |

18+ MD_FLAG_TABLES |

19+ MD_FLAG_HIGHLIGHT |

20+ MD_FLAG_TASKLISTS;

21+ unsigned renderer_flags = 0;

22+ md_html(str, strlen(str), _util_md_html_parse_cb, out, parser_flags, renderer_flags);

23+}

24+

25+#endif // UTIL_IMPL

26

27

diff --git a/templ/default.html b/templ/default.html

new file mode 100644

index 0000000..9b8276a

--- /dev/null

+++ b/templ/default.html

1@@ -0,0 +1,33 @@

2+% #include "src/duc_macro.h" %

3+

4+% DUC_TEMPLDEF(

5+ duc_default,

6+) %

7+

8+<style>

9+

10+ .default {

11+ margin: 4rem 0px;

12+

13+ h1 {

14+ text-align: center;

15+ }

16+

17+ h3 {

18+ text-align: center;

19+ }

20+

21+ p {

22+ text-align: center;

23+ }

24+ }

25+

26+</style>

27+

28+<div class="default">

29+ <h1>404</h1>

30+ <h3>Page not found</h3>

31+ <p>This page doesn't exist or has been moved.</p>

32+</div>

33+

34+% DUC_ENDTEMPL %

35

36

diff --git a/templ/git_list.html b/templ/git_list.html

new file mode 100644

index 0000000..9d46f81

--- /dev/null

+++ b/templ/git_list.html

1@@ -0,0 +1,31 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_list.h" %

4+

5+% DUC_TEMPLDEF(

6+ duc_git_list,

7+ git_repo_meta_t *repo_meta_list;

8+ size_t repo_meta_list_len;

9+) %

10+

11+<ul class="row-list list--repo">

12+ <li class="row row--head">

13+ <span class="col col--name">NAME</span>

14+ <span class="col col--description">DESCRIPTION</span>

15+ <span class="col col--owner">OWNER</span>

16+ </li>

17+ % for (size_t i = 0; i < ctx->repo_meta_list_len; i++) {

18+ git_repo_meta_t *repo_meta = &ctx->repo_meta_list[i];

19+ %

20+ <li class="row">

21+ <span class="col col--name">

22+ <a href="/git/% STRING(repo_meta->name) %">% STRING(repo_meta->name) %</a>

23+ % if (repo_meta->is_archive) { %<span>(archive)</span>% } %

24+ % if (repo_meta->is_fork) { %<span>(fork)</span>% } %

25+ </span>

26+ <span class="col col--description">% STRING(repo_meta->description) %</span>

27+ <span class="col col--owner">% STRING(repo_meta->owner) %</span>

28+ </li>

29+ % } %

30+</ul>

31+

32+% DUC_ENDTEMPL %

33

34

diff --git a/templ/git_repo.html b/templ/git_repo.html

new file mode 100644

index 0000000..b3fad4c

--- /dev/null

+++ b/templ/git_repo.html

1@@ -0,0 +1,97 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_repo.h" %

4+% #include "src/module/git_repo_log.h" %

5+% #include "src/module/git_repo_tree.h" %

6+% #include "src/module/git_repo_refs.h" %

7+% #include "src/module/git_repo_commit.h" %

8+% #include "src/module/git_repo_blob.h" %

9+

10+% DUC_TEMPLDEF(

11+ duc_git_repo,

12+ git_repo_t repo;

13+ const char *tab_mode;

14+) %

15+

16+<div class="git--repo">

17+ <h2>% STRING(ctx->repo.meta.name) %</h2>

18+ <p>

19+ % if (ctx->repo.meta.description) { % <span>% STRING(ctx->repo.meta.description) %</span><br/> % } %

20+ git clone <a href="git://soophie.de/% STRING(ctx->repo.meta.name) %">git://soophie.de/% STRING(ctx->repo.meta.name) %</a>

21+ </p>

22+ <ul class="link-list">

23+ <li class="link % if (strcmp(ctx->tab_mode, "log") == 0 || strcmp(ctx->tab_mode, "commit") == 0) { %link--active% } %"><a href="/git/% STRING(ctx->repo.meta.name) if (ctx->repo.ref) { %?ref=% STRING(ctx->repo.ref->oid) } %">log</a></li>

24+ % bool is_readme = ctx->repo.oid && ctx->repo.readme_oid && strcmp(ctx->repo.oid, ctx->repo.readme_oid) == 0; %

25+ % bool is_license = ctx->repo.oid && ctx->repo.license_oid && strcmp(ctx->repo.oid, ctx->repo.license_oid) == 0; %

26+ <li class="link % if (strcmp(ctx->tab_mode, "tree") == 0 || (strcmp(ctx->tab_mode, "blob") == 0 && !is_readme && !is_license)) { %link--active% } %"><a href="/git/% STRING(ctx->repo.meta.name) %/tree% if (ctx->repo.ref) { %?ref=% STRING(ctx->repo.ref->oid) } %">tree</a></li>

27+ <li class="link % if (strcmp(ctx->tab_mode, "refs") == 0) { %link--active% } %"><a href="/git/% STRING(ctx->repo.meta.name) %/refs% if (ctx->repo.ref) { %?ref=% STRING(ctx->repo.ref->oid) } %">refs</a></li>

28+ % if (ctx->repo.readme_oid) { %

29+ <li class="link % if (strcmp(ctx->tab_mode, "blob") == 0 && is_readme) { %link--active% } %"><a href="/git/% STRING(ctx->repo.meta.name) %/blob?% if (ctx->repo.ref) { %ref=% STRING(ctx->repo.ref->oid) %&% } %oid=% STRING(ctx->repo.readme_oid) %">README</a></li>

30+ % } %

31+ % if (ctx->repo.license_oid) { %

32+ <li class="link % if (strcmp(ctx->tab_mode, "blob") == 0 && is_license) { %link--active% } %"><a href="/git/% STRING(ctx->repo.meta.name) %/blob?% if (ctx->repo.ref) { %ref=% STRING(ctx->repo.ref->oid) %&% } %oid=% STRING(ctx->repo.license_oid) %">LICENSE</a></li>

33+ % } %

34+ </ul>

35+ %

36+ char *tab = "";

37+ if (ctx->tab_mode) {

38+ if (strcmp(ctx->tab_mode, "log") == 0) tab = "log";

39+ if (strcmp(ctx->tab_mode, "commit") == 0) tab = "log";

40+ if (strcmp(ctx->tab_mode, "tree") == 0) tab = "tree";

41+ if (strcmp(ctx->tab_mode, "blob") == 0) tab = "tree";

42+ if (strcmp(ctx->tab_mode, "refs") == 0) tab = "refs";

43+ }

44+ bool is_branch_select = true;

45+ if (ctx->repo.oid) {

46+ if (ctx->repo.readme_oid && strcmp(ctx->repo.oid, ctx->repo.readme_oid) == 0) is_branch_select = false;

47+ if (ctx->repo.license_oid && strcmp(ctx->repo.oid, ctx->repo.license_oid) == 0) is_branch_select = false;

48+ }

49+ %

50+ % if (is_branch_select) { %

51+ <select class="repo--branch" onchange="window.location.href = '/git/% STRING(ctx->repo.meta.name) %/% STRING(tab) %?ref=' + this.value">

52+ % for (size_t i = 0; i < ctx->repo.ref_list_len; i++) {

53+ git_repo_ref_t *ref = &ctx->repo.ref_list[i];

54+ if (!ref->name || !ref->oid) continue;

55+ bool is_selected = (ctx->repo.ref && strcmp(ctx->repo.ref->oid, ref->oid) == 0)

56+ || (!ctx->repo.ref && ctx->repo.ref_head && strcmp(ctx->repo.ref_head->oid, ref->oid) == 0);

57+ %

58+ <option value="% STRING(ref->oid) %" % if (is_selected) { %selected% } %>

59+ % if (ref->type) { STRING(ref->type) %:% } STRING(ref->name) %

60+ </option>

61+ % } %

62+ </select>

63+ % } %

64+ <div class="repo--tab">

65+ % if (ctx->tab_mode && strcmp(ctx->tab_mode, "log") == 0) {

66+ ORB_MODULE_DEF(GIT_REPO_LOG_TYPE, &((git_repo_log_param_list_t) {

67+ .repo = &ctx->repo,

68+ }))

69+ } %

70+ % if (ctx->tab_mode && strcmp(ctx->tab_mode, "tree") == 0) {

71+ ORB_MODULE_DEF(GIT_REPO_TREE_TYPE, &((git_repo_tree_param_list_t) {

72+ .repo = &ctx->repo,

73+ }))

74+ } %

75+ % if (ctx->tab_mode && strcmp(ctx->tab_mode, "refs") == 0) {

76+ ORB_MODULE_DEF(GIT_REPO_REFS_TYPE, &((git_repo_refs_param_list_t) {

77+ .repo = &ctx->repo,

78+ }))

79+ } %

80+ % if (ctx->tab_mode && strcmp(ctx->tab_mode, "commit") == 0) {

81+ ORB_MODULE_DEF(GIT_REPO_COMMIT_TYPE, &((git_repo_commit_param_list_t) {

82+ .repo = &ctx->repo,

83+ }))

84+ } %

85+ % if (ctx->tab_mode && strcmp(ctx->tab_mode, "blob") == 0) {

86+ bool is_markdown = false;

87+ if (ctx->repo.oid) {

88+ if (ctx->repo.readme_oid && strcmp(ctx->repo.oid, ctx->repo.readme_oid) == 0) is_markdown = true;

89+ }

90+ ORB_MODULE_DEF(GIT_REPO_BLOB_TYPE, &((git_repo_blob_param_list_t) {

91+ .repo = &ctx->repo,

92+ .is_markdown = is_markdown,

93+ }))

94+ } %

95+ </div>

96+</div>

97+

98+% DUC_ENDTEMPL %

99

100

diff --git a/templ/git_repo_blob.html b/templ/git_repo_blob.html

new file mode 100644

index 0000000..2a0c059

--- /dev/null

+++ b/templ/git_repo_blob.html

1@@ -0,0 +1,22 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_repo.h" %

4+% #include "src/module/git_repo_blob.h" %

5+

6+% DUC_TEMPLDEF(

7+ duc_git_repo_blob,

8+ git_repo_t *repo;

9+ git_repo_blob_t *blob;

10+ bool is_markdown;

11+) %

12+

13+% if (ctx->is_markdown) { %

14+ <div class="blob--markdown">% MARKDOWN(ctx->blob->content) %</div>

15+% } else { %

16+ <p>

17+ <span>blob: % STRING(ctx->blob->oid) %</span><br/>

18+ <span>size: % NUMBER(ctx->blob->size) %</span>

19+ </p>

20+ <pre class="blob--code">% STRING(ctx->blob->content) %</pre>

21+% } %

22+

23+% DUC_ENDTEMPL %

24

25

diff --git a/templ/git_repo_commit.html b/templ/git_repo_commit.html

new file mode 100644

index 0000000..3ac9e24

--- /dev/null

+++ b/templ/git_repo_commit.html

1@@ -0,0 +1,82 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_repo.h" %

4+% #include "src/module/git_repo_commit.h" %

5+% #include <math.h> %

6+

7+% DUC_TEMPLDEF(

8+ duc_git_repo_commit,

9+ git_repo_t *repo;

10+ git_repo_commit_t *commit;

11+ char *parent_commit_oid;

12+ char *diff;

13+) %

14+

15+<p>

16+ <h3>% STRING(ctx->commit->message) %</h3>

17+ <span>commit: <a href="/git/% STRING(ctx->repo->meta.name) %/commit?oid=% STRING(ctx->commit->oid) %">% STRING(ctx->commit->oid) %</a></span><br/>

18+ % if (ctx->parent_commit_oid) { %

19+ <span>parent: <a href="/git/% STRING(ctx->repo->meta.name) %/commit?oid=% STRING(ctx->parent_commit_oid) %">% STRING(ctx->parent_commit_oid) %</a></span><br/>

20+ % } %

21+ <span>author: % STRING(ctx->commit->author_name) % &lt;<a href="mailto:% STRING(ctx->commit->author_email) %">% STRING(ctx->commit->author_email) %</a>&gt;</span><br/>

22+ <span>time: % FORMAT_TIME(&ctx->commit->time, "%%Y-%%m-%%d %%H:%%M") %</span></br>

23+</p>

24+<div class="commit-diff">

25+ % if (ctx->diff) { %

26+ <pre class="diff--meta">%

27+

28+ size_t len = strlen(ctx->diff);

29+ size_t i = 0;

30+ size_t ln = 0;

31+ size_t sol = 0;

32+ while (i < len) {

33+ char c = ctx->diff[i];

34+ if (c == '\n') {

35+ ln++;

36+ }

37+ i++;

38+ }

39+ size_t pad = floor(log10(ln + 1)) + 1;

40+ i = 0;

41+ ln = 0;

42+ sol = 0;

43+ size_t df = 0;

44+ bool is_meta = false;

45+ while (i < len) {

46+ char c = ctx->diff[i];

47+ if (c == '\n') {

48+ size_t ln_len = i - sol;

49+ char line[ln_len + 1];

50+ memcpy(line, ctx->diff + sol, ln_len);

51+ line[ln_len] = '\0';

52+ sol = i + 1;

53+ bool is_diff = strncmp(line, "diff --git", 10) == 0;

54+ bool is_hunk = strncmp(line, "@@ ", 3) == 0;

55+ if (is_diff) {

56+ is_meta = true;

57+ }

58+ if (is_diff && ln != 0) {

59+ df++;

60+ %</pre>%

61+ %<pre class="diff--meta">%

62+ }

63+ if (is_hunk && is_meta) {

64+ is_meta = false;

65+ ln = 0;

66+ %</pre>%

67+ %<pre class="diff--code">%

68+ }

69+ if (is_meta) {

70+ %<p>%STRING(line)%</p>%

71+ }

72+ else {

73+ %<p id="% NUMBER(df) %-% NUMBER(ln + 1) %" class="% if (is_hunk) { %is-hunk% } % % if (strncmp(line, "+", 1) == 0 && strncmp(line, "+++", 3) != 0) { %is-add% } % % if (strncmp(line, "-", 1) == 0 && strncmp(line, "---", 3) != 0) { %is-rem% } %"><span class="ln"><a href="#% NUMBER(df) %-% NUMBER(ln + 1) %">% PRINTF("%%*ld", pad, ln + 1) %</a></span><span class="line"><code>% STRING(line) %</code></span></p>%

74+ }

75+ ln++;

76+ }

77+ i++;

78+ }

79+ %</pre>

80+ % } %

81+</div>

82+

83+% DUC_ENDTEMPL %

84

85

diff --git a/templ/git_repo_log.html b/templ/git_repo_log.html

new file mode 100644

index 0000000..19d3593

--- /dev/null

+++ b/templ/git_repo_log.html

1@@ -0,0 +1,32 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_repo.h" %

4+% #include "src/module/git_repo_log.h" %

5+

6+% DUC_TEMPLDEF(

7+ duc_git_repo_log,

8+ git_repo_t *repo;

9+ git_repo_commit_t *commit_list;

10+ size_t commit_list_len;

11+) %

12+

13+<p>% NUMBER(ctx->commit_list_len) % commit% if (ctx->commit_list_len != 1) { %s% } %</p>

14+<ul class="row-list list--log">

15+ <li class="row row--head">

16+ <span class="col col--oid">OID</span>

17+ <span class="col col--time">TIME</span>

18+ <span class="col col--message">MESSAGE</span>

19+ <span class="col col--author">AUTHOR</span>

20+ </li>

21+ % for (size_t i = 0; i < ctx->commit_list_len; i++) {

22+ git_repo_commit_t *commit = &ctx->commit_list[i];

23+ %

24+ <li class="row">

25+ <span class="col col--oid">% PRINTF("%%.7s", commit->oid) %</span>

26+ <span class="col col--time">% FORMAT_TIME(&commit->time, "%%Y-%%m-%%d") %</span>

27+ <span class="col col--message"><a href="/git/% STRING(ctx->repo->meta.name) %/commit?% if (ctx->repo->ref) { %ref=% STRING(ctx->repo->ref->oid) %&% } %oid=% STRING(commit->oid) %">% STRING(commit->message) %</a></span>

28+ <span class="col col--author">% STRING(commit->author_name) %</span>

29+ </li>

30+ % } %

31+</ul>

32+

33+% DUC_ENDTEMPL %

34

35

diff --git a/templ/git_repo_refs.html b/templ/git_repo_refs.html

new file mode 100644

index 0000000..93e6fc6

--- /dev/null

+++ b/templ/git_repo_refs.html

1@@ -0,0 +1,27 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_repo.h" %

4+% #include "src/module/git_repo_refs.h" %

5+

6+% DUC_TEMPLDEF(

7+ duc_git_repo_refs,

8+ git_repo_t *repo;

9+ git_repo_ref_t *ref_list;

10+ size_t ref_list_len;

11+) %

12+

13+<ul class="row-list list--refs">

14+ <li class="row row--head">

15+ <span class="col col--oid">OID</a></span>

16+ <span class="col col--name">NAME</span>

17+ </li>

18+ % for (size_t i = 0; i < ctx->ref_list_len; i++) {

19+ git_repo_ref_t *ref = &ctx->ref_list[i];

20+ %

21+ <li class="row">

22+ <span class="col col--oid">% PRINTF("%%.7s", ref->oid) %</span>

23+ <span class="col col--name"><a href="/git/% STRING(ctx->repo->meta.name) if (strcmp(ctx->repo->ref_head->oid, ref->oid) != 0) { %?ref=% STRING(ref->oid) } %">% if (ref->type) { STRING(ref->type) %:% } STRING(ref->name) %</a></span>

24+ </li>

25+ % } %

26+</ul>

27+

28+% DUC_ENDTEMPL %

29

30

diff --git a/templ/git_repo_tree.html b/templ/git_repo_tree.html

new file mode 100644

index 0000000..a1c2cc8

--- /dev/null

+++ b/templ/git_repo_tree.html

1@@ -0,0 +1,31 @@

2+% #include "src/duc_macro.h" %

3+% #include "src/module/git_repo.h" %

4+% #include "src/module/git_repo_tree.h" %

5+

6+% DUC_TEMPLDEF(

7+ duc_git_repo_tree,

8+ git_repo_t *repo;

9+ git_repo_tree_entry_t *entry_list;

10+ size_t entry_list_len;

11+) %

12+

13+<ul class="row-list list--tree">

14+ <li class="row row--head">

15+ <span class="col col--oid">OID</span>

16+ <span class="col col--mode">MODE</span>

17+ <span class="col col--name">NAME</span>

18+ <span class="col col--size">SIZE</span>

19+ </li>

20+ % for (size_t i = 0; i < ctx->entry_list_len; i++) {

21+ git_repo_tree_entry_t *entry = &ctx->entry_list[i];

22+ %

23+ <li class="row">

24+ <span class="col col--oid">% PRINTF("%%.7s", entry->oid) %</span>

25+ <span class="col col--mode">% STRING(entry->mode) %</span>

26+ <span class="col col--name"><a href="/git/% STRING(ctx->repo->meta.name) %/blob?% if (ctx->repo->ref) { %ref=% STRING(ctx->repo->ref->oid) %&% } %oid=% STRING(entry->oid) %">% STRING(entry->name) %</a></span>

27+ <span class="col col--size">% NUMBER(entry->size) %</span>

28+ </li>

29+ % } %

30+</ul>

31+

32+% DUC_ENDTEMPL %

33

34

diff --git a/templ/index.html b/templ/index.html

new file mode 100644

index 0000000..541300b

--- /dev/null

+++ b/templ/index.html

1@@ -0,0 +1,12 @@

2+% #include "src/duc_macro.h" %

3+

4+% DUC_TEMPLDEF(

5+ duc_index,

6+) %

7+

8+<p>Hi there! I'm Sophie, and welcome to my own little place on the internet.</p>

9+<p>I'm a self-thought programmer who likes to travel and learn new stuff. I enjoy listening to music, being outside in nature, and having a chat with a good cup of coffee.</p>

10+<p>I'm currently working as an SAP E-Commerce Consultant at <i>innovate Software GmbH</i>. My expertise lies in developing custom-tailored software solutions for the SAP Commerce ecosystem.</p>

11+<p>You can get in touch with me via E-mail: <a href="mailto:info@soophie.de">info@soophie.de</a></p>

12+

13+% DUC_ENDTEMPL %

14

15

diff --git a/templ/page.html b/templ/page.html

new file mode 100644

index 0000000..00e8e83

--- /dev/null

+++ b/templ/page.html

1@@ -0,0 +1,27 @@

2+% #include "src/duc_macro.h" %

3+

4+% DUC_TEMPLDEF(

5+ duc_page,

6+ char *title;

7+ orb_module_t *module;

8+) %

9+

10+<!DOCTYPE html>

11+<html lang="en">

12+ <head>

13+ <meta charset="utf-8">

14+ <meta name="viewport" content="width=device-width, initial-scale=1">

15+ <title>% STRING(ctx->title) %</title>

16+ <link rel="stylesheet" href="/asset/style/main.css">

17+ </head>

18+ <body>

19+ <h1>so://phie</h1>

20+ <nav>

21+ <a href="/">home</a>

22+ <a href="/git">git</a>

23+ </nav>

24+ % ORB_MODULE(ctx->module) %

25+ </body>

26+</html>

27+

28+% DUC_ENDTEMPL %

29

30