From 0d169ada2ba81210ab1191a5f2212662e90db77e Mon Sep 17 00:00:00 2001 From: Lars Hjemli Date: Sat, 9 Dec 2006 15:18:17 +0100 Subject: Import cgit prototype from git tree This enables basic cgit functionality, using libgit.a and xdiff/lib.a from git + a custom "git.h" + openssl for sha1 routines. Signed-off-by: Lars Hjemli diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1470c0a --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ + +INSTALL_BIN = /var/www/htdocs/cgit.cgi +INSTALL_CSS = /var/www/htdocs/cgit.css + +EXTLIBS = ../git/libgit.a ../git/xdiff/lib.a -lz -lcrypto +OBJECTS = cgit.o config.o html.o + +all: cgit + +install: all + install cgit $(INSTALL_BIN) + install cgit.css $(INSTALL_CSS) + +clean: + rm -f cgit *.o + +cgit: $(OBJECTS) + $(CC) -o cgit $(OBJECTS) $(EXTLIBS) + +cgit.o: cgit.h git.h config.o html.o + +config.o: cgit.h git.h html.c + +html.o: cgit.h git.h html.c diff --git a/cgit.c b/cgit.c new file mode 100644 index 0000000..4c14f77 --- /dev/null +++ b/cgit.c @@ -0,0 +1,410 @@ +#include "cgit.h" + +static const char cgit_doctype[] = +"\n"; + +static const char cgit_error[] = +"
%s
"; + +static const char cgit_lib_error[] = +"
%s: %s
"; + + +char *cgit_root = "/var/git"; +char *cgit_root_title = "Git repository browser"; +char *cgit_css = "/cgit.css"; +char *cgit_logo = "/git-logo.png"; +char *cgit_logo_link = "http://www.kernel.org/pub/software/scm/git/docs/"; +char *cgit_virtual_root = NULL; + +char *cgit_repo_name = NULL; +char *cgit_repo_desc = NULL; +char *cgit_repo_owner = NULL; + +char *cgit_query_repo = NULL; +char *cgit_query_page = NULL; +char *cgit_query_head = NULL; + +int cgit_parse_query(char *txt, configfn fn) +{ + char *t = txt, *value = NULL, c; + + if (!txt) + return 0; + + while((c=*t) != '\0') { + if (c=='=') { + *t = '\0'; + value = t+1; + } else if (c=='&') { + *t = '\0'; + (*fn)(txt, value); + txt = t+1; + value = NULL; + } + t++; + } + if (t!=txt) + (*fn)(txt, value); + return 0; +} + +void cgit_global_config_cb(const char *name, const char *value) +{ + if (!strcmp(name, "root")) + cgit_root = xstrdup(value); + else if (!strcmp(name, "root-title")) + cgit_root_title = xstrdup(value); + else if (!strcmp(name, "css")) + cgit_css = xstrdup(value); + else if (!strcmp(name, "logo")) + cgit_logo = xstrdup(value); + else if (!strcmp(name, "logo-link")) + cgit_logo_link = xstrdup(value); + else if (!strcmp(name, "virtual-root")) + cgit_virtual_root = xstrdup(value); +} + +void cgit_repo_config_cb(const char *name, const char *value) +{ + if (!strcmp(name, "name")) + cgit_repo_name = xstrdup(value); + else if (!strcmp(name, "desc")) + cgit_repo_desc = xstrdup(value); + else if (!strcmp(name, "owner")) + cgit_repo_owner = xstrdup(value); +} + +void cgit_querystring_cb(const char *name, const char *value) +{ + if (!strcmp(name,"r")) + cgit_query_repo = xstrdup(value); + else if (!strcmp(name, "p")) + cgit_query_page = xstrdup(value); + else if (!strcmp(name, "h")) + cgit_query_head = xstrdup(value); +} + +char *cgit_repourl(const char *reponame) +{ + if (cgit_virtual_root) { + return fmt("%s/%s/", cgit_virtual_root, reponame); + } else { + return fmt("?r=%s", reponame); + } +} + +char *cgit_pageurl(const char *reponame, const char *pagename, + const char *query) +{ + if (cgit_virtual_root) { + return fmt("%s/%s/%s/?%s", cgit_virtual_root, reponame, + pagename, query); + } else { + return fmt("?r=%s&p=%s&%s", reponame, pagename, query); + } +} + +static int cgit_print_branch_cb(const char *refname, const unsigned char *sha1, + int flags, void *cb_data) +{ + struct commit *commit; + char buf[256], *url; + + commit = lookup_commit(sha1); + if (commit && !parse_commit(commit)){ + html(""); + url = cgit_pageurl(cgit_query_repo, "log", + fmt("h=%s", refname)); + html_link_open(url, NULL, NULL); + strncpy(buf, refname, sizeof(buf)); + html_txt(buf); + html_link_close(); + html(""); + pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, buf, + sizeof(buf), 0, NULL, NULL, 0); + html_txt(buf); + html("\n"); + } else { + html(""); + html_txt(buf); + html(""); + htmlf("*** bad ref %s", sha1_to_hex(sha1)); + html("\n"); + } + return 0; +} + +static void cgit_print_docstart(char *title) +{ + html("Content-Type: text/html; charset=utf-8\n"); + html("\n"); + html(cgit_doctype); + html("\n"); + html("\n"); + html(""); + html_txt(title); + html("\n"); + html("\n"); + html("\n"); + html("\n"); +} + +static void cgit_print_docend() +{ + html("\n\n"); +} + +static void cgit_print_pageheader(char *title) +{ + html(""); +} + +static void cgit_print_repolist() +{ + DIR *d; + struct dirent *de; + struct stat st; + char *name; + + cgit_print_docstart(cgit_root_title); + cgit_print_pageheader(cgit_root_title); + + if (!(d = opendir("."))) { + htmlf(cgit_lib_error, "Unable to scan repository directory", + strerror(errno)); + cgit_print_docend(); + return; + } + + html("

Repositories

\n"); + html(""); + html("\n"); + while ((de = readdir(d)) != NULL) { + if (de->d_name[0] == '.') + continue; + if (stat(de->d_name, &st) < 0) + continue; + if (!S_ISDIR(st.st_mode)) + continue; + + cgit_repo_name = cgit_repo_desc = cgit_repo_owner = NULL; + name = fmt("%s/.git/info/cgit", de->d_name); + if (cgit_read_config(name, cgit_repo_config_cb)) + continue; + + html("\n"); + } + closedir(d); + html("
NameDescriptionOwner
"); + html_link_open(cgit_repourl(de->d_name), NULL, NULL); + html_txt(cgit_repo_name); + html_link_close(); + html(""); + html_txt(cgit_repo_desc); + html(""); + html_txt(cgit_repo_owner); + html("
"); + cgit_print_docend(); +} + +static void cgit_print_branches() +{ + html(""); + html("\n"); + for_each_branch_ref(cgit_print_branch_cb, NULL); + html("
Branch nameHead commit
"); +} + +static int get_one_line(char *txt) +{ + char *t; + + for(t=txt; *t != '\n' && t != '\0'; t++) + ; + *t = '\0'; + return t-txt-1; +} + +static void cgit_print_commit_shortlog(struct commit *commit) +{ + char *h, *t, *p; + char *tree = NULL, *author = NULL, *subject = NULL; + int len; + time_t sec; + struct tm *time; + char buf[32]; + + h = t = commit->buffer; + + if (strncmp(h, "tree ", 5)) + die("Bad commit format: %s", + sha1_to_hex(commit->object.sha1)); + + len = get_one_line(h); + tree = h+5; + h += len + 2; + + while (!strncmp(h, "parent ", 7)) + h += get_one_line(h) + 2; + + if (!strncmp(h, "author ", 7)) { + author = h+7; + h += get_one_line(h) + 2; + t = author; + while(t!=h && *t!='<') + t++; + *t='\0'; + p = t; + while(--t!=author && *t==' ') + *t='\0'; + while(++p!=h && *p!='>') + ; + while(++p!=h && !isdigit(*p)) + ; + + t = p; + while(++p && isdigit(*p)) + ; + *p = '\0'; + sec = atoi(t); + time = gmtime(&sec); + } + + while((len = get_one_line(h)) > 0) + h += len+2; + + h++; + len = get_one_line(h); + + subject = h; + + html(""); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", time); + html_txt(buf); + html(""); + char *qry = fmt("h=%s", sha1_to_hex(commit->object.sha1)); + char *url = cgit_pageurl(cgit_query_repo, "view", qry); + html_link_open(url, NULL, NULL); + html_txt(subject); + html_link_close(); + html(""); + html_txt(author); + html("\n"); +} + +static void cgit_print_log(const char *tip, int ofs, int cnt) +{ + struct rev_info rev; + struct commit *commit; + const char *argv[2] = {NULL, tip}; + int n = 0; + + init_revisions(&rev, NULL); + rev.abbrev = DEFAULT_ABBREV; + rev.commit_format = CMIT_FMT_DEFAULT; + rev.verbose_header = 1; + rev.show_root_diff = 0; + setup_revisions(2, argv, &rev, NULL); + prepare_revision_walk(&rev); + + html("

Log

"); + html(""); + html("\n"); + while ((commit = get_revision(&rev)) != NULL && n++ < 100) { + cgit_print_commit_shortlog(commit); + free(commit->buffer); + commit->buffer = NULL; + free_commit_list(commit->parents); + commit->parents = NULL; + } + html("
DateMessageAuthor
\n"); +} + +static void cgit_print_repo_summary() +{ + html("

"); + html_txt("Repo summary page"); + html("

"); + cgit_print_branches(); +} + +static void cgit_print_object(char *hex) +{ + unsigned char sha1[20]; + //struct object *object; + char type[20]; + unsigned char *buf; + unsigned long size; + + if (get_sha1_hex(hex, sha1)){ + htmlf(cgit_error, "Bad hex value"); + return; + } + + if (sha1_object_info(sha1, type, NULL)){ + htmlf(cgit_error, "Bad object name"); + return; + } + + buf = read_sha1_file(sha1, type, &size); + if (!buf) { + htmlf(cgit_error, "Error reading object"); + return; + } + + buf[size] = '\0'; + html("

Object view

"); + htmlf("sha1=%s
type=%s
size=%i
", hex, type, size); + html("
");
+	html_txt(buf);
+	html("
"); +} + +static void cgit_print_repo_page() +{ + if (chdir(cgit_query_repo) || + cgit_read_config(".git/info/cgit", cgit_repo_config_cb)) { + char *title = fmt("%s - %s", cgit_root_title, "Bad request"); + cgit_print_docstart(title); + cgit_print_pageheader(title); + htmlf(cgit_lib_error, "Unable to scan repository", + strerror(errno)); + cgit_print_docend(); + return; + } + + char *title = fmt("%s - %s", cgit_repo_name, cgit_repo_desc); + cgit_print_docstart(title); + cgit_print_pageheader(title); + if (!cgit_query_page) + cgit_print_repo_summary(); + else if (!strcmp(cgit_query_page, "log")) { + cgit_print_log(cgit_query_head, 0, 100); + } else if (!strcmp(cgit_query_page, "view")) { + cgit_print_object(cgit_query_head); + } + cgit_print_docend(); +} + +int main(int argc, const char **argv) +{ + if (cgit_read_config("/etc/cgitrc", cgit_global_config_cb)) + die("Error reading config: %d %s", errno, strerror(errno)); + + chdir(cgit_root); + cgit_parse_query(getenv("QUERY_STRING"), cgit_querystring_cb); + if (cgit_query_repo) + cgit_print_repo_page(); + else + cgit_print_repolist(); + return 0; +} diff --git a/cgit.css b/cgit.css new file mode 100644 index 0000000..3ed0c22 --- /dev/null +++ b/cgit.css @@ -0,0 +1,63 @@ +body { + font-family: arial; + font-size: normal; + background: white; + padding: 0em; + margin: 0.5em; +} + + +h2 { + font-size: normal; + font-weight: bold; + margin-bottom: 0.1em; +} + + +table.list { + border: solid 1px black; + border-collapse: collapse; + border: solid 1px #aaa; +} + +table.list th { + text-align: left; + font-weight: bold; + background: #ddd; + border-bottom: solid 1px #aaa; + padding: 0.1em 0.5em 0.1em; + vertical-align: baseline; +} +table.list td { + border: none; + padding: 0.1em 0.5em; + background: white; +} + +img { + border: none; +} + + +div#header { + background-color: #ddd; + padding: 0.25em 0.25em 0.25em 0.5em; + font-size: 150%; + font-weight: bold; + border: solid 1px #aaa; + vertical-align: middle; +} + +div#header img#logo { + float: right; +} + +div#content { + margin: 0.5em 0.5em; +} + +div.error { + color: red; + font-weight: bold; + margin: 1em 2em; +} \ No newline at end of file diff --git a/cgit.h b/cgit.h new file mode 100644 index 0000000..19f7ba7 --- /dev/null +++ b/cgit.h @@ -0,0 +1,21 @@ +#ifndef CGIT_H +#define CGIT_H + +#include "git.h" +#include + +extern char *fmt(const char *format,...); + +extern void html(const char *txt); +extern void htmlf(const char *format,...); +extern void html_txt(char *txt); +extern void html_attr(char *txt); + +extern void html_link_open(char *url, char *title, char *class); +extern void html_link_close(void); + +typedef void (*configfn)(const char *name, const char *value); + +extern int cgit_read_config(const char *filename, configfn fn); + +#endif /* CGIT_H */ diff --git a/config.c b/config.c new file mode 100644 index 0000000..858ab69 --- /dev/null +++ b/config.c @@ -0,0 +1,73 @@ +#include "cgit.h" + +int next_char(FILE *f) +{ + int c = fgetc(f); + if (c=='\r') { + c = fgetc(f); + if (c!='\n') { + ungetc(c, f); + c = '\r'; + } + } + return c; +} + +void skip_line(FILE *f) +{ + int c; + + while((c=next_char(f)) && c!='\n' && c!=EOF) + ; +} + +int read_config_line(FILE *f, char *line, const char **value, int bufsize) +{ + int i = 0, isname = 0; + + *value = NULL; + while(i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +static inline char* xstrdup(const char *str) +{ + char *ret = strdup(str); + if (!ret) + die("Out of memory, strdup failed"); + return ret; +} + +static inline void *xmalloc(size_t size) +{ + void *ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) + die("Out of memory, malloc failed"); +#ifdef XMALLOC_POISON + memset(ret, 0xA5, size); +#endif + return ret; +} + +static inline void *xrealloc(void *ptr, size_t size) +{ + void *ret = realloc(ptr, size); + if (!ret && !size) + ret = realloc(ptr, 1); + if (!ret) + die("Out of memory, realloc failed"); + return ret; +} + +static inline void *xcalloc(size_t nmemb, size_t size) +{ + void *ret = calloc(nmemb, size); + if (!ret && (!nmemb || !size)) + ret = calloc(1, 1); + if (!ret) + die("Out of memory, calloc failed"); + return ret; +} + +static inline ssize_t xread(int fd, void *buf, size_t len) +{ + ssize_t nr; + while (1) { + nr = read(fd, buf, len); + if ((nr < 0) && (errno == EAGAIN || errno == EINTR)) + continue; + return nr; + } +} + +static inline ssize_t xwrite(int fd, const void *buf, size_t len) +{ + ssize_t nr; + while (1) { + nr = write(fd, buf, len); + if ((nr < 0) && (errno == EAGAIN || errno == EINTR)) + continue; + return nr; + } +} + + + + +/* + * from git:cache.h + */ + + +/* Convert to/from hex/sha1 representation */ +#define MINIMUM_ABBREV 4 +#define DEFAULT_ABBREV 7 + + +extern void * read_sha1_file(const unsigned char *sha1, char *type, unsigned long *size); + + + + +/* + * from git:object.h + */ + +struct object_list { + struct object *item; + struct object_list *next; +}; + +struct object_refs { + unsigned count; + struct object *base; + struct object *ref[FLEX_ARRAY]; /* more */ +}; + +struct object_array { + unsigned int nr; + unsigned int alloc; + struct object_array_entry { + struct object *item; + const char *name; + } *objects; +}; + +#define TYPE_BITS 3 +#define FLAG_BITS 27 + +/* + * The object type is stored in 3 bits. + */ +struct object { + unsigned parsed : 1; + unsigned used : 1; + unsigned type : TYPE_BITS; + unsigned flags : FLAG_BITS; + unsigned char sha1[20]; +}; + + +/* + * from git:tree.h + */ + +struct tree { + struct object object; + void *buffer; + unsigned long size; +}; + + + + +/* from git:commit.h */ + +struct commit_list { + struct commit *item; + struct commit_list *next; +}; + +struct commit { + struct object object; + void *util; + unsigned long date; + struct commit_list *parents; + struct tree *tree; + char *buffer; +}; + + +/* Commit formats */ +enum cmit_fmt { + CMIT_FMT_RAW, + CMIT_FMT_MEDIUM, + CMIT_FMT_DEFAULT = CMIT_FMT_MEDIUM, + CMIT_FMT_SHORT, + CMIT_FMT_FULL, + CMIT_FMT_FULLER, + CMIT_FMT_ONELINE, + CMIT_FMT_EMAIL, + + CMIT_FMT_UNSPECIFIED, +}; + + + +struct commit *lookup_commit(const unsigned char *sha1); +struct commit *lookup_commit_reference(const unsigned char *sha1); +struct commit *lookup_commit_reference_gently(const unsigned char *sha1, + int quiet); + +typedef void (*topo_sort_set_fn_t)(struct commit*, void *data); +typedef void* (*topo_sort_get_fn_t)(struct commit*); + + + + +/* + * from git:diff.h + */ + + +struct rev_info; +struct diff_options; +struct diff_queue_struct; + +typedef void (*change_fn_t)(struct diff_options *options, + unsigned old_mode, unsigned new_mode, + const unsigned char *old_sha1, + const unsigned char *new_sha1, + const char *base, const char *path); + +typedef void (*add_remove_fn_t)(struct diff_options *options, + int addremove, unsigned mode, + const unsigned char *sha1, + const char *base, const char *path); + +typedef void (*diff_format_fn_t)(struct diff_queue_struct *q, + struct diff_options *options, void *data); + +#define DIFF_FORMAT_RAW 0x0001 +#define DIFF_FORMAT_DIFFSTAT 0x0002 +#define DIFF_FORMAT_NUMSTAT 0x0004 +#define DIFF_FORMAT_SUMMARY 0x0008 +#define DIFF_FORMAT_PATCH 0x0010 + +/* These override all above */ +#define DIFF_FORMAT_NAME 0x0100 +#define DIFF_FORMAT_NAME_STATUS 0x0200 +#define DIFF_FORMAT_CHECKDIFF 0x0400 + +/* Same as output_format = 0 but we know that -s flag was given + * and we should not give default value to output_format. + */ +#define DIFF_FORMAT_NO_OUTPUT 0x0800 + +#define DIFF_FORMAT_CALLBACK 0x1000 + +struct diff_options { + const char *filter; + const char *orderfile; + const char *pickaxe; + const char *single_follow; + unsigned recursive:1, + tree_in_recursive:1, + binary:1, + text:1, + full_index:1, + silent_on_remove:1, + find_copies_harder:1, + color_diff:1, + color_diff_words:1; + int context; + int break_opt; + int detect_rename; + int line_termination; + int output_format; + int pickaxe_opts; + int rename_score; + int reverse_diff; + int rename_limit; + int setup; + int abbrev; + const char *msg_sep; + const char *stat_sep; + long xdl_opts; + + int stat_width; + int stat_name_width; + + int nr_paths; + const char **paths; + int *pathlens; + change_fn_t change; + add_remove_fn_t add_remove; + diff_format_fn_t format_callback; + void *format_callback_data; +}; + +enum color_diff { + DIFF_RESET = 0, + DIFF_PLAIN = 1, + DIFF_METAINFO = 2, + DIFF_FRAGINFO = 3, + DIFF_FILE_OLD = 4, + DIFF_FILE_NEW = 5, + DIFF_COMMIT = 6, + DIFF_WHITESPACE = 7, +}; + + + + + + + +/* + * from git:revision.h + */ + +struct rev_info; +struct log_info; + +typedef void (prune_fn_t)(struct rev_info *revs, struct commit *commit); + +struct rev_info { + /* Starting list */ + struct commit_list *commits; + struct object_array pending; + + /* Basic information */ + const char *prefix; + void *prune_data; + prune_fn_t *prune_fn; + + /* Traversal flags */ + unsigned int dense:1, + no_merges:1, + no_walk:1, + remove_empty_trees:1, + simplify_history:1, + lifo:1, + topo_order:1, + tag_objects:1, + tree_objects:1, + blob_objects:1, + edge_hint:1, + limited:1, + unpacked:1, /* see also ignore_packed below */ + boundary:1, + parents:1; + + /* Diff flags */ + unsigned int diff:1, + full_diff:1, + show_root_diff:1, + no_commit_id:1, + verbose_header:1, + ignore_merges:1, + combine_merges:1, + dense_combined_merges:1, + always_show_header:1; + + /* Format info */ + unsigned int shown_one:1, + abbrev_commit:1, + relative_date:1; + + const char **ignore_packed; /* pretend objects in these are unpacked */ + int num_ignore_packed; + + unsigned int abbrev; + enum cmit_fmt commit_format; + struct log_info *loginfo; + int nr, total; + const char *mime_boundary; + const char *message_id; + const char *ref_message_id; + const char *add_signoff; + const char *extra_headers; + + /* Filter by commit log message */ + struct grep_opt *grep_filter; + + /* special limits */ + int max_count; + unsigned long max_age; + unsigned long min_age; + + /* diff info for patches and for paths limiting */ + struct diff_options diffopt; + struct diff_options pruning; + + topo_sort_set_fn_t topo_setter; + topo_sort_get_fn_t topo_getter; +}; + + +extern struct commit *get_revision(struct rev_info *revs); + + + + +#endif /* GIT_H */ diff --git a/html.c b/html.c new file mode 100644 index 0000000..5780dc1 --- /dev/null +++ b/html.c @@ -0,0 +1,100 @@ +#include "cgit.h" + +char *fmt(const char *format, ...) +{ + static char buf[8][1024]; + static int bufidx; + int len; + va_list args; + + bufidx++; + bufidx &= 7; + + va_start(args, format); + len = vsnprintf(buf[bufidx], sizeof(buf[bufidx]), format, args); + va_end(args); + if (len>sizeof(buf[bufidx])) + die("[html.c] string truncated: %s", format); + return buf[bufidx]; +} + +void html(const char *txt) +{ + fputs(txt, stdout); +} + +void htmlf(const char *format, ...) +{ + va_list args; + + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +void html_txt(char *txt) +{ + char *t = txt; + while(*t){ + int c = *t; + if (c=='<' || c=='>' || c=='&') { + *t = '\0'; + html(txt); + *t = c; + if (c=='>') + html(">"); + else if (c=='<') + html("<"); + else if (c=='&') + html("&"); + txt = t+1; + } + t++; + } + if (t!=txt) + html(txt); +} + + +void html_attr(char *txt) +{ + char *t = txt; + while(*t){ + int c = *t; + if (c=='<' || c=='>' || c=='\'') { + *t = '\0'; + html(txt); + *t = c; + if (c=='>') + html(">"); + else if (c=='<') + html("<"); + else if (c=='\'') + html(""e;"); + txt = t+1; + } + t++; + } + if (t!=txt) + html(txt); +} + +void html_link_open(char *url, char *title, char *class) +{ + html(""); +} + +void html_link_close(void) +{ + html(""); +} -- cgit v0.10.1