package main
import "core:fmt"
import "core:mem"
import "core:slice"
import "core:strings"
import "core:sync"
import "core:time"
import libgit "lib:database/git/bindings"
import "lib:database/sqlite"
import "lib:net/http/router"
Repo :: struct {
name: string,
description: string,
head: Repo_Head,
}
Repo_Head :: struct {
author: string,
summary: string,
updated_at: time.Time,
}
get_head_info :: proc(soft_serve_path, repo_name: string) -> (Repo_Head, bool) {
repo: libgit.Repository
repo_path := strings.join({soft_serve_path, "repos", repo_name}, "/")
defer delete(repo_path)
repo_path_cstr := strings.clone_to_cstring(repo_path)
defer delete(repo_path_cstr)
result := libgit.repository_open_bare(&repo, repo_path_cstr)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return {}, false
}
defer libgit.repository_free(repo)
head_ref: libgit.Reference
result = libgit.repository_head(&head_ref, repo)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return {}, false
}
defer libgit.reference_free(head_ref)
head_obj: libgit.Object
result = libgit.reference_peel(&head_obj, head_ref, .Commit)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return {}, false
}
defer libgit.object_free(head_obj)
commit := libgit.Commit(head_obj)
// oid := libgit.commit_id(commit)
signature := libgit.commit_author(commit)
return {
author = strings.clone_from(signature.name),
summary = strings.clone_from(libgit.commit_summary(commit)),
updated_at = time.Time{_nsec = signature.sig_when.time * i64(time.Second)},
},
true
}
get_repos :: proc(repos: ^[dynamic]Repo, soft_serve_path, user_filter: string) -> bool {
libgit.libgit2_init()
defer libgit.libgit2_shutdown()
db_path := strings.join({soft_serve_path, "soft-serve.db"}, "/")
defer delete(db_path)
db := sqlite.open(db_path) or_return
defer sqlite.close(db)
user_id: int
if user_filter != "" {
// users
// id|username|admin|password|created_at|updated_at
// 1|admin|1||2026-01-15 12:34:08|2026-01-15 12:34:08
// 2|perlw|0||2026-01-15 12:47:02|2026-01-15 12:47:02
rows := sqlite.query(db, "SELECT id FROM users WHERE username = ?", user_filter) or_return
defer sqlite.rows_close(rows)
if sqlite.rows_next(rows) {
sqlite.rows_scan(rows, &user_id)
}
}
// repos
// id|name|project_name|description|private|mirror|hidden|user_id|created_at|updated_at
// 1|valkyr||Go-powered proxy|0|0|0|2|2026-01-15 14:23:52|2026-01-15 14:23:52
rows: ^sqlite.Rows
if user_id > 0 {
rows = sqlite.query(
db,
"SELECT name, description FROM repos WHERE user_id = ? AND private = 0 AND hidden = 0",
user_id,
) or_return
} else {
rows = sqlite.query(db, "SELECT name, description FROM repos WHERE private = 0 AND hidden = 0") or_return
}
defer sqlite.rows_close(rows)
for sqlite.rows_next(rows) {
name, description: string
sqlite.rows_scan(rows, &name, &description)
git_name := strings.join({name, ".git"}, "")
defer delete(git_name)
head := get_head_info(soft_serve_path, git_name) or_return
append(repos, Repo{name = name, description = description, head = head})
}
return true
}
Repo_Map :: struct {
repos: [dynamic]Repo,
soft_serve_path: string,
user_filter: string,
updated_at: time.Time,
arena: mem.Arena,
update_mutex: sync.RW_Mutex,
}
repo_map_init :: proc(repo_map: ^Repo_Map, soft_serve_path, user_filter: string) {
mem.arena_init(&repo_map.arena, make([]u8, 10485760))
context.allocator = mem.arena_allocator(&repo_map.arena)
repo_map.repos = make([dynamic]Repo, 0, 10)
repo_map.soft_serve_path = soft_serve_path
repo_map.user_filter = user_filter
}
repo_map_destroy :: proc(repo_map: ^Repo_Map) {
context.allocator = mem.arena_allocator(&repo_map.arena)
repo_map_clear(repo_map)
delete(repo_map.repos)
delete(repo_map.arena.data)
}
repo_map_clear :: proc(repo_map: ^Repo_Map) {
context.allocator = mem.arena_allocator(&repo_map.arena)
for repo in repo_map.repos {
delete(repo.name)
delete(repo.description)
delete(repo.head.author)
delete(repo.head.summary)
}
clear(&repo_map.repos)
}
repo_map_update :: proc(repo_map: ^Repo_Map) {
context.allocator = mem.arena_allocator(&repo_map.arena)
if time.diff(repo_map.updated_at, time.now()) > time.Minute {
sync.lock(&repo_map.update_mutex)
repo_map_clear(repo_map)
if !get_repos(&repo_map.repos, repo_map.soft_serve_path, repo_map.user_filter) {
fmt.eprintf("get_repos: failed refreshing repo_map")
} else {
fmt.printf("#!# refreshed repo map #!#\n")
}
repo_map.updated_at = time.now()
sync.unlock(&repo_map.update_mutex)
}
}
repo_map_get_repo :: proc(repo_map: ^Repo_Map, repo_name: string) -> (Repo, bool) {
for repo in repo_map.repos {
if repo.name == repo_name {
return repo, true
}
}
return {}, false
}
repo_map_updater :: proc(
req: router.Request,
res: ^router.Response,
ctx: rawptr,
next: router.Route_Handler,
next_ctx: rawptr,
) {
repo_map := (^Repo_Map)(ctx)
repo_map_update(repo_map)
sync.shared_lock(&repo_map.update_mutex)
next(req, res, next_ctx)
defer sync.shared_unlock(&repo_map.update_mutex)
}
Entry_Type :: enum {
File,
Dir,
Other,
}
Repo_Tree_Entry :: struct {
type: Entry_Type,
name: string,
}
get_repo_tree :: proc(
repo_map: ^Repo_Map,
repo_name: string,
ref_name := "",
base_path := "",
) -> (
[]Repo_Tree_Entry,
bool,
) {
libgit.libgit2_init()
defer libgit.libgit2_shutdown()
repo: libgit.Repository
repo_name_postfixed := strings.join({repo_name, ".git"}, "")
defer delete(repo_name_postfixed)
repo_path := strings.join({repo_map.soft_serve_path, "repos", repo_name_postfixed}, "/")
defer delete(repo_path)
repo_path_cstr := strings.clone_to_cstring(repo_path)
defer delete(repo_path_cstr)
result := libgit.repository_open_bare(&repo, repo_path_cstr)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.repository_free(repo)
head_obj: libgit.Object
if ref_name == "" {
ref: libgit.Reference
result = libgit.repository_head(&ref, repo)
if result < .Ok {
when ODIN_DEBUG {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
}
return nil, false
}
defer libgit.reference_free(ref)
result = libgit.reference_peel(&head_obj, ref, .Commit)
if result < .Ok {
when ODIN_DEBUG {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
}
return nil, false
}
} else if len(ref_name) > 4 && ref_name[0:4] == "refs" {
ref: libgit.Reference
ref_name_cstr := strings.clone_to_cstring(ref_name)
defer delete(ref_name_cstr)
result = libgit.reference_lookup(&ref, repo, ref_name_cstr)
if result < .Ok {
when ODIN_DEBUG {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
}
return nil, false
}
defer libgit.reference_free(ref)
result = libgit.reference_peel(&head_obj, ref, .Commit)
if result < .Ok {
when ODIN_DEBUG {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
}
return nil, false
}
} else {
hash_cstr := strings.clone_to_cstring(ref_name)
defer delete(hash_cstr)
result = libgit.revparse_single(&head_obj, repo, hash_cstr)
if result < .Ok {
when ODIN_DEBUG {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
}
return nil, false
}
}
defer libgit.object_free(head_obj)
tree: libgit.Tree
result = libgit.commit_tree(&tree, libgit.Commit(head_obj))
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.tree_free(tree)
sub_tree: libgit.Tree
if base_path != "" {
base_entry: libgit.Tree_Entry
base_path_cstr := strings.clone_to_cstring(base_path)
defer delete(base_path_cstr)
result = libgit.tree_entry_bypath(&base_entry, tree, base_path_cstr)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.tree_entry_free(base_entry)
if libgit.tree_entry_type(base_entry) != .Tree {
fmt.eprintf("git error: not a tree -> %s\n", base_path)
return nil, false
}
result = libgit.tree_lookup(&sub_tree, repo, libgit.tree_entry_id(base_entry))
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
}
defer if sub_tree != nil {
libgit.tree_free(sub_tree)
}
search_tree := sub_tree if sub_tree != nil else tree
count := libgit.tree_entrycount(search_tree)
repo_tree_list := make([]Repo_Tree_Entry, count)
for i in 0 ..< count {
entry := libgit.tree_entry_byindex(search_tree, i)
name := libgit.tree_entry_name(entry)
type := libgit.tree_entry_type(entry)
#partial switch type {
case .Tree:
repo_tree_list[i].type = .Dir
case .Blob:
repo_tree_list[i].type = .File
case:
repo_tree_list[i].type = .Other
}
repo_tree_list[i].name = strings.clone_from_cstring(name)
}
return repo_tree_list, true
}
get_raw_blob :: proc(repo_map: ^Repo_Map, repo_name: string, ref_name := "", filename := "") -> ([]u8, bool) {
libgit.libgit2_init()
defer libgit.libgit2_shutdown()
repo: libgit.Repository
repo_name_postfixed := strings.join({repo_name, ".git"}, "")
defer delete(repo_name_postfixed)
repo_path := strings.join({repo_map.soft_serve_path, "repos", repo_name_postfixed}, "/")
defer delete(repo_path)
repo_path_cstr := strings.clone_to_cstring(repo_path)
defer delete(repo_path_cstr)
result := libgit.repository_open_bare(&repo, repo_path_cstr)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.repository_free(repo)
ref: libgit.Reference
if ref_name == "" {
result = libgit.repository_head(&ref, repo)
} else {
ref_name_cstr := strings.clone_to_cstring(ref_name)
defer delete(ref_name_cstr)
result = libgit.reference_lookup(&ref, repo, ref_name_cstr)
}
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.reference_free(ref)
head_obj: libgit.Object
result = libgit.reference_peel(&head_obj, ref, .Commit)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.object_free(head_obj)
commit := libgit.Commit(head_obj)
tree: libgit.Tree
result = libgit.commit_tree(&tree, commit)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.tree_free(tree)
entry: libgit.Tree_Entry
filename_cstr := strings.clone_to_cstring(filename)
defer delete(filename_cstr)
result = libgit.tree_entry_bypath(&entry, tree, filename_cstr)
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.tree_entry_free(entry)
if libgit.tree_entry_type(entry) != .Blob {
fmt.eprintf("git error: not a blob -> %s\n", filename)
return nil, false
}
blob: libgit.Blob
result = libgit.blob_lookup(&blob, repo, libgit.tree_entry_id(entry))
if result < .Ok {
err := libgit.error_last()
fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
return nil, false
}
defer libgit.blob_free(blob)
raw_blob := libgit.blob_rawcontent(blob)
size := libgit.blob_rawsize(blob)
return slice.clone(slice.bytes_from_ptr(raw_blob, int(size))), true
}