/*
* 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 "shared.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
// How many single-child directories a listing row follows into one path.
#define MAX_DIR_CHAIN_LEVELS 15
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 {
const 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;
};
static void print_linenumbers(const char *buf, size_t size)
{
size_t lines = 0, idx;
if (size) {
lines = 1;
// The newline that ends the last line must not open a line of
// its own, so the final byte is left out of the count.
for (idx = 0; idx < size - 1; idx++)
if (buf[idx] == '\n')
lines++;
}
cgit_print_line_anchors(1, lines);
}
static void print_text_buffer(const char *filename, char *buf, size_t size)
{
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 {
const char *ext = strrchr(name, '.');
strbuf_addstr(&class, "ls-blob");
// The extension becomes a class a stylesheet can pick files out
// by, prefixed so that a name such as x.button cannot land on a
// class of the theme's own.
if (ext && ext[1] && strspn(ext + 1, "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") == strlen(ext + 1))
strbuf_addf(&class, " ext-%s", ext + 1);
cgit_tree_link(name, NULL, class.buf, ctx.qry.head, walk->rev, fullpath.buf);
}
// A target longer than any path is no link, and reading it would pull
// a blob of any size into memory.
if (S_ISLNK(mode) && size <= PATH_MAX) {
html(" -> ");
buf = odb_read_object(the_repository->objects, oid, &type, &size);
// An unreadable target stands in for the link, and the rest of
// the row still goes out so the table stays whole.
if (!buf) {
htmlf("Unable to read object %s", oid_to_hex(oid));
} else {
strbuf_addbuf(&linkpath, &fullpath);
strbuf_addf(&linkpath, "/../%s", buf);
// A target climbing above the tree root, or one given
// from the root of the filesystem, has nothing in the
// tree to link to.
if (buf[0] == '/' || strbuf_normalize_path(&linkpath))
html_txt(buf);
else
cgit_tree_link(buf, NULL, class.buf, ctx.qry.head, walk->rev,
linkpath.buf);
free(buf);
strbuf_release(&linkpath);
}
}
if (S_ISGITLINK(mode))
html(" | - | ");
else
htmlf("%zu | ", 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->max_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) && !S_ISGITLINK(mode) && ctx.repo->enable_blame)
cgit_blame_link("blame", NULL, "button", ctx.qry.head, walk->rev, fullpath.buf);
html(" |
\n");
cleanup:
strbuf_release(&fullpath);
strbuf_release(&class);
}
static int ls_item(const struct object_id *oid, struct strbuf *base UNUSED,
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("