/*
* The tree page, which renders one level of a repository at a revision as a
* table of the folders and files in it, or the contents of the file when the
* path names one. A file is shown as numbered source, passed through the
* repository's source filter when it has one, or as a hex dump when its bytes
* look binary. A folder whose only entry is another folder is followed, and
* both are named in the same row, so a long chain of single folders does not
* cost a page each.
*/
#define USE_THE_REPOSITORY_VARIABLE
#include "cgit.h"
#include "filter.h"
#include "html.h"
#include "ui-shared.h"
#include "ui-tree.h"
#define HEXDUMP_ROW_BYTES 32
#define HEXDUMP_ROW_HALF (HEXDUMP_ROW_BYTES / 2)
#define HEXDUMP_GAP_WIDTH 4
enum walk_state {
WALK_LOOKING,
WALK_LISTING,
WALK_BLOB_SHOWN,
WALK_ERROR_SHOWN,
};
struct ls_entry {
struct object_id oid;
char *name;
unsigned mode;
};
struct walk_tree_context {
char *rev;
char *match_path;
enum walk_state state;
struct ls_entry *entries;
size_t entries_nr, entries_alloc;
};
/*
* The count runs past one as soon as a second entry or a file turns up, which
* ends the descent.
*/
struct only_child {
struct strbuf *path;
struct object_id oid;
char *name;
size_t count;
};
/*
* A formatted write per line meant a syscall and a temporary buffer for every
* line of the file, so the anchors are handed over in batches. Building the
* column whole was rejected because it would come to several times the size of
* the blob.
*/
static void print_linenumbers(const char *buf, unsigned long size)
{
const char *numberfmt = "%1$d\n";
struct strbuf numbers = STRBUF_INIT;
unsigned long lineno = 0, idx = 0;
if (size) {
strbuf_addf(&numbers, numberfmt, ++lineno);
// The newline that ends the last line must not open a line of
// its own, so the final byte is left out of the scan.
while (idx < size - 1) {
if (buf[idx] == '\n') {
strbuf_addf(&numbers, numberfmt, ++lineno);
if (numbers.len >= HTML_BATCH) {
html_raw(numbers.buf, numbers.len);
strbuf_reset(&numbers);
}
}
idx++;
}
html_raw(numbers.buf, numbers.len);
}
strbuf_release(&numbers);
}
static void print_text_buffer(const char *filename, char *buf,
unsigned long size)
{
html("
\n");
if (ctx.cfg.enable_tree_linenumbers) {
html("");
print_linenumbers(buf, size);
html(" | \n");
} else {
html("
\n");
}
if (ctx.repo->source_filter) {
char *filter_arg = xstrdup(filename);
html("");
cgit_open_filter(ctx.repo->source_filter, filter_arg);
html_raw(buf, size);
cgit_close_filter(ctx.repo->source_filter);
free(filter_arg);
html("
|
\n");
return;
}
// Syntax highlighting ships as one of the filters under
// custom/extensions, which keeps knowledge of languages out of cgit.
html("");
html_txt(buf);
html("
| \n");
}
/*
* git's ctype macros are locale free, but there is no isgraph among them, so
* the dump works one out for itself.
*/
static int is_graphic(unsigned char ch)
{
return isprint(ch) && !isspace(ch);
}
static void print_binary_buffer(char *buf, unsigned long size)
{
unsigned long offset, idx;
char ascii[HEXDUMP_ROW_BYTES + 1];
struct strbuf row = STRBUF_INIT;
html("\n");
html("| ofs | hex dump | ascii |
");
// At the default blob size limit a write per byte spent almost all of
// its time in the kernel, so a row goes out in one write.
for (offset = 0; offset < size;
offset += HEXDUMP_ROW_BYTES, buf += HEXDUMP_ROW_BYTES) {
strbuf_reset(&row);
strbuf_addf(&row, "| %04lx | ", offset);
for (idx = 0; idx < HEXDUMP_ROW_BYTES && offset + idx < size; idx++) {
int gap = idx == HEXDUMP_ROW_HALF ? HEXDUMP_GAP_WIDTH : 1;
strbuf_addf(&row, "%*s%02x", gap, "", buf[idx] & 0xff);
}
strbuf_addstr(&row, " | ");
html_raw(row.buf, row.len);
for (idx = 0; idx < HEXDUMP_ROW_BYTES && offset + idx < size; idx++)
ascii[idx] = is_graphic(buf[idx]) ? buf[idx] : '.';
ascii[idx] = '\0';
html_txt(ascii);
html(" |
\n");
}
strbuf_release(&row);
html("
\n");
}
/*
* Answer whether the page layout was left open for the caller to close. A
* false means a complete error page went out in place of the blob, so nothing
* may be added to it.
*/
static bool print_object(const struct object_id *oid, const char *path,
const char *filename, const char *rev)
{
enum object_type type;
char *buf;
unsigned long size;
bool is_binary;
type = odb_read_object_info(the_repository->objects, oid, &size);
if (type == OBJ_BAD) {
cgit_print_error_page(404, "Not found",
"Bad object name: %s", oid_to_hex(oid));
return false;
}
if (ctx.cfg.max_blob_size && size / 1024 > (unsigned long)ctx.cfg.max_blob_size) {
cgit_print_error_page(413, "Too large",
"blob size (%luKB) exceeds display size limit (%dKB)",
size / 1024, ctx.cfg.max_blob_size);
return false;
}
buf = odb_read_object(the_repository->objects, oid, &type, &size);
if (!buf) {
cgit_print_error_page(500, "Internal server error",
"Error reading object %s", oid_to_hex(oid));
return false;
}
is_binary = buffer_is_binary(buf, size);
cgit_set_title_from_path(path);
cgit_print_layout_start();
htmlf("blob: %s (", oid_to_hex(oid));
cgit_plain_link("plain", NULL, NULL, ctx.qry.head,
rev, path);
if (ctx.repo->enable_blame && !is_binary) {
html(") (");
cgit_blame_link("blame", NULL, NULL, ctx.qry.head,
rev, path);
}
html(")\n");
if (is_binary)
print_binary_buffer(buf, size);
else
print_text_buffer(filename, buf, size);
free(buf);
return true;
}
static int only_child_cb(const struct object_id *oid, struct strbuf *base,
const char *pathname, unsigned mode, void *data)
{
struct only_child *child = data;
if (++child->count > 1)
return -1;
if (!S_ISDIR(mode)) {
child->count = 2;
return -1;
}
child->name = xstrdup(pathname);
oidcpy(&child->oid, oid);
strbuf_addf(child->path, "/%s", pathname);
return 0;
}
static void print_dir_chain(const struct object_id *oid, char *name,
char *rev, struct strbuf *fullpath)
{
size_t initial_length = fullpath->len;
struct tree *tree;
struct only_child child = {
.path = fullpath,
.count = 1,
};
struct pathspec paths = {
.nr = 0
};
oidcpy(&child.oid, oid);
while (child.count == 1) {
cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, rev,
fullpath->buf);
tree = lookup_tree(the_repository, &child.oid);
if (!tree)
return;
free(child.name);
child.name = NULL;
child.count = 0;
read_tree(the_repository, tree, &paths, only_child_cb, &child);
if (child.count != 1)
break;
html(" / ");
name = child.name;
}
strbuf_setlen(fullpath, initial_length);
}
static void print_ls_row(const struct object_id *oid, const char *pathname,
unsigned mode, struct walk_tree_context *walk)
{
char *name;
struct strbuf fullpath = STRBUF_INIT;
struct strbuf linkpath = STRBUF_INIT;
struct strbuf class = STRBUF_INIT;
enum object_type type;
unsigned long size = 0;
char *buf;
name = xstrdup(pathname);
strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "",
ctx.qry.path ? "/" : "", name);
if (!S_ISGITLINK(mode)) {
type = odb_read_object_info(the_repository->objects, oid, &size);
if (type == OBJ_BAD) {
// The name comes from the tree, so it can hold
// anything a commit was allowed to record.
html("| Bad object: ");
html_txt(name);
htmlf(" %s |
", oid_to_hex(oid));
goto cleanup;
}
}
html("| ");
cgit_print_filemode(mode);
html(" | ");
if (S_ISGITLINK(mode)) {
cgit_submodule_link("ls-mod", fullpath.buf, oid_to_hex(oid));
} else if (S_ISDIR(mode)) {
print_dir_chain(oid, name, walk->rev, &fullpath);
} else {
char *ext = strrchr(name, '.');
strbuf_addstr(&class, "ls-blob");
if (ext)
strbuf_addf(&class, " %s", ext + 1);
cgit_tree_link(name, NULL, class.buf, ctx.qry.head,
walk->rev, fullpath.buf);
}
if (S_ISLNK(mode)) {
html(" -> ");
buf = odb_read_object(the_repository->objects, oid, &type, &size);
if (!buf) {
htmlf("Error reading object: %s", oid_to_hex(oid));
goto cleanup;
}
strbuf_addbuf(&linkpath, &fullpath);
strbuf_addf(&linkpath, "/../%s", buf);
strbuf_normalize_path(&linkpath);
cgit_tree_link(buf, NULL, class.buf, ctx.qry.head,
walk->rev, linkpath.buf);
free(buf);
strbuf_release(&linkpath);
}
htmlf(" | %lu | ", size);
html("");
cgit_log_link("log", NULL, "button", ctx.qry.head,
walk->rev, fullpath.buf, 0, NULL, NULL,
ctx.qry.showmsg, 0);
if (ctx.repo->enable_stats)
cgit_stats_link("stats", NULL, "button", ctx.qry.head,
fullpath.buf);
if (!S_ISGITLINK(mode))
cgit_plain_link("plain", NULL, "button", ctx.qry.head,
walk->rev, fullpath.buf);
if (!S_ISDIR(mode) && ctx.repo->enable_blame)
cgit_blame_link("blame", NULL, "button", ctx.qry.head,
walk->rev, fullpath.buf);
html(" |
\n");
cleanup:
free(name);
strbuf_release(&fullpath);
strbuf_release(&class);
}
static int ls_item(const struct object_id *oid, struct strbuf *base,
const char *pathname, unsigned mode, void *data)
{
struct walk_tree_context *walk = data;
// With folders grouped first, a row cannot go out as it arrives,
// because git hands the level over in name order.
if (ctx.cfg.enable_tree_group_dirs) {
struct ls_entry *entry;
ALLOC_GROW(walk->entries, walk->entries_nr + 1,
walk->entries_alloc);
entry = &walk->entries[walk->entries_nr++];
oidcpy(&entry->oid, oid);
entry->name = xstrdup(pathname);
entry->mode = mode;
return 0;
}
print_ls_row(oid, pathname, mode, walk);
return 0;
}
static void ls_flush(struct walk_tree_context *walk)
{
size_t i;
for (i = 0; i < walk->entries_nr; i++) {
struct ls_entry *entry = &walk->entries[i];
if (S_ISDIR(entry->mode))
print_ls_row(&entry->oid, entry->name, entry->mode, walk);
}
for (i = 0; i < walk->entries_nr; i++) {
struct ls_entry *entry = &walk->entries[i];
if (!S_ISDIR(entry->mode))
print_ls_row(&entry->oid, entry->name, entry->mode, walk);
}
for (i = 0; i < walk->entries_nr; i++)
free(walk->entries[i].name);
free(walk->entries);
walk->entries = NULL;
walk->entries_nr = walk->entries_alloc = 0;
}
static void ls_head(void)
{
cgit_print_layout_start();
html("\n");
html("");
html("| Mode | ");
html("Name | ");
html("Size | ");
html(" | ");
html("
\n");
}
static void ls_tail(void)
{
html("
\n");
cgit_print_layout_end();
}
static void ls_tree(const struct object_id *oid, const char *path,
struct walk_tree_context *walk)
{
struct tree *tree;
struct pathspec paths = {
.nr = 0
};
tree = parse_tree_indirect(oid);
if (!tree) {
cgit_print_error_page(404, "Not found",
"Not a tree object: %s", oid_to_hex(oid));
return;
}
ls_head();
read_tree(the_repository, tree, &paths, ls_item, walk);
ls_flush(walk);
ls_tail();
}
static int walk_tree(const struct object_id *oid, struct strbuf *base,
const char *pathname, unsigned mode, void *data)
{
struct walk_tree_context *walk = data;
if (walk->state == WALK_LOOKING) {
struct strbuf fullpath = STRBUF_INIT;
strbuf_addbuf(&fullpath, base);
strbuf_addstr(&fullpath, pathname);
if (strcmp(walk->match_path, fullpath.buf))
return READ_TREE_RECURSIVE;
if (S_ISDIR(mode)) {
walk->state = WALK_LISTING;
cgit_set_title_from_path(fullpath.buf);
strbuf_release(&fullpath);
ls_head();
return READ_TREE_RECURSIVE;
} else {
bool shown = print_object(oid, fullpath.buf,
pathname, walk->rev);
walk->state = shown ? WALK_BLOB_SHOWN : WALK_ERROR_SHOWN;
strbuf_release(&fullpath);
return 0;
}
}
ls_item(oid, base, pathname, mode, walk);
return 0;
}
// Either argument may be null, in which case the head of the current query
// stands in for the revision and the listing starts at the root of the tree.
void cgit_print_tree(const char *rev, char *path)
{
struct object_id oid;
struct commit *commit;
int path_len = path ? strlen(path) : 0;
// nowildcard_len matches len so git treats the path as literal rather
// than as a glob.
struct pathspec_item path_items = {
.match = path,
.len = path_len,
.nowildcard_len = path_len
};
struct pathspec paths = {
.nr = path ? 1 : 0,
.items = &path_items
};
struct walk_tree_context walk = {
.match_path = path,
.state = WALK_LOOKING
};
if (!rev)
rev = ctx.qry.head;
if (repo_get_oid(the_repository, rev, &oid)) {
cgit_print_error_page(404, "Not found",
"Invalid revision name: %s", rev);
return;
}
commit = lookup_commit_reference(the_repository, &oid);
if (!commit || repo_parse_commit(the_repository, commit)) {
cgit_print_error_page(404, "Not found",
"Invalid commit reference: %s", rev);
return;
}
walk.rev = xstrdup(rev);
if (path == NULL) {
ls_tree(get_commit_tree_oid(commit), NULL, &walk);
goto cleanup;
}
read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
&paths, walk_tree, &walk);
if (walk.state == WALK_LISTING) {
ls_flush(&walk);
ls_tail();
} else if (walk.state == WALK_BLOB_SHOWN)
cgit_print_layout_end();
else if (walk.state == WALK_LOOKING)
cgit_print_error_page(404, "Not found", "Path not found");
// WALK_ERROR_SHOWN is left alone, since the error page print_object
// put out is already complete.
cleanup:
free(walk.rev);
}