Reimplementation of Pijul in C, for education, fun and absolutely no profit
#include <stdint.h>
#include <unistd.h> /* read() */
#include <fcntl.h>  /* open() */
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>

#include "common.h"
#include "types.h"
#include "scaffold.h"
#include "zstdseek.h"
#include "blake3.h"
#include "repository.h"
#include "hash.h"
#include "vertex.h"
#include "atom.h"
#include "hunk.h"
#include "mbuf.h"
#include "change.h"
#include "base32.h"
#include "bincode.h"

enum error
changefileoffsets(struct mbuf *ch, struct offsets *off)
{
	struct bincode bc;

	if (ch->len < OFFSETS_SIZE)
		return CHANGEFILE_TOOSHORT;

	bc.buf = ch->buf;
	bc.avail = OFFSETS_SIZE;

	off->version = bincode_getu64(&bc);
	off->hashed_len = bincode_getu64(&bc);
	off->unhashed_off = bincode_getu64(&bc);
	off->unhashed_len = bincode_getu64(&bc);
	off->contents_off = bincode_getu64(&bc);
	off->contents_len = bincode_getu64(&bc);
	off->total = bincode_getu64(&bc);

	if (off->total != (u64)ch->len)
		return CHANGEFILE_LENGTHMISMATCH;

	switch (off->version) {
	case VERSION:
	case VERSION_NOENC:
		return CHANGEFILE_OK;
	default:
		return CHANGEFILE_UNSUPPORTEDVERSION;
	}
}

static char *
change_strerror(enum error err)
{
	switch (err) {
	case CHANGEFILE_OK:
		return "ok";
	case CHANGEFILE_TOOSHORT:
		return ".change file is too short";
	case CHANGEFILE_LENGTHMISMATCH:
		return "length in .change file mismatch with actual length";
	case CHANGEFILE_UNSUPPORTEDVERSION:
		return ".change file version not supported";
	case CHANGEFILE_HASHMISMATCH:
		return "hash mismatch";
	}
	die("unhandled error variant: %d\n", err);
}

static void
change_readhash(struct bincode *bc, u8 *dest, u8 variant)
{
	switch (variant) {
	case HASH_BLAKE3:
		bincode_getbytes(bc, dest, BLAKE3_BYTES);
		break;
	case HASH_NONE:
		/* Do nothing - no bytes to consume */
		break;
	default:
		die("unknown hash variant: %u", variant);
	}
}

static void
change_decode_hashlist(struct bincode *bc, struct hashlist *target)
{
	usize len;
	usize i;

	len = bincode_getu64(bc);
	target->len = len;
	target->entries = xmalloc(sizeof(struct hash) * len);

	for (i = 0; i < target->len; i++) {
		target->entries[i].variant = (u8)bincode_getu32(bc);
		change_readhash(
			bc, target->entries[i].bytes, target->entries[i].variant
		);
	}
}

static void
change_decode_position(struct bincode *bc, struct position *pos)
{
	if (bincode_getu8(bc)) {
		pos->change.variant = (u8)bincode_getu32(bc);
		change_readhash(
			bc, (u8 *)&pos->change.bytes, pos->change.variant
		);
	}
	pos->pos = bincode_getu64(bc);
}

static void
change_decode_vertex(struct bincode *bc, struct vertex *v)
{
	struct hash *hash;

	hash = &v->change;

	u8 ishash = bincode_getu8(bc);
	if (ishash) {
		hash->variant = (u8)bincode_getu32(bc);
		change_readhash(bc, (u8 *)hash->bytes, hash->variant);
	}
	v->start = bincode_getu64(bc);
	v->end = bincode_getu64(bc);
}

static void
change_decode_introducedby(struct bincode *bc, struct hash *hash)
{
	u8 ishash;

	ishash = bincode_getu8(bc);
	if (ishash) {
		hash->variant = (u8)bincode_getu32(bc);
		change_readhash(bc, (u8 *)hash->bytes, hash->variant);
	}
}

static void
change_decode_newedge(struct bincode *bc, struct edge *edge)
{
	edge->previous = bincode_getu8(bc);
	edge->flag = bincode_getu8(bc);

	change_decode_position(bc, &edge->from);
	change_decode_vertex(bc, &edge->to);
	change_decode_introducedby(bc, &edge->introducedby);
}

static void
change_decode_positionlist(struct bincode *bc, struct positionlist *poslist)
{
	usize len;
	usize i;

	poslist->len = len = bincode_getu64(bc);
	if (len > 1)
		poslist->entries = xmalloc(sizeof(struct position) * (len - 1));
	if (len > 0) {
		change_decode_position(bc, &poslist->first);
		for (i = 0; i < len - 1; i++)
			change_decode_position(bc, &poslist->entries[i]);
	}
}

static void
change_decode_newvertex(struct bincode *bc, struct newvertex *newvertex)
{
	/* Fields: upcontext: Vec<Position>, downcontext: Vec<Position>, flag, start, end: ChangePosition, inode: Position */

	change_decode_positionlist(bc, &newvertex->upcontext);
	change_decode_positionlist(bc, &newvertex->downcontext);

	newvertex->flag = bincode_getu8(bc);
	newvertex->start = bincode_getu64(bc);
	newvertex->end = bincode_getu64(bc);

	change_decode_position(bc, &newvertex->inode);
}

static void
change_decode_edgemap(struct bincode *bc, struct edgemap *edgemap)
{
	usize i;
	usize len;
	edgemap->edges.len = len = bincode_getu64(bc);
	edgemap->edges.entries = xmalloc(sizeof(struct edge) * len);
	for (i = 0; i < len; i++) {
		change_decode_newedge(bc, &edgemap->edges.entries[i]);
	}
	/* inode */
	change_decode_position(bc, &edgemap->inode);
}

static void
change_decode_atom(struct bincode *bc, struct atom *atom)
{
	u32 x;

	x = bincode_getu32(bc);
	switch (x) {
	case 0:
		atom->atomtype = NEW_VERTEX;
		change_decode_newvertex(bc, &atom->newvertex);
		break;
	case 1:
		/* A list of edges */
		atom->atomtype = EDGE_MAP;
		change_decode_edgemap(bc, &atom->edgemap);
		break;
	default:
		die("unknown atom type: %u", x);
	}
}

static u64
change_readstr(char **dest, struct bincode *bc)
{
	u64 len;
	len = bincode_getu64(bc);
	*dest = xmalloc(len + 1);
	bincode_getstr(bc, *dest, len);
	return len;
}

static void
change_decode_local(struct bincode *bc, struct local *local)
{
	change_readstr(&local->path, bc);
	local->line = bincode_getu64(bc);
}

static void
change_decode_encoding(struct bincode *bc, char **encoding)
{
	/* This is an optional field - basically just an Option<String> */
	if (bincode_getu8(bc)) {
		change_readstr(encoding, bc);
	}
}

static void
change_decode_encoding_noenc(struct bincode *bc, char **encoding)
{
	(void)bc;
	*encoding = xstrdup("UTF-8");
}

/**
 * Decode the hunks in a change
 */
static void
change_decode_hunks(struct bincode *bc, struct hashed *hashed)
{
	u64 len, slen;
	usize i;
	void (*fn_change_decode_encoding)(struct bincode *, char **);

	fn_change_decode_encoding = &change_decode_encoding;
	if (hashed->version == VERSION_NOENC)
		fn_change_decode_encoding = change_decode_encoding_noenc;

	len = bincode_getu64(bc);
	hunklistinit(&hashed->hunks, len);

	for (i = 0; i < len; i++) {
		struct basehunk *bhunk = &hashed->hunks.entries[i];
		bhunk->hunktype = bincode_getu32(bc);

		switch (bhunk->hunktype) {
		case EDIT:
			/* An edit is a (1) struct atom (2) Local (3) Option<Encoding> */
			change_decode_atom(
				bc, &bhunk->edit.change
			); /* the change field */
			change_decode_local(bc, &bhunk->edit.local);
			fn_change_decode_encoding(bc, &bhunk->edit.encoding);
			break;
		case REPLACEMENT:
			change_decode_atom(bc, &bhunk->replacement.change);
			change_decode_atom(bc, &bhunk->replacement.replacement);
			change_decode_local(bc, &bhunk->replacement.local);
			fn_change_decode_encoding(
				bc, &bhunk->replacement.encoding
			);
			break;
		case FILE_ADD:
			change_decode_atom(bc, &bhunk->fileadd.addname);
			change_decode_atom(bc, &bhunk->fileadd.addinode);
			if (bincode_getu8(bc))
				change_decode_atom(bc, &bhunk->fileadd.contents);
			change_readstr(&bhunk->fileadd.path, bc);
			fn_change_decode_encoding(bc, &bhunk->fileadd.encoding);
			break;
		case FILE_MOVE:
			change_decode_atom(bc, &bhunk->filemove.del);
			change_decode_atom(bc, &bhunk->filemove.add);
			slen = bincode_getu64(bc);
			bhunk->filemove.path = xmalloc(slen + 1);
			bincode_getstr(bc, bhunk->filemove.path, slen);
			break;
		case FILE_DEL:
			change_decode_atom(bc, &bhunk->filedel.del);
			if (bincode_getu8(bc)) {
				change_decode_atom(bc, &bhunk->filedel.contents);
			}
			slen = bincode_getu64(bc);
			bhunk->filedel.path = xmalloc(slen + 1);
			bincode_getstr(bc, bhunk->filedel.path, slen);
			fn_change_decode_encoding(bc, &bhunk->filedel.encoding);
			break;
		case FILE_UNDEL:
			change_decode_atom(bc, &bhunk->fileundel.undel);
			if (bincode_getu8(bc))
				change_decode_atom(
					bc, &bhunk->fileundel.contents
				);
			slen = bincode_getu64(bc);
			bhunk->fileundel.path = xmalloc(slen + 1);
			bincode_getstr(bc, bhunk->fileundel.path, slen);
			fn_change_decode_encoding(
				bc, &bhunk->fileundel.encoding
			);
			break;
		case SOLVE_ORDER_CONFLICT:
			change_decode_atom(
				bc, &bhunk->solveorderconflict.change
			);
			change_decode_local(
				bc, &bhunk->solveorderconflict.local
			);
			break;
		case UNSOLVE_ORDER_CONFLICT:
			change_decode_atom(
				bc, &bhunk->unsolveorderconflict.change
			);
			change_decode_local(
				bc, &bhunk->unsolveorderconflict.local
			);
			break;
		case SOLVE_NAME_CONFLICT:
			change_decode_atom(bc, &bhunk->solvenameconflict.name);
			slen = bincode_getu64(bc);
			bhunk->solvenameconflict.path = xmalloc(slen + 1);
			bincode_getstr(bc, bhunk->solvenameconflict.path, slen);
			break;
		case UNSOLVE_NAME_CONFLICT:
			change_decode_atom(bc, &bhunk->unsolvenameconflict.name);
			slen = bincode_getu64(bc);
			bhunk->unsolvenameconflict.path = xmalloc(slen + 1);
			bincode_getstr(
				bc, bhunk->unsolvenameconflict.path, slen
			);
			break;
		case RESURRECT_ZOMBIES:
			change_decode_atom(bc, &bhunk->resurrectzombies.change);
			change_decode_local(bc, &bhunk->resurrectzombies.local);
			fn_change_decode_encoding(
				bc, &bhunk->resurrectzombies.encoding
			);
			break;
		case ADD_ROOT:
			change_decode_atom(bc, &bhunk->addroot.name);
			change_decode_atom(bc, &bhunk->addroot.inode);
			break;
		case DEL_ROOT:
			change_decode_atom(bc, &bhunk->delroot.name);
			change_decode_atom(bc, &bhunk->delroot.inode);
			break;
		default:
			die("not yet implemented: %s",
			    hunk_basehunk_type_str(bhunk->hunktype));
		}
	}
}

void
change_decode_author(struct bincode *bc, struct author *author)
{
	usize len, i;

	len = bincode_getu64(bc);

	author->len = len;
	author->entries = xmalloc(sizeof(struct authorentry) * len);

	for (i = 0; i < len; i++) {
		change_readstr(&author->entries[i].key, bc);
		change_readstr(&author->entries[i].value, bc);
	}
}

void
change_decode_author_noenc(struct bincode *bc, struct author *author)
{
	usize i;
	char *name, *full_name, *email;

	i = 1;
	full_name = email = NULL;

	change_readstr(&name, bc);

	if (bincode_getu8(bc)) {
		i++;
		change_readstr(&full_name, bc);
	}

	if (bincode_getu8(bc)) {
		i++;
		change_readstr(&email, bc);
	}

	author->len = i;
	author->entries = xmalloc(sizeof(struct authorentry) * i);
	i = 0;

	author->entries[i].key = xstrdup("name");
	author->entries[i].value = name;

	if (full_name) {
		i++;
		author->entries[i].key = xstrdup("full_name");
		author->entries[i].value = full_name;
	}
	if (email) {
		i++;
		author->entries[i].key = xstrdup("email");
		author->entries[i].value = email;
	}
}

enum error
change_decodehashed(
	u8 *data, usize datalen, u8 *expectedhash, struct hashed *hashed
)
{
	u8 computedhash[BLAKE3_LEN];
	struct bincode bc;
	u64 len;
	usize i;
	void (*fn_change_decode_author)(struct bincode *, struct author *);

	/*Step 1. Compute the hash */
	if (expectedhash != NULL) {
		blake3_hash(computedhash, data, datalen);
		if (blake3_cmp(computedhash, expectedhash))
			return CHANGEFILE_HASHMISMATCH;
	}

	bc.avail = datalen;
	bc.buf = data;

	hashed->version = bincode_getu64(&bc);
	change_readstr(&hashed->header.message, &bc);

	hashed->header.description = NULL;
	if (bincode_getu8(&bc)) {
		change_readstr(&hashed->header.description, &bc);
	}

	len = change_readstr(&hashed->header.timestamp, &bc);
	if (len != 30)
		fprintf(stderr,
			"warning: timestamp field has unexpected length %lu\n",
			len);

	len = bincode_getu64(&bc);
	hashed->header.authors.len = len;
	hashed->header.authors.map = xmalloc(sizeof(struct author) * len);

	fn_change_decode_author = change_decode_author;
	if (hashed->version == VERSION_NOENC)
		fn_change_decode_author = change_decode_author_noenc;

	for (i = 0; i < hashed->header.authors.len; i++) {
		fn_change_decode_author(&bc, &hashed->header.authors.map[i]);
	}

	/* Decode dependencies (Vec<Hash>) */
	change_decode_hashlist(&bc, &hashed->dependencies);

	/* Decode extra_known (Vec<Hash> again) */
	change_decode_hashlist(&bc, &hashed->extraknown);

	/* metadata: Vec<u8> */
	len = bincode_getu64(&bc);
	hashed->metadata = NULL;
	if (len > 0) {
		hashed->metadata = xmalloc(len);
		bincode_getbytes(&bc, hashed->metadata, len);
	}

	/* changes (without the contents): Vec<Hunk> */
	change_decode_hunks(&bc, hashed);

	return CHANGEFILE_OK;
}

/**
 * Given an atom from a given change (by hash), extract the contents to return.
 *
 * Returns a malloc()'ed buffer that must be free()'d, if there were
 * any contents to available. Returns NULL otherwise. The size of the
 * returned buffer is provided in the "n" parameter.
 */
static u8 *
changecontents(
	struct changestore *changes, struct atom *change, u8 *contents,
	usize *n
)
{
	struct newvertex *v;
	struct edgemap *e;
	struct change *ch;
	struct vertex *vto = NULL;
	u8 *buf = NULL;
	usize sz = 0;

	switch (change->atomtype) {
	case NEW_VERTEX:
		v = &change->newvertex;
		sz = v->end - v->start;
		buf = xmalloc(sizeof(u8) * sz);
		memcpy(buf, &contents[v->start], sz);
		break;
	case EDGE_MAP:
		e = &change->edgemap;
		if (e->edges.len == 0) {
			/* Technically an error */
			break;
		}
		if (!(e->edges.entries[0].flag & EDGE_FLAG_DELETED))
			break;
		for (usize i = 0; i < e->edges.len; i++) {
			struct edge *edge = &e->edges.entries[i];

			if (vto && vertexeq(&edge->to, vto))
				continue;
			vto = &edge->to;
			usize z = vto->end - vto->start;

			ch = changestoreget(changes, &vto->change);
			if (!ch) {
				changestoreload(changes, &vto->change);
				ch = changestoreget(changes, &vto->change);
			}

			// We find a change with some contents, and we
			// need to extract contents[start; end] we
			// then need to either xmalloc() buf, or
			// extend it to include more bytes
			if (ch) {
				if (buf)
					buf = xrealloc(
						buf, sz + sizeof(u8) * z
					);
				else
					buf = xmalloc(sizeof(u8) * z);
				memcpy(&buf[sz], &ch->contents[vto->start], z);
				sz += z;
			}
		}
		break;
	}
	*n = sz;
	return buf;
}

void
print_positionlist(struct positionlist *plist)
{
	struct position *p;
	usize i;

	p = &plist->first;
	printf("P0.%lu", p->pos);
	if (plist->len > 1)
		for (i = 0; i < plist->len - 1; i++) {
			printf("P%lu.%lu", i + 1, plist->entries[i].pos);
		}
}

void
print_edgeflags(u8 flags)
{
	if (flags & EDGE_FLAG_BLOCK)
		printf("B");
	if (flags & EDGE_FLAG_FOLDER)
		printf("F");
	if (flags & EDGE_FLAG_DELETED)
		printf("D");
}

void
printcontents(u8 *buf, usize len, char prefix)
{
	int waseol = 1;
	usize i;
	u8 x;

	for (i = 0; i < len; i++) {
		if (waseol) {
			printf("%c ", prefix);
			waseol = 0;
		}
		x = buf[i];
		putchar(x);
		if (x == '\n')
			waseol = 1;
	}
	if (!waseol)
		printf("\n");
}

void
printedgemap(struct edgemap *m)
{
	printf("edgemap {\n  edges: [\n");
	for (usize i = 0; i < m->edges.len; i++) {
		struct edge *e = &m->edges.entries[i];
		printf("    edge { previous = %u, flag = ", e->previous);
		print_edgeflags(e->flag);
		printf(", from = position { change = ");
		hashprint(&e->from.change);
		printf(", pos = %lu }", e->from.pos);

		printf(", to = vertex { change = ");
		hashprint(&e->to.change);
		printf(", start = %lu, end = %lu }, introducedby = ",
		       e->to.start, e->to.end);
		hashprint(&e->introducedby);
		printf(" }\n");
	}
	printf("  ]\n}\n");
}

/**
 * TODO To properly print EDGE_MAP types we need access to referenced changes to
 * pull deleted lines.
 */
void
print_atom(
	struct changestore *changes, struct atom *a, u8 *contents,
	int verbose
)
{
	struct newvertex *v;
	struct edgemap *m;
	usize n;
	u8 *res;

	switch (a->atomtype) {
	case NEW_VERTEX:
		v = &a->newvertex;
		/* printf("up: "); */
		/* print_positionlist(&v->upcontext); */
		/* printf(", "); */

		/* printf("new: %lu:%lu, down: ", v->start, v->end); */

		/* print_positionlist(&v->downcontext); */
		/* printf("\n"); */

		printcontents(&contents[v->start], v->end - v->start, '+');

		break;
	case EDGE_MAP:
		m = &a->edgemap;

		if (verbose)
			printedgemap(m);

		res = changecontents(changes, a, contents, &n);
		if (res) {
			printcontents(res, n, '-');
			free(res);
		}
		break;
	default:
		die("unknown atom type: %u", a->atomtype);
	}
}

/**
 * decode a filemetadata struct from the given contents - the input
 * (contents) are also bincoded, so we need to deserialize it
 */
static void
read_filemetadata(struct filemetadata *m, u8 *contents, usize contents_len)
{
	usize len;
	struct bincode bc = { .avail = contents_len, .buf = contents };

	m->inodemetadata = bincode_getu16(&bc);
	len = bincode_getu64(&bc);
	m->basename = xmalloc(len + 1);
	bincode_getstr(&bc, m->basename, len);
	change_decode_encoding(&bc, &m->encoding);
}

/**
 * In the VERSION_NOENC file metadata (for file additions), the
 * filemetadata contains *only* the inodemetadata directly followed by
 * the basename. The basename is not bincode-encoded, so we assume the
 * length of the string is the given content length minus 2 (for the
 * first two bytes used by the inodemetadata).
 */
static void
read_filemetadata_noenc(
	struct filemetadata *m, u8 *contents, usize contents_len
)
{
	usize len;
	struct bincode bc = { .avail = contents_len, .buf = contents };

	m->inodemetadata = bincode_getu16(&bc);
	len = contents_len - 2;
	m->basename = xmalloc(len + 1);
	memcpy(m->basename, &contents[2], len);
	m->basename[len] = '\0';
}

void
print_raw_change(struct changestore *changes, struct change *ch)
{
	usize i;

	printf("offsets\n");
	printf("  version: %lu\n", ch->offsets.version);
	printf("  hashed_len: %lu\n", ch->offsets.hashed_len);
	printf("  unhashed_off: %lu\n", ch->offsets.unhashed_off);
	printf("  unhashed_len: %lu\n", ch->offsets.unhashed_len);
	printf("  contents_off: %lu\n", ch->offsets.contents_off);
	printf("  contents_len: %lu\n", ch->offsets.contents_len);
	printf("  total: %lu\n", ch->offsets.total);
	printf("\n");

	if (ch->offsets.contents_len > 0) {
		printf("contents: [");
		printf("0x%02x", ch->contents[0]);
		for (i = 1; i < ch->offsets.contents_len; i++) {
			printf(", 0x%02x", ch->contents[i]);
		}
		printf("]\n");
	}
}

void
print_change(
	struct changestore *changes, struct hashed *hashed, u8 *contents,
	int verbose
)
{
	usize i;
	void (*fn_read_filemetadata)(struct filemetadata *, u8 *, usize);

	fn_read_filemetadata = read_filemetadata;
	if (hashed->version == VERSION_NOENC)
		fn_read_filemetadata = read_filemetadata_noenc;

	printf("message = %s\n", hashed->header.message);
	if (hashed->header.description)
		printf("description = '%s'\n", hashed->header.description);
	printf("timestamp = '%s'\n\n", hashed->header.timestamp);

	for (i = 0; i < hashed->header.authors.len; i++) {
		printf("[[authors]]\n");
		usize j;
		for (j = 0; j < hashed->header.authors.map[i].len; j++) {
			printf("%s = '%s'\n",
			       hashed->header.authors.map[i].entries[j].key,
			       hashed->header.authors.map[i].entries[j].value);
		}
		printf("\n");
	}

	printf("# Dependencies\n");
	for (i = 0; i < hashed->dependencies.len; i++) {
		printf(" ");
		hashprintln(&hashed->dependencies.entries[i]);
	}
	for (i = 0; i < hashed->extraknown.len; i++) {
		printf("+");
		hashprintln(&hashed->extraknown.entries[i]);
	}
	printf("\n# Hunks\n");

	for (i = 0; i < hashed->hunks.len; i++) {
		struct basehunk *hunk = &hashed->hunks.entries[i];
		printf("\n%lu. %s", i + 1,
		       hunk_basehunk_type_str(hunk->hunktype));

		switch (hunk->hunktype) {
		case EDIT: {
			struct edit *e = &hunk->edit;
			struct atom *c = &e->change;
			printf(" in %s:%lu (%s)", e->local.path, e->local.line,
			       e->encoding);
			printf("\n");
			print_atom(changes, c, contents, verbose);
			break;
		}
		case REPLACEMENT: {
			struct replacement *r = &hunk->replacement;
			struct atom *c =
				&r->change; /* Expected to be an edgemap */
			struct atom *replacement =
				&r->replacement; /* Expected to be a newvertex */
			printf(" in %s:%lu (%s)", r->local.path, r->local.line,
			       r->encoding);
			printf("\n");

			print_atom(changes, c, contents, verbose);

			print_atom(changes, replacement, contents, verbose);
			break;
		}
		case FILE_ADD: {
			struct fileadd *f = &hunk->fileadd;
			struct filemetadata metadata = { 0 };
			u64 start, end;

			start = f->addname.newvertex.start;
			end = f->addname.newvertex.end;
			if (start == end) {
				/* a directory */
			} else {
				fn_read_filemetadata(
					&metadata, &contents[start], end - start
				);
			}

			printf(" %s (%s)\n", metadata.basename,
			       metadata.encoding);
			/* f->contents is presumably empty when either
			 * (a) the file is a directory, or (b) the new
			 * file is really empty. For now, assume it's
			 * present (it's an Option<Atom> though) and
			 * crash on dirs */
			print_atom(changes, &f->contents, contents, verbose);

			free(metadata.basename);
			if (metadata.encoding)
				free(metadata.encoding);
			break;
		}
		case FILE_MOVE: {
			/* read filemetadata from contents */
			struct filemove *f = &hunk->filemove;
			struct filemetadata metadata = { 0 };
			u64 start, end;

			start = f->add.newvertex.start;
			end = f->add.newvertex.end;
			fn_read_filemetadata(
				&metadata, &contents[start], end - start
			);

			printf(" %s -> %s\n", f->path, metadata.basename);

			free(metadata.basename);
			if (metadata.encoding)
				free(metadata.encoding);

			break;
		}
		case FILE_DEL: {
			struct filedel *f = &hunk->filedel;
			printf(" %s\n", f->path);
			break;
		}
		case FILE_UNDEL: {
			struct fileundel *f = &hunk->fileundel;
			printf(" %s\n", f->path);
			break;
		}
		case SOLVE_ORDER_CONFLICT: {
			struct solveorderconflict *s =
				&hunk->solveorderconflict;
			printf(" in %s:%lu\n", s->local.path, s->local.line);
			break;
		}
		case UNSOLVE_ORDER_CONFLICT: {
			struct unsolveorderconflict *s =
				&hunk->unsolveorderconflict;
			printf(" in %s:%lu\n", s->local.path, s->local.line);
			break;
		}
		case SOLVE_NAME_CONFLICT: {
			struct solvenameconflict *s = &hunk->solvenameconflict;
			/* FIXME: get  */
			printf(" in %s\n", s->path);
			break;
		}
		case UNSOLVE_NAME_CONFLICT: {
			struct unsolvenameconflict *s =
				&hunk->unsolvenameconflict;
			printf(" in %s\n", s->path);
			break;
		}
		case RESURRECT_ZOMBIES: {
			struct resurrectzombies *z = &hunk->resurrectzombies;
			printf(" in %s:%lu\n", z->local.path, z->local.line);
			break;
		}
		case ADD_ROOT: {
			struct addroot *r = &hunk->addroot;
			printf(" start = %lu\n", r->name.newvertex.start);
			break;
		}
		case DEL_ROOT: {
			struct delroot *r = &hunk->delroot;
			printf("name:  ");
			print_atom(changes, &r->name, contents, verbose);
			printf("inode: ");
			print_atom(changes, &r->inode, contents, verbose);
			break;
		}
		default:
			printf(" [not yet implemented]\n");
			break;
		}
	}
}

/**
 * Format a path to a change file, given the input repo dir and a
 * hash. The result is placed in dst.
 */
static void
formatchangepath(char *dst, const char *repodir, const char *hashstr)
{
	/* assert strlen(repodir) < PATH_MAX - strlen(".pijul/changes/XX/YYYYYYYYY...YYY.change") */
	char *p;

	p = stpncpy(dst, repodir, PATH_MAX);
	*p++ = '/';
	p = stpncpy(p, DOTPIJUL, 6);
	*p++ = '/';
	p = stpncpy(p, "changes/", 8);

	*p++ = hashstr[0];
	*p++ = hashstr[1];
	*p++ = '/';

	p = stpncpy(p, &hashstr[2], 51);
	stpncpy(p, ".change", 7);
}

static int
loadchange(
	struct change *c, struct hash *hash, const char *repodir,
	const char *hashstr
)
{
	int fd, err;
	enum error cherr;
	char chfile[PATH_MAX] = { 0 };
	struct mbuf buf = { 0 };
	struct offsets *off;
	struct hashed *hashed;
	u8 contents_hash[32];
	u8 *scratch;
	usize r;

	off = &c->offsets;
	hashed = &c->hashed;
	err = 0;

	formatchangepath(chfile, repodir, hashstr);

	fd = open(chfile, O_RDONLY);
	if (fd == -1) {
		printf("error: %s\n", strerror(errno));
		return -1;
	}

	err = mkmbuf(fd, &buf);
	if (err) {
		printf("error: %s\n", strerror(errno));
		goto out2;
	}
	// FIXME: hash decoding should result in a "struct hash" not
	// just raw bytes
	b32dec(contents_hash, hashstr);
	memcpy(hash->bytes, contents_hash, BLAKE3_BYTES);
	hash->variant = HASH_BLAKE3;

	cherr = changefileoffsets(&buf, off);
	if (cherr != CHANGEFILE_OK) {
		printf("error: %s\n", change_strerror(cherr));
		err = (int)cherr;
		goto out;
	}

	scratch = xmalloc(sizeof(u8) * off->hashed_len);
	r = zstdseek_decompress(
		scratch, off->hashed_len, buf.buf + OFFSETS_SIZE,
		off->unhashed_off - OFFSETS_SIZE
	);
	if (!r) {
		err = 1;
		free(scratch);
		goto out;
	}
	err = change_decodehashed(
		scratch, off->hashed_len, contents_hash, hashed
	);
	free(scratch);

	if (err != 0) {
		printf("error: failed to decode hashed\n");
		goto out;
	}

	c->contents = xmalloc(sizeof(u8) * off->contents_len);
	r = zstdseek_decompress(
		c->contents, off->contents_len, buf.buf + off->contents_off,
		off->total - off->contents_off
	);
	if (!r) {
		err = 1;
		free(c->contents);
	}
	err = 0;
out:
	freembuf(&buf);
out2:
	close(fd);
	return err;
}

/**
 * Same as loadchange() but takes a "struct hash" as input instead of a string
 */
static int
loadchangeh(
	struct change *c, struct hash *h, const char *repodir, struct hash *hash
)
{
	char hashstr[54];

	/* FIXME consider that it might not only be blake3 bytes */
	b32enc(hashstr, hash->bytes);

	return loadchange(c, h, repodir, hashstr);
}

struct change *
changestoreget(struct changestore *store, struct hash *hash)
{
	usize i;

	for (i = 0; i < store->len; i++) {
		if (hasheq(hash, &store->entries[i].hash))
			return &store->entries[i].change;
	}
	return NULL;
}

// loading changes as needed: ==15877==   total heap usage: 326 allocs, 326 frees, 50,066,715 bytes allocated
/**
 * load a change by the given hash into the changestore
 */
int
changestoreload(struct changestore *store, struct hash *hash)
{
	struct changeentry *ch;
	usize i;
	int err;

	if (store->len == store->cap) {
		/* resize */
		usize newcap = store->cap << 1;
		store->entries = xrealloc(
			store->entries, sizeof(struct changeentry) * newcap
		);
		store->cap = newcap;
	}

	i = store->len;
	ch = &store->entries[i];

	err = loadchangeh(&ch->change, &ch->hash, store->repodir, hash);
	if (err)
		return err;

	store->len++;
	return err;
}

void
changestoreinit(struct changestore *s, usize cap, const char *repodir)
{
	s->repodir = xstrdup(repodir);
	s->entries = xmalloc(sizeof(struct changeentry) * cap);
	s->cap = cap;
	s->len = 0;
}

void
changestorefree(struct changestore *s)
{
	usize i;
	struct change *c;

	/* Free all the entries first */
	for (i = 0; i < s->len; i++) {
		c = &s->entries[i].change;
		hashedfree(&c->hashed);
		if (c->contents)
			free(c->contents);
	}
	free(s->repodir);
	free(s->entries);
}

void
authorsfree(struct authors *authors)
{
	usize i, j;
	struct author *author;

	for (i = 0; i < authors->len; i++) {
		author = &authors->map[i];
		for (j = 0; j < author->len; j++) {
			free(author->entries[j].key);
			free(author->entries[j].value);
		}
		free(author->entries);
	}
	free(authors->map);
}

void
hashedfree(struct hashed *h)
{
	free(h->header.message);
	if (h->header.description)
		free(h->header.description);
	free(h->header.timestamp);
	authorsfree(&h->header.authors);

	hashlist_free(&h->dependencies);
	hashlist_free(&h->extraknown);
	if (h->metadata)
		free(h->metadata);
	hunklistfree(&h->hunks);
}

/**
 * Takes an input hash, and tries to find a change file to open and read out.
 */
int
change(const char *hash, int verbose, int raw, struct repository *repo)
{
	int err;
	struct changestore changestore = { 0 };
	struct changeentry *ch;
	usize x;

	changestoreinit(&changestore, 4, repo->path);
	ch = &changestore.entries[0];

	err = loadchange(&ch->change, &ch->hash, repo->path, hash);
	if (err)
		goto changeout;

	changestore.len = x = 1;

	changestore.entries[0].num = 1;

	if (raw)
		print_raw_change(&changestore, &ch->change);
	else
		print_change(
			&changestore, &ch->change.hashed, ch->change.contents,
			verbose
		);

changeout:
	changestorefree(&changestore);
	return err;
}

static void
cmd_change_usage()
{
	printf("ani change [-h] [-r] [-v] <hash>\n");
}

int
cmd_change(int argc, char **argv, struct repository *repo)
{
	const char *hash;
	usize hash_len;
	int verbose;
	int raw;
	int c;

	verbose = raw = 0;

	while ((c = getopt(argc, argv, "hrv")) != -1) {
		switch (c) {
		case 'v':
			verbose = 1;
			break;
		case 'r':
			raw = 1;
			break;
		case 'h':
			cmd_change_usage();
			return 0;
		case '?':
			fprintf(stderr, "unrecognized option: '-%c'\n", optopt);
			return -1;
		}
	}
	if (optind >= argc) {
		fprintf(stderr, "error: invalid number of arguments. See -h\n");
		return -1;
	}

	hash = argv[optind];

	/**
	 * input validation
	 * FIXME: Support prefixes properly
	 */
	if ((hash_len = strnlen(hash, 54)) != 53) {
		fprintf(stderr,
			"error: hash must be exactly 53 characters (got: %lu)\n",
			hash_len);
		return -1;
	}

	return change(hash, verbose, raw, repo);
}