/* * CvsGraph graphical representation generator of brances and revisions * of a file in cvs/rcs. * * Copyright (C) 2001,2002,2003,2004,2005,2006 B. Stultiens * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "config.h" #include #include #include #include #include #include #include #include #ifdef HAVE_SYS_WAIT_H # include #endif #include #include #include #include #include #include #include #ifdef HAVE_GETOPT_H # include #endif #include #include #include "cvsgraph.h" #include "utils.h" #include "readconf.h" #include "rcs.h" #if !defined(HAVE_IMAGE_GIF) && !defined(HAVE_IMAGE_PNG) && !defined(HAVE_IMAGE_JPEG) # error No image output format available. Check libgd #endif /*#define DEBUG 1*/ /*#define NOGDFILL 1*/ /*#define DEBUG_IMAGEMAP 1*/ #define LOOPSAFEGUARD 10000 /* Max itterations in possible infinite loops */ #ifndef MAX # define MAX(a,b) ((a) > (b) ? (a) : (b)) #endif #ifndef MIN # define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif #define ALIGN_HL 0x00 #define ALIGN_HC 0x01 #define ALIGN_HR 0x02 #define ALIGN_HX 0x0f #define ALIGN_VT 0x00 #define ALIGN_VC 0x10 #define ALIGN_VB 0x20 #define ALIGN_VX 0xf0 #ifndef M_PI /* math.h should have defined this */ # define M_PI 3.14159265358979323846 #endif #define ROUND(f) ((f >= 0.0)?((int)(f + 0.5)):((int)(f - 0.5))) #define ARROW_LENGTH 12 /* Default arrow dimensions */ #define ARROW_WIDTH 3 /* ************************************************************************** * Globals ************************************************************************** */ config_t conf; int debuglevel; static color_t white_color = {255, 255, 255, 0, NULL}; static color_t black_color = {0, 0, 0, 0, NULL}; static branch_t *subtree_branch = NULL; /* Set to the (first) subtree branch that we want to show */ static revision_t *subtree_rev = NULL; /* Set to the subtree revision which branches we want to show */ static msg_stack_t *msg_stack = NULL; /* Messages that would otherwise be sent to stderr goto the image */ static int nmsg_stack = 0; /* ************************************************************************** * Forwards ************************************************************************** */ static void zap_string(void); static char *dup_string(void); static void add_string_str(const char *s); static void add_string_ch(int ch); static void add_string_date(const char *d); static void add_string_str_html(const char *s, int maxlen); static void add_string_str_len(const char *s, int maxlen); static void calc_subtree_size(branch_t *b, int *x, int *y, int *w, int *h); /* ************************************************************************** * Debug routines ************************************************************************** */ static void dump_rev(char *p, rev_t *r) { printf("%s", p); if(r) printf("'%s', '%s', %d\n", r->rev, r->branch, r->isbranch); else printf("\n"); } static void dump_id(char *p, char *d) { printf("%s", p); if(d) printf("'%s'\n", d); else printf("\n"); } static void dump_idrev(char *p, idrev_t *t) { printf("%s", p); if(t) { printf("'%s' -> ", t->id); dump_rev("", t->rev); } else printf("\n"); } static void dump_tag(char *p, tag_t *t) { printf("%s", p); if(t) { printf("'%s' -> ", t->tag); dump_rev("", t->rev); } else printf("\n"); } static void dump_delta(char *p, delta_t *d) { int i; printf("%sdelta.rev : ", p); dump_rev("", d->rev); printf("%sdelta.date : %s\n", p, d->date); printf("%sdelta.author: %s\n", p, d->author); printf("%sdelta.state : %s\n", p, d->state); for(i = 0; d->branches && i < d->branches->nrevs; i++) { printf("%sdelta.branch: ", p); dump_rev("", d->branches->revs[i]); } printf("%sdelta.next : ", p); dump_rev("", d->next); printf("\n"); } static void dump_dtext(char *p, dtext_t *d) { printf("%sdtext.rev : ", p); dump_rev("", d->rev); printf("%sdtext.log : %ld bytes\n", p, d->log ? (long) strlen(d->log) : -1); printf("%sdtext.text : %ld bytes\n", p, d->text ? (long) strlen(d->text) : -1); printf("\n"); } static void dump_rcsfile(rcsfile_t *rcs) { int i; printf("root : '%s'\n", rcs->root); printf("module : '%s'\n", rcs->module); printf("file : '%s'\n", rcs->file); dump_rev("head : ", rcs->head); dump_rev("branch : ", rcs->branch); printf("access :\n"); for(i = 0; rcs->access && i < rcs->access->nids; i++) dump_id("\t", rcs->access->ids[i]); printf("tags :\n"); for(i = 0; rcs->tags && i < rcs->tags->ntags; i++) dump_tag("\t", rcs->tags->tags[i]); printf("locks :%s\n", rcs->strict ? " (strict)" : ""); for(i = 0; rcs->locks && i < rcs->locks->nidrevs; i++) dump_idrev("\t", rcs->locks->idrevs[i]); printf("comment: '%s'\n", rcs->comment); printf("expand : '%s'\n", rcs->expand ? rcs->expand : "(default -kv)"); printf("deltas :\n"); for(i = 0; rcs->deltas && i < rcs->deltas->ndeltas; i++) dump_delta("\t", rcs->deltas->deltas[i]); printf("desc : '%s'\n", rcs->desc); printf("dtexts :\n"); for(i = 0; rcs->dtexts && i < rcs->dtexts->ndtexts; i++) dump_dtext("\t", rcs->dtexts->dtexts[i]); fflush(stdout); } /* ************************************************************************** * Error/Warning Message helpers ************************************************************************** */ #define MSGBUFSIZE 256 void stack_msg(int severity, const char *fmt, ...) { va_list va; int i; char *buf = xmalloc(MSGBUFSIZE); switch(severity) { case MSG_WARN: sprintf(buf, "Warning: "); break; case MSG_ERR: sprintf(buf, "Error: "); break; default: sprintf(buf, "Unqualified error: "); break; } i = strlen(buf); assert(i < MSGBUFSIZE); va_start(va, fmt); vsnprintf(buf+i, MSGBUFSIZE-i, fmt, va); va_end(va); if(!msg_stack) msg_stack = xmalloc(sizeof(*msg_stack)); else { msg_stack = xrealloc(msg_stack, (nmsg_stack+1)*sizeof(*msg_stack)); } msg_stack[nmsg_stack].msg = buf; msg_stack[nmsg_stack].severity = severity; nmsg_stack++; } /* ************************************************************************** * Read the rcs file ************************************************************************** */ static rcsfile_t *get_rcsfile(const char *cvsroot, const char *module, const char *file) { char *cmd = NULL; int rv; if(file) { cmd = xmalloc(strlen(cvsroot) + strlen(module) + strlen(file) + 1); sprintf(cmd, "%s%s%s", cvsroot, module, file); if(!(rcsin = fopen(cmd, "rb"))) { perror(cmd); return NULL; } input_file = cmd; } else { rcsin = stdin; input_file = ""; } line_number = 1; rv = rcsparse(); if(file) { fclose(rcsin); xfree(cmd); } if(rv) return NULL; input_file = NULL; if(file) { rcsfile->root = xstrdup(cvsroot); rcsfile->module = xstrdup(module); rcsfile->file = xstrdup(file); } else { rcsfile->root = xstrdup(""); rcsfile->module = xstrdup(""); rcsfile->file = xstrdup(""); } return rcsfile; } /* ************************************************************************** * Sort and find helpers ************************************************************************** */ static int count_dots(const char *s) { int i; for(i = 0; *s; s++) { if(*s == '.') i++; } return i; } static int compare_rev(int bcmp, const rev_t *r1, const rev_t *r2) { int d1, d2; char *c1, *c2; char *v1, *v2; char *s1, *s2; int retval = 0; assert(r1 != NULL); assert(r2 != NULL); if(bcmp) { assert(r1->branch != NULL); assert(r2->branch != NULL); c1 = r1->branch; c2 = r2->branch; } else { assert(r1->rev != NULL); assert(r2->rev != NULL); c1 = r1->rev; c2 = r2->rev; } d1 = count_dots(c1); d2 = count_dots(c2); if(d1 != d2) { return d1 - d2; } s1 = v1 = xstrdup(c1); s2 = v2 = xstrdup(c2); while(1) { char *vc1 = strchr(s1, '.'); char *vc2 = strchr(s2, '.'); if(vc1 && vc2) *vc1 = *vc2 = '\0'; if(*s1 && *s2) { d1 = atoi(s1); d2 = atoi(s2); if(d1 != d2) { retval = d1 - d2; break; } } if(!vc1 || !vc2) break; s1 = vc1 + 1; s2 = vc2 + 1; } xfree(v1); xfree(v2); return retval; } /* ************************************************************************** * Reorganise the rcsfile for the branches * * Basically, we have a list of deltas (i.e. administrative info on * revisions) and a list of delta text (the actual logs and diffs). * The deltas are linked through the 'next' and the 'branches' fields * which describe the tree-structure of revisions. * The reorganisation means that we put each delta and corresponding * delta text in a revision structure and assign it to a specific * branch. This is required because we want to be able to calculate * the bounding boxes of each branch. The revisions expand vertically * and the branches expand horizontally. * The reorganisation is performed in these steps: * 1 - sort deltas and delta text on revision number for quick lookup * 2 - start at the denoted head revision: * * create a branch structure and add this revision * * for each 'branches' in the delta do: * - walk all 'branches' of the delta and recursively goto 2 * with the denoted branch delta as new head * - backlink the newly create sub-branch to the head revision * so that we can draw them recursively * * set head to the 'next' field and goto 2 until no next is * available * 3 - update the administration ************************************************************************** */ static int sort_delta(const void *d1, const void *d2) { return compare_rev(0, (*(delta_t **)d1)->rev, (*(delta_t **)d2)->rev); } static int search_delta(const void *r, const void *d) { return compare_rev(0, (rev_t *)r, (*(delta_t **)d)->rev); } static delta_t *find_delta(delta_t **dl, int n, rev_t *r) { delta_t **d; if(!n) return NULL; d = bsearch(r, dl, n, sizeof(*dl), search_delta); if(!d) return NULL; return *d; } static int sort_dtext(const void *d1, const void *d2) { return compare_rev(0, (*(dtext_t **)d1)->rev, (*(dtext_t **)d2)->rev); } static int search_dtext(const void *r, const void *d) { return compare_rev(0, (rev_t *)r, (*(dtext_t **)d)->rev); } static dtext_t *find_dtext(dtext_t **dl, int n, rev_t *r) { dtext_t **d; if(!n) return NULL; d = bsearch(r, dl, n, sizeof(*dl), search_dtext); if(!d) return NULL; return *d; } static rev_t *dup_rev(const rev_t *r) { rev_t *t = xmalloc(sizeof(*t)); t->rev = xstrdup(r->rev); t->branch = xstrdup(r->branch); t->isbranch = r->isbranch; return t; } static branch_t *new_branch(delta_t *d, dtext_t *t) { branch_t *b = xmalloc(sizeof(*b)); revision_t *r = xmalloc(sizeof(*r)); r->delta = d; r->dtext = t; r->rev = d->rev; r->branch = b; b->branch = dup_rev(d->rev); b->branch->isbranch = 1; b->nrevs = 1; b->revs = xmalloc(sizeof(b->revs[0])); b->revs[0] = r; return b; } static revision_t *add_to_branch(branch_t *b, delta_t *d, dtext_t *t) { revision_t *r = xmalloc(sizeof(*r)); r->delta = d; r->dtext = t; r->rev = d->rev; r->branch = b; b->revs = xrealloc(b->revs, (b->nrevs+1) * sizeof(b->revs[0])); b->revs[b->nrevs] = r; b->nrevs++; return r; } static int sort_branch_height(const void *b1, const void *b2) { return (*(branch_t **)b1)->nrevs - (*(branch_t **)b2)->nrevs; } static void build_branch(branch_t ***bl, int *nbl, delta_t **sdl, int nsdl, dtext_t **sdt, int nsdt, delta_t *head) { branch_t *b; dtext_t *text; revision_t *currev; assert(head != NULL); if(head->flag) { stack_msg(MSG_ERR, "Circular reference on '%s' in branchpoint\n", head->rev->rev); return; } head->flag++; text = find_dtext(sdt, nsdt, head->rev); /* Create a new branch for this head */ b = new_branch(head, text); *bl = xrealloc(*bl, (*nbl+1)*sizeof((*bl)[0])); (*bl)[*nbl] = b; (*nbl)++; currev = b->revs[0]; while(1) { /* Process all sub-branches */ if(head->branches) { int i; for(i = 0; i < head->branches->nrevs; i++) { delta_t *d = find_delta(sdl, nsdl, head->branches->revs[i]); int btag = *nbl; if(!d) continue; build_branch(bl, nbl, sdl, nsdl, sdt, nsdt, d); /* Set the new branch's origin */ (*bl)[btag]->branchpoint = currev; /* Add branch to this revision */ currev->branches = xrealloc(currev->branches, (currev->nbranches+1)*sizeof(currev->branches[0])); currev->branches[currev->nbranches] = (*bl)[btag]; currev->nbranches++; } if(conf.branch_resort) qsort(currev->branches, currev->nbranches, sizeof(currev->branches[0]), sort_branch_height); } /* Walk through the next list */ if(!head->next) return; head = find_delta(sdl, nsdl, head->next); if(!head) { stack_msg(MSG_ERR, "Next revision (%s) not found in deltalist\n", head->next->rev); return; } if(head->flag) { stack_msg(MSG_ERR, "Circular reference on '%s'\n", head->rev->rev); return; } head->flag++; text = find_dtext(sdt, nsdt, head->rev); currev = add_to_branch(b, head, text); } } int reorganise_branches(rcsfile_t *rcs) { delta_t **sdelta; int nsdelta; dtext_t **sdtext; int nsdtext; delta_t *head; branch_t **bl; int nbl; int i; assert(rcs->deltas != NULL); assert(rcs->head != NULL); /* Make a new list for quick lookup */ nsdelta = rcs->deltas->ndeltas; sdelta = xmalloc(nsdelta * sizeof(sdelta[0])); memcpy(sdelta, rcs->deltas->deltas, nsdelta * sizeof(sdelta[0])); qsort(sdelta, nsdelta, sizeof(sdelta[0]), sort_delta); /* Do the same for the delta text */ if(rcs->dtexts) { nsdtext = rcs->dtexts->ndtexts; sdtext = xmalloc(nsdtext * sizeof(sdtext[0])); memcpy(sdtext, rcs->dtexts->dtexts, nsdtext * sizeof(sdtext[0])); qsort(sdtext, nsdtext, sizeof(sdtext[0]), sort_dtext); } else { nsdtext = 0; sdtext = NULL; } /* Start from the head of the trunk */ head = find_delta(sdelta, nsdelta, rcs->head); if(!head) { stack_msg(MSG_ERR, "Head revision (%s) not found in deltalist\n", rcs->head->rev); return 0; } bl = NULL; nbl = 0; build_branch(&bl, &nbl, sdelta, nsdelta, sdtext, nsdtext, head); /* Reverse the head branch */ for(i = 0; i < bl[0]->nrevs/2; i++) { revision_t *r; r = bl[0]->revs[i]; bl[0]->revs[i] = bl[0]->revs[bl[0]->nrevs-i-1]; bl[0]->revs[bl[0]->nrevs-i-1] = r; } /* Update the branch-number of the head because it was reversed */ xfree(bl[0]->branch->branch); bl[0]->branch->branch = xstrdup(bl[0]->revs[0]->rev->branch); /* Keep the admin */ rcs->branches = bl; rcs->nbranches = nbl; rcs->sdelta = sdelta; rcs->nsdelta = nsdelta; rcs->sdtext = sdtext; rcs->nsdtext = nsdtext; rcs->active = bl[0]; return 1; } /* ************************************************************************** * Assign the symbolic tags to the revisions and branches * * The tags point to revision numbers somewhere in the tree structure * of branches and revisions. First we make a sorted list of all * revisions and then we assign each tag to the proper revision. ************************************************************************** */ static int sort_revision(const void *r1, const void *r2) { return compare_rev(0, (*(revision_t **)r1)->delta->rev, (*(revision_t **)r2)->delta->rev); } static int search_revision(const void *t, const void *r) { return compare_rev(0, (rev_t *)t, (*(revision_t **)r)->delta->rev); } static int sort_branch(const void *b1, const void *b2) { return compare_rev(1, (*(branch_t **)b1)->branch, (*(branch_t **)b2)->branch); } static int search_branch(const void *t, const void *b) { return compare_rev(1, (rev_t *)t, (*(branch_t **)b)->branch); } static char *previous_rev(const char *c) { int dots = count_dots(c); char *cptr; char *r; if(!dots) { stack_msg(MSG_ERR, "FIXME: previous_rev(\"%s\"): Cannot determine parent branch revision\n", c); return xstrdup("1.0"); /* FIXME: don't know what the parent is */ } if(dots & 1) { /* Is is a revision we want the parent of */ r = xstrdup(c); cptr = strrchr(r, '.'); assert(cptr != NULL); if(dots == 1) { stack_msg(MSG_ERR, "FIXME: previous_rev(\"%s\"): Going beyond top-level?\n", c); /* FIXME: What is the parent of 1.1? */ cptr[1] = '\0'; strcat(r, "0"); return r; } /* Here we have a "x.x[.x.x]+" case */ *cptr = '\0'; cptr = strrchr(r, '.'); assert(cptr != NULL); *cptr = '\0'; return r; } /* It is a branch we want the parent of */ r = xstrdup(c); cptr = strrchr(r, '.'); assert(cptr != NULL); *cptr = '\0'; return r; } static char *build_regex(size_t n, regmatch_t *m, const char *ms, int idx) { char *cptr; int i; if(!conf.merge_to.strs[idx]) return NULL; zap_string(); for(cptr = conf.merge_to.strs[idx]; *cptr; cptr++) { if(*cptr == '%') { if(cptr[1] >= '1' && cptr[1] <= '9') { int idx = cptr[1] - '0'; regmatch_t *p = &m[idx]; if(idx < n && !(p->rm_so == -1 || p->rm_so >= p->rm_eo)) { for(i = p->rm_so; i < p->rm_eo; i++) { if(strchr("^$.*+\\[{()", ms[i])) add_string_ch('\\'); add_string_ch(ms[i]); } } cptr++; } else add_string_ch('%'); } else add_string_ch(*cptr); } return dup_string(); } static void find_merges_cvsnt(rcsfile_t *rcs) { int i; if(!conf.merge_cvsnt) return; for(i = 0; i < rcs->nsrev; i++) { revision_t **r; if(!rcs->srev[i]->delta->mergepoint) continue; r = bsearch(rcs->srev[i]->delta->mergepoint->rev, rcs->srev, rcs->nsrev, sizeof(rcs->srev[0]), search_revision); if(!r) continue; rcs->merges = xrealloc(rcs->merges, sizeof(rcs->merges[0]) * (rcs->nmerges+1)); rcs->merges[rcs->nmerges].type = TR_REVISION; rcs->merges[rcs->nmerges].from.rev = *r; rcs->merges[rcs->nmerges].to.rev = rcs->srev[i]; rcs->merges[rcs->nmerges].clr = -1; rcs->nmerges++; (*r)->mergetarget = 1; rcs->srev[i]->mergetarget = 1; } } static void find_merges(rcsfile_t *rcs) { int i, j; int err; int rcflags = REG_EXTENDED | (conf.merge_nocase ? REG_ICASE : 0); regex_t *refrom = NULL; regex_t *reto = NULL; regmatch_t *matchfrom = NULL; if(!conf.merge_from.n || !conf.merge_to.n) return; for(j = 0; j < conf.merge_from.n; j++) { if(!conf.merge_from.strs[0] || !conf.merge_to.strs[0]) continue; refrom = xmalloc(sizeof(*refrom)); reto = xmalloc(sizeof(*reto)); /* Compile the 'from' regex match for merge identification */ err = regcomp(refrom, conf.merge_from.strs[j], rcflags); if(err) { char *msg; i = regerror(err, refrom, NULL, 0); msg = xmalloc(i+1); regerror(err, refrom, msg, i+1); stack_msg(MSG_WARN, "%s", msg); xfree(msg); xfree(refrom); xfree(reto); return; } else matchfrom = xmalloc((refrom->re_nsub+1) * sizeof(*matchfrom)); for(i = 0; i < rcs->tags->ntags; i++) { tag_t *t = rcs->tags->tags[i]; /* Must be revision tags and not detached */ if(t->rev->isbranch || !t->logrev) continue; /* Try to find merge tag matches */ if(!regexec(refrom, t->tag, refrom->re_nsub+1, matchfrom, 0)) { int n; char *to; to = build_regex(refrom->re_nsub+1, matchfrom, t->tag, j); if(to) { err = regcomp(reto, to, rcflags); if(err) { char *msg; i = regerror(err, reto, NULL, 0); msg = xmalloc(i+1); regerror(err, reto, msg, i+1); stack_msg(MSG_WARN, "%s", msg); xfree(msg); } else if(!err) { for(n = 0; n < rcs->tags->ntags; n++) { tag_t *nt = rcs->tags->tags[n]; /* From and To never should match the same tag or belong to a branch */ if(n == i || nt->rev->isbranch || !nt->logrev) continue; if(!regexec(reto, nt->tag, 0, NULL, 0)) { /* Tag matches */ rcs->merges = xrealloc(rcs->merges, sizeof(rcs->merges[0]) * (rcs->nmerges+1)); rcs->merges[rcs->nmerges].type = TR_TAG; rcs->merges[rcs->nmerges].to.tag = nt; rcs->merges[rcs->nmerges].from.tag = t; rcs->merges[rcs->nmerges].clr = j; rcs->nmerges++; if(!conf.tag_ignore_merge) { nt->ignore = 0; t->ignore = 0; } /* We cannot (should not) match multiple times */ if(!conf.merge_findall) break; } } regfree(reto); } xfree(to); } } } if(matchfrom) xfree(matchfrom); if(refrom) { regfree(refrom); xfree(refrom); } if(reto) xfree(reto); refrom = NULL; reto = NULL; matchfrom = NULL; } } static void assign_tags(rcsfile_t *rcs) { int i; int nr; regex_t *regextag = NULL; if(conf.tag_ignore && conf.tag_ignore[0]) { int err; regextag = xmalloc(sizeof(*regextag)); err = regcomp(regextag, conf.tag_ignore, REG_EXTENDED | REG_NOSUB | (conf.tag_nocase ? REG_ICASE : 0)); if(err) { char *msg; i = regerror(err, regextag, NULL, 0); msg = xmalloc(i+1); regerror(err, regextag, msg, i+1); stack_msg(MSG_WARN, "%s", msg); xfree(msg); xfree(regextag); regextag = NULL; } } for(i = nr = 0; i < rcs->nbranches; i++) nr += rcs->branches[i]->nrevs; rcs->srev = xmalloc(nr * sizeof(rcs->srev[0])); rcs->nsrev = nr; for(i = nr = 0; i < rcs->nbranches; i++) { memcpy(&rcs->srev[nr], rcs->branches[i]->revs, rcs->branches[i]->nrevs * sizeof(rcs->branches[i]->revs[0])); nr += rcs->branches[i]->nrevs; } qsort(rcs->srev, rcs->nsrev, sizeof(rcs->srev[0]), sort_revision); qsort(rcs->branches, rcs->nbranches, sizeof(rcs->branches[0]), sort_branch); if(!rcs->branch) { /* The main trunk is the active trunk */ rcs->tags->tags = xrealloc(rcs->tags->tags, (rcs->tags->ntags+1)*sizeof(rcs->tags->tags[0])); rcs->tags->tags[rcs->tags->ntags] = xmalloc(sizeof(tag_t)); rcs->tags->tags[rcs->tags->ntags]->tag = xstrdup("MAIN"); rcs->tags->tags[rcs->tags->ntags]->rev = xmalloc(sizeof(rev_t)); rcs->tags->tags[rcs->tags->ntags]->rev->rev = xstrdup(rcs->active->branch->rev); rcs->tags->tags[rcs->tags->ntags]->rev->branch = xstrdup(rcs->active->branch->branch); rcs->tags->tags[rcs->tags->ntags]->rev->isbranch = 1; rcs->tags->ntags++; } /* We should have at least two tags (HEAD and MAIN) */ assert(rcs->tags != NULL); for(i = 0; i < rcs->tags->ntags; i++) { tag_t *t = rcs->tags->tags[i]; if(t->rev->isbranch) { branch_t **b; add_btag: b = bsearch(t->rev, rcs->branches, rcs->nbranches, sizeof(rcs->branches[0]), search_branch); if(!b) { rev_t rev; revision_t **r; /* This happens for the magic branch numbers if there are * no commits within the new branch yet. So, we add the * branch and try to continue. */ rev.rev = previous_rev(t->rev->branch); rev.branch = NULL; rev.isbranch = 0; r = bsearch(&rev, rcs->srev, rcs->nsrev, sizeof(rcs->srev[0]), search_revision); xfree(rev.rev); if(!r) { stack_msg(MSG_WARN, "No branch found for tag '%s:%s'", t->tag, t->rev->branch); } else { rcs->branches = xrealloc(rcs->branches, (rcs->nbranches+1)*sizeof(rcs->branches[0])); rcs->branches[rcs->nbranches] = xmalloc(sizeof(branch_t)); rcs->branches[rcs->nbranches]->branch = dup_rev(t->rev); rcs->branches[rcs->nbranches]->branchpoint = *r; (*r)->branches = xrealloc((*r)->branches, ((*r)->nbranches+1)*sizeof((*r)->branches[0])); (*r)->branches[(*r)->nbranches] = rcs->branches[rcs->nbranches]; (*r)->nbranches++; rcs->nbranches++; /* Resort the branches */ qsort(rcs->branches, rcs->nbranches, sizeof(rcs->branches[0]), sort_branch); goto add_btag; } } else { branch_t *bb = *b; bb->tags = xrealloc(bb->tags, (bb->ntags+1)*sizeof(bb->tags[0])); bb->tags[bb->ntags] = t; bb->ntags++; } } else { revision_t **r = bsearch(t->rev, rcs->srev, rcs->nsrev, sizeof(rcs->srev[0]), search_revision); if(!r) { stack_msg(MSG_WARN, "No revision found for tag '%s:%s'\n", t->tag, t->rev->rev); } else { revision_t *rr = *r; t->logrev = rr; if(!conf.rev_maxtags || rr->ntags <= conf.rev_maxtags) { rr->tags = xrealloc(rr->tags, (rr->ntags+1)*sizeof(rr->tags[0])); if(conf.rev_maxtags && rr->ntags == conf.rev_maxtags) { rr->tags[rr->ntags] = xmalloc(sizeof(tag_t)); rr->tags[rr->ntags]->tag = xstrdup("..."); rr->tags[rr->ntags]->rev = t->rev; } else rr->tags[rr->ntags] = t; rr->ntags++; } } if(conf.tag_negate) t->ignore++; /* Mark the tag ignored if it matches the configuration */ if(regextag && !regexec(regextag, t->tag, 0, NULL, 0)) { if(conf.tag_negate) t->ignore--; else t->ignore++; } } } /* We need to reset the first in the list of branches to the * active branch to ensure the drawing of all */ if(rcs->active != rcs->branches[0]) { branch_t **b = bsearch(rcs->active->branch, rcs->branches, rcs->nbranches, sizeof(rcs->branches[0]), search_branch); branch_t *t; assert(b != NULL); t = *b; *b = rcs->branches[0]; rcs->branches[0] = t; } if(regextag) { regfree(regextag); xfree(regextag); } } /* ************************************************************************** * String expansion ************************************************************************** */ static char *_string; static int _nstring; static int _nastring; static void zap_string(void) { _nstring = 0; if(_string) _string[0] = '\0'; } static char *dup_string(void) { if(_string) return xstrdup(_string); else return ""; } static void add_string_str(const char *s) { int l = strlen(s) + 1; if(_nstring + l > _nastring) { _nastring += MAX(128, l); _string = xrealloc(_string, _nastring * sizeof(_string[0])); } memcpy(_string+_nstring, s, l); _nstring += l-1; } static void add_string_ch(int ch) { char buf[2]; buf[0] = ch; buf[1] = '\0'; add_string_str(buf); } static void add_string_date(const char *d) { struct tm tm, *tmp; int n; time_t t; char *buf; int nbuf; char *env; memset(&tm, 0, sizeof(tm)); n = sscanf(d, "%d.%d.%d.%d.%d.%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec); tm.tm_mon--; if(tm.tm_year > 1900) tm.tm_year -= 1900; env = getenv("TZ"); putenv("TZ=UTC0"); t = mktime(&tm); if(env) { char *c = xmalloc(strlen(env) + 3 + 1); /* Extra space for TZ and = */ sprintf(c, "TZ=%s", env); putenv(c); xfree(c); } else putenv("TZ"); if(n != 6 || t == (time_t)(-1)) { add_string_str(""); return; } tmp = localtime(&t); nbuf = (strlen(conf.date_format)+1) * 16; /* Should be enough to hold all types of expansions */ buf = xmalloc(nbuf); strftime(buf, nbuf, conf.date_format, tmp); add_string_str(buf); xfree(buf); } static void add_string_str_html(const char *s, int maxlen) { int l = 0; char *str = xmalloc(6 * strlen(s) + 1); /* Should hold all char entity-expand */ char *cptr = str; for(; *s; s++) { if(maxlen && l > abs(maxlen)) { cptr += sprintf(cptr, "..."); break; } if(*s < 0x20) { if(*s == '\n') { if(maxlen < 0) *cptr++ = ' '; else cptr += sprintf(cptr, "", conf.html_level == HTMLLEVEL_X ? " /" : ""); } } else if(*s >= 0x7f || *s == '"') cptr += sprintf(cptr, "&#%d;", (int)(unsigned char)*s); else if(*s == '<') cptr += sprintf(cptr, "<"); else if(*s == '>') cptr += sprintf(cptr, ">"); else if(*s == '&') cptr += sprintf(cptr, "&"); else if(*s == '"') cptr += sprintf(cptr, """); else *cptr++ = *s; l++; } *cptr = '\0'; add_string_str(str); xfree(str); } static void add_string_str_len(const char *s, int maxlen) { int l = strlen(s); char *str = xmalloc(l + 1 + 3); strcpy(str, s); if(maxlen < l) sprintf(&str[maxlen], "..."); add_string_str(str); xfree(str); } static char *expand_string(const char *s, rcsfile_t *rcs, revision_t *r, rev_t *rev, rev_t *prev, tag_t *tag) { char nb[32]; char nr[32]; char *base; char *exp; int l; char ch; if(!s) return xstrdup(""); zap_string(); sprintf(nb, "%d", rcs->nbranches + rcs->nfolds); sprintf(nr, "%d", rcs->nsrev); for(; *s; s++) { if(*s == '%') { switch(*++s) { case 'c': case 'C': add_string_str(conf.cvsroot); if(*s == 'C' && conf.cvsroot[0] && conf.cvsroot[strlen(conf.cvsroot)-1] == '/') { /* Strip the trailing '/' */ _nstring--; _string[_nstring] = '\0'; } break; case 'f': case 'F': base = strrchr(rcs->file, '/'); if(!base) add_string_str(rcs->file); else add_string_str(base+1); if(*s == 'F' && _string[_nstring-1] == 'v' && _string[_nstring-2] == ',') { _nstring -= 2; _string[_nstring] = '\0'; } break; case 'p': base = strrchr(rcs->file, '/'); if(base) { char *c = xstrdup(rcs->file); base = strrchr(c, '/'); assert(base != NULL); base[1] = '\0'; add_string_str(c); xfree(c); } /* * We should not add anything here because we can encounter * a completely empty path, in which case we do not want * to add any slash. This prevents an inadvertent root redirect. * * else * add_string_str("/"); */ break; case 'm': case 'M': add_string_str(conf.cvsmodule); if(*s == 'M' && conf.cvsmodule[0] && conf.cvsmodule[strlen(conf.cvsmodule)-1] == '/') { /* Strip the trailing '/' */ _nstring--; _string[_nstring] = '\0'; } break; case 'r': add_string_str(nr); break; case 'b': add_string_str(nb); break; case '%': add_string_ch('%'); break; case '0': if(conf.expand[0]) add_string_str(conf.expand[0]); break; case '1': if(conf.expand[1]) add_string_str(conf.expand[1]); break; case '2': if(conf.expand[2]) add_string_str(conf.expand[2]); break; case '3': if(conf.expand[3]) add_string_str(conf.expand[3]); break; case '4': if(conf.expand[4]) add_string_str(conf.expand[4]); break; case '5': if(conf.expand[5]) add_string_str(conf.expand[5]); break; case '6': if(conf.expand[6]) add_string_str(conf.expand[6]); break; case '7': if(conf.expand[7]) add_string_str(conf.expand[7]); break; case '8': if(conf.expand[8]) add_string_str(conf.expand[8]); break; case '9': if(conf.expand[9]) add_string_str(conf.expand[9]); break; case 'R': if(rev && rev->rev) add_string_str(rev->rev); break; case 'P': if(prev && prev->rev) add_string_str(prev->rev); break; case 'B': if(rev && rev->branch) add_string_str(rev->branch); break; case 't': if(tag && tag->tag) add_string_str(tag->tag); break; case 'd': if(r && r->delta && r->delta->date) add_string_date(r->delta->date); break; case 's': if(r && r->delta && r->delta->state) add_string_str(r->delta->state); break; case 'a': if(r && r->delta && r->delta->author) add_string_str(r->delta->author); break; case 'L': case 'l': ch = *s; l = 0; if(s[1] == '[') { char *cptr = strchr(s, ']'); char *eptr; if(cptr) { l = strtol(&s[2], &eptr, 10); if(eptr != cptr) l = 0; else s = cptr; } } if(!conf.parse_logs) add_string_str("N/A"); else if(r && r->dtext && r->dtext->log) { if(ch == 'l') add_string_str_html(r->dtext->log, l); else add_string_str_len(r->dtext->log, abs(l)); } break; case '(': base = dup_string(); exp = expand_string(s+1, rcs, r, rev, prev, tag); zap_string(); add_string_str(base); add_string_str_html(exp, 0); xfree(base); xfree(exp); /* Find the %) in this recursion level */ for(; *s; s++) { if(*s == '%' && s[1] == ')') { s++; break; } } if(!*s) { s--; /* To end outer loop */ stack_msg(MSG_WARN, "string expand: Missing %%) in expansion"); } break; case ')': return dup_string(); default: add_string_ch('%'); add_string_ch(*s); break; } } else add_string_ch(*s); } return dup_string(); } /* ************************************************************************** * Drawing routines ************************************************************************** */ static color_t *clr_id = NULL; static int nclr_id = 0; static int rexpr_eval(const char *key, const char *content, int flags) { int res; regex_t re; if(regcomp(&re, content, flags | REG_EXTENDED | REG_NOSUB)) return 0; res = regexec(&re, key, 0, NULL, 0); regfree(&re); return res == 0; } static int expr_eval(const char *key, int op, const char *content) { switch(op) { case OP_CONTAINED: return rexpr_eval(key, content, 0); case OP_CONTAINEDI: return rexpr_eval(key, content, REG_ICASE); case OP_NCONTAINED: return !rexpr_eval(key, content, 0); case OP_NCONTAINEDI: return !rexpr_eval(key, content, REG_ICASE); case OP_EQ: return strcmp(key, content) == 0; case OP_NE: return strcmp(key, content) != 0; case OP_GE: return strcmp(key, content) >= 0; case OP_GT: return strcmp(key, content) > 0; case OP_LE: return strcmp(key, content) <= 0; case OP_LT: return strcmp(key, content) < 0; } return 0; } static char *eval_string(node_t *node, revision_t *r) { int i; assert(node != NULL); switch(node->key) { default: case TYPE_COLOR: return ""; /* This should not happen */ case TYPE_STRING: return node->value.str; case KEY_STATE: if(r && expr_eval(r->delta->state, node->op, node->content)) return eval_string(node->tcase, r); else return eval_string(node->fcase, r); case KEY_AUTHOR: if(r && expr_eval(r->delta->author, node->op, node->content)) return eval_string(node->tcase, r); else return eval_string(node->fcase, r); case KEY_TAG: for(i = 0; r && i < r->ntags; i++) { if(expr_eval(r->tags[i]->tag, node->op, node->content)) return eval_string(node->tcase, r); } return eval_string(node->fcase, r); case KEY_DATE: if(r && expr_eval(r->delta->date, node->op, node->content)) return eval_string(node->tcase, r); else return eval_string(node->fcase, r); case KEY_REV: if(r && expr_eval(r->rev->rev, node->op, node->content)) return eval_string(node->tcase, r); else return eval_string(node->fcase, r); } return ""; } static color_t *eval_color(node_t *node, revision_t *r, branch_t *b) { int i; assert(node != NULL); switch(node->key) { default: case TYPE_STRING: return &black_color; /* This should not happen */ case TYPE_COLOR: return &node->value.clr; case KEY_STATE: if(r && expr_eval(r->delta->state, node->op, node->content)) return eval_color(node->tcase, r, b); else return eval_color(node->fcase, r, b); case KEY_AUTHOR: if(r && expr_eval(r->delta->author, node->op, node->content)) return eval_color(node->tcase, r, b); else return eval_color(node->fcase, r, b); case KEY_TAG: for(i = 0; r && i < r->ntags; i++) { if(expr_eval(r->tags[i]->tag, node->op, node->content)) return eval_color(node->tcase, r, b); } return eval_color(node->fcase, r, b); case KEY_DATE: if(r && expr_eval(r->delta->date, node->op, node->content)) return eval_color(node->tcase, r, b); else return eval_color(node->fcase, r, b); case KEY_REV: if(r && expr_eval(r->rev->rev, node->op, node->content)) return eval_color(node->tcase, r, b); if(b && expr_eval(b->branch->branch, node->op, node->content)) return eval_color(node->tcase, r, b); return eval_color(node->fcase, r, b); } return &black_color; } static color_t *clr(gdImagePtr im, const char *s, revision_t *r, branch_t *b, int idx) { int i; color_t *c = get_colorref(s, idx); if(!c) c = &black_color; if(c->node) c = eval_color(c->node, r, b); for(i = 0; i < nclr_id; i++) { if(c->r == clr_id[i].r && c->g == clr_id[i].g && c->b == clr_id[i].b) return &clr_id[i]; } clr_id = xrealloc(clr_id, (nclr_id+1) * sizeof(*clr_id)); clr_id[nclr_id] = *c; clr_id[nclr_id].id = gdImageColorAllocate(im, c->r, c->g, c->b); return &clr_id[nclr_id++]; } static void zap_clr(void) { if(clr_id) xfree(clr_id); clr_id = NULL; nclr_id = 0; } static int get_swidth(const char *s, font_t *f) { int n; int m; if(!s || !*s) return 0; #if defined(HAVE_GDIMAGESTRINGFT) || defined(HAVE_GDIMAGESTRINGTTF) if(conf.use_ttf && f->ttfont) { int bb[8]; char *e; #ifdef HAVE_GDIMAGESTRINGFT e = gdImageStringFT(NULL, bb, 0, f->ttfont, f->ttsize, 0.0, 0, 0, (char *)s); #else e = gdImageStringTTF(NULL, bb, 0, f->ttfont, f->ttsize, 0.0, 0, 0, (char *)s); #endif if(!e) return bb[2] - bb[6]; } #endif for(n = m = 0; *s; n++, s++) { if(*s == '\n') { if(n > m) m = n; n = 0; } } if(n > m) m = n; return f->gdfont ? m * f->gdfont->w : m; } static int get_sheight(const char *s, font_t *f) { int nl; if(!s || !*s) return 0; #if defined(HAVE_GDIMAGESTRINGFT) || defined(HAVE_GDIMAGESTRINGTTF) if(conf.use_ttf && f->ttfont) { int bb[8]; char *e; #ifdef HAVE_GDIMAGESTRINGFT e = gdImageStringFT(NULL, bb, 0, f->ttfont, f->ttsize, 0.0, 0, 0, (char *)s); #else e = gdImageStringTTF(NULL, bb, 0, f->ttfont, f->ttsize, 0.0, 0, 0, (char *)s); #endif if(!e) return bb[3] - bb[7] + 4; } #endif for(nl = 1; *s; s++) { if(*s == '\n' && s[1]) nl++; } return nl * f->gdfont->h; } static void draw_rbox(gdImagePtr im, int x1, int y1, int x2, int y2, int r, color_t *color, color_t *bgcolor) { int r2 = 2*r; if(!r) gdImageFilledRectangle(im, x1, y1, x2, y2, bgcolor->id); #ifdef HAVE_GDIMAGEFILLEDARC else { gdImageFilledArc(im, x1+r, y1+r, r2, r2, 180, 270, bgcolor->id, gdArc); gdImageFilledArc(im, x2-r, y1+r, r2, r2, 270, 360, bgcolor->id, gdArc); gdImageFilledArc(im, x1+r, y2-r, r2, r2, 90, 180, bgcolor->id, gdArc); gdImageFilledArc(im, x2-r, y2-r, r2, r2, 0, 90, bgcolor->id, gdArc); gdImageFilledRectangle(im, x1+r, y1, x2-r, y1+r, bgcolor->id); gdImageFilledRectangle(im, x1, y1+r, x2, y2-r, bgcolor->id); gdImageFilledRectangle(im, x1+r, y2-r, x2-r, y2, bgcolor->id); } #endif gdImageLine(im, x1+r, y1, x2-r, y1, color->id); gdImageLine(im, x1+r, y2, x2-r, y2, color->id); gdImageLine(im, x1, y1+r, x1, y2-r, color->id); gdImageLine(im, x2, y1+r, x2, y2-r, color->id); if(conf.box_shadow) { gdImageLine(im, x1+r+1, y2+1, x2-r, y2+1, clr(im, NULL, NULL, NULL, 0)->id); gdImageLine(im, x2+1, y1+r+1, x2+1, y2-r, clr(im, NULL, NULL, NULL, 0)->id); } if(r) { /* FIXME: Pixelization is not perfect */ gdImageArc(im, x1+r, y1+r, r2, r2, 180, 270, color->id); gdImageArc(im, x2-r, y1+r, r2, r2, 270, 360, color->id); gdImageArc(im, x1+r, y2-r, r2, r2, 90, 180, color->id); if(conf.box_shadow) { gdImageArc(im, x2-r+1, y2-r+1, r2, r2, 0, 90, clr(im, NULL, NULL, NULL, 0)->id); gdImageArc(im, x2-r+1, y2-r, r2, r2, 0, 90, clr(im, NULL, NULL, NULL, 0)->id); gdImageArc(im, x2-r, y2-r+1, r2, r2, 0, 90, clr(im, NULL, NULL, NULL, 0)->id); } gdImageArc(im, x2-r, y2-r, r2, r2, 0, 90, color->id); #if !defined(NOGDFILL) && !defined(HAVE_GDIMAGEFILLEDARC) /* BUG: We clip manually because libgd segfaults on out of bound values */ if((x1+x2)/2 >= 0 && (x1+x2)/2 < gdImageSX(im) && (y1+y2)/2 >= 0 && (y1+y2)/2 < gdImageSY(im)) gdImageFillToBorder(im, (x1+x2)/2, (y1+y2)/2, color->id, bgcolor->id); #endif } } static void draw_string(gdImagePtr im, char *s, font_t *f, int x, int y, int align, color_t *c) { int h = get_sheight(s, f); int xx, yy; switch(align & ALIGN_HX) { default: case ALIGN_HL: xx = 0; break; case ALIGN_HC: xx = -get_swidth(s, f)/2; break; case ALIGN_HR: xx = -get_swidth(s, f); break; } switch(align & ALIGN_VX) { default: case ALIGN_VT: yy = 0; break; case ALIGN_VC: yy = h/2; break; case ALIGN_VB: yy = h; break; } #if defined(HAVE_GDIMAGESTRINGFT) || defined(HAVE_GDIMAGESTRINGTTF) if(conf.use_ttf && f->ttfont) { int bb[8]; char *e; int cid = conf.anti_alias ? c->id : -c->id; #ifdef HAVE_GDIMAGESTRINGFT e = gdImageStringFT(im, bb, cid, f->ttfont, f->ttsize, 0.0, x+xx, y+yy+h-2, (char *)s); #else e = gdImageStringTTF(im, bb, cid, f->ttfont, f->ttsize, 0.0, x+xx, y+yy+h-2, (char *)s); #endif if(!e) return; } #endif yy = -yy; gdImageString(im, f->gdfont, x+xx+1, y+yy, (unsigned char *)s, c->id); } static void draw_stringnl(gdImagePtr im, char *s, font_t *f, int x, int y, int align, color_t *c) { char *t; char *d; d = s = xstrdup(s); do { t = strchr(s, '\n'); if(t) *t = '\0'; draw_string(im, s, f, x, y, align, c); y += get_sheight(s, f); s = t+1; } while(t); xfree(d); } static void draw_rev(gdImagePtr im, revision_t *r) { int lx; int rx; int x2; int i; int ty; if(conf.left_right) { lx = r->cx; rx = r->cx + r->w; ty = r->y - r->h/2; x2 = r->cx + r->w/2; } else { lx = r->cx - r->w/2; rx = lx + r->w; ty = r->y; x2 = r->cx; } draw_rbox(im, lx, ty, rx, ty+r->h, 0, clr(im, "rev_color", r, NULL, 0), clr(im, "rev_bgcolor", r, NULL, 0)); ty += conf.rev_tspace; if(!conf.rev_hidenumber) { draw_string(im, r->revidtext, &conf.rev_font, x2, ty, ALIGN_HC, clr(im, "rev_color", r, NULL, 0)); ty += get_sheight(r->revidtext, &conf.rev_font); } draw_stringnl(im, r->revtext, &conf.rev_text_font, x2, ty, ALIGN_HC, clr(im, "rev_text_color", r, NULL, 0)); ty += get_sheight(r->revtext, &conf.rev_text_font); for(i = 0; i < r->ntags; i++) { draw_string(im, r->tags[i]->tag, &conf.tag_font, x2, ty, ALIGN_HC, clr(im, "tag_color", r, NULL, 0)); ty += get_sheight(r->tags[i]->tag, &conf.tag_font) + conf.rev_separator; } } static void draw_branch_box(gdImagePtr im, branch_t *b, int xp, int yp) { int lx; int rx; int i; int yy; int x2; if(conf.left_right) { lx = b->cx; rx = lx + b->w; x2 = b->cx + b->w/2; } else { lx = b->cx - b->w/2; rx = lx + b->w; x2 = b->cx; } draw_rbox(im, lx+xp, yp, rx+xp, yp+b->h, 5, clr(im, "branch_color", NULL, b, 0), clr(im, "branch_bgcolor", NULL, b, 0)); yy = conf.branch_tspace; if(!b->nfolds) { if(!conf.rev_hidenumber) { draw_string(im, b->branch->branch, &conf.branch_font, x2+xp, yp+yy, ALIGN_HC, clr(im, "branch_color", NULL, b, 0)); yy += get_sheight(b->branch->branch, &conf.branch_font); } for(i = 0; i < b->ntags; i++) { draw_string(im, b->tags[i]->tag, &conf.branch_tag_font, x2+xp, yp+yy, ALIGN_HC, clr(im, "branch_tag_color", NULL, b, 0)); yy += get_sheight(b->tags[i]->tag, &conf.branch_tag_font); } } else { int y1, y2; int tx = lx + b->fw + conf.branch_lspace; int nx = tx - get_swidth(" ", &conf.branch_font); draw_string(im, b->branch->branch, &conf.branch_font, nx+xp, yp+yy, ALIGN_HR, clr(im, "branch_color", NULL, b, 0)); y1 = get_sheight(b->branch->branch, &conf.branch_font); draw_string(im, b->tags[0]->tag, &conf.branch_tag_font, tx+xp, yp+yy, ALIGN_HL, clr(im, "branch_tag_color", NULL, b, 0)); y2 = get_sheight(b->tags[0]->tag, &conf.branch_font); yy += MAX(y1, y2); for(i = 0; i < b->nfolds; i++) { draw_string(im, b->folds[i]->branch->branch, &conf.branch_font, nx+xp, yp+yy, ALIGN_HR, clr(im, "branch_color", NULL, b, 0)); y1 = get_sheight(b->folds[i]->branch->branch, &conf.branch_font); draw_string(im, b->folds[i]->tags[0]->tag, &conf.branch_tag_font, tx+xp, yp+yy, ALIGN_HL, clr(im, "branch_tag_color", NULL, b, 0)); y2 = get_sheight(b->folds[i]->tags[0]->tag, &conf.branch_tag_font); yy += MAX(y1, y2); } } } static void draw_branch(gdImagePtr im, branch_t *b) { int yy, xx; int i; int line[4]; int l; int sign; line[1] = line[2] = gdTransparent; /* Trivial clip the branch */ if(conf.left_right) { if(b->cx > gdImageSX(im) || b->cx+b->tw < 0 || b->y-b->th/2 > gdImageSY(im) || b->y+b->th/2 < 0) return; } else { if(b->cx-b->tw/2 > gdImageSX(im) || b->cx+b->tw/2 < 0 || b->y > gdImageSY(im) || b->y+b->th < 0) return; } draw_branch_box(im, b, 0, conf.left_right ? b->y - b->h/2 : b->y); if(conf.left_right) { if(conf.upside_down) { xx = b->cx; for(i = 0; i < b->nrevs; i++) { revision_t *r = b->revs[i]; line[0] = line[3] = clr(im, "rev_color", r, b, 0)->id; gdImageSetStyle(im, line, r->stripped > 0 ? 4 : 1); gdImageLine(im, xx, r->y, r->cx+r->w, r->y, gdStyled); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, xx, r->y+pp, r->cx+r->w, r->y+pp, gdStyled); sign *= -1; } draw_rev(im, r); xx = r->cx; } if(conf.branch_dupbox && b->nrevs) { i = b->cx - b->tw + b->w; gdImageLine(im, xx, b->y, i+b->w, b->y, clr(im, "rev_color", NULL, b, 0)->id); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, xx, b->y+pp, i+b->w, b->y+pp, clr(im, "rev_color", NULL, b, 0)->id); sign *= -1; } draw_branch_box(im, b, i - b->cx, b->y - b->h/2); } } else { xx = b->cx + b->w; for(i = 0; i < b->nrevs; i++) { revision_t *r = b->revs[i]; line[0] = line[3] = clr(im, "rev_color", r, b, 0)->id; gdImageSetStyle(im, line, r->stripped > 0 ? 4 : 1); gdImageLine(im, xx, r->y, r->cx, r->y, gdStyled); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, xx, r->y+pp, r->cx, r->y+pp, gdStyled); sign *= -1; } draw_rev(im, r); xx = r->cx + r->w; } if(conf.branch_dupbox && b->nrevs) { i = b->cx + b->tw - b->w; gdImageLine(im, xx, b->y, i, b->y, clr(im, "rev_color", NULL, b, 0)->id); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, xx, b->y+pp, i, b->y+pp, clr(im, "rev_color", NULL, b, 0)->id); sign *= -1; } draw_branch_box(im, b, i - b->cx, b->y - b->h/2); } } } else { if(conf.upside_down) { yy = b->y; for(i = 0; i < b->nrevs; i++) { revision_t *r = b->revs[i]; line[0] = line[3] = clr(im, "rev_color", r, b, 0)->id; gdImageSetStyle(im, line, r->stripped > 0 ? 4 : 1); gdImageLine(im, r->cx, yy, r->cx, r->y+r->h, gdStyled); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, r->cx+pp, yy, r->cx+pp, r->y+r->h, gdStyled); sign *= -1; } draw_rev(im, r); yy = r->y; } if(conf.branch_dupbox && b->nrevs) { i = b->y - b->th + b->h; gdImageLine(im, b->cx, yy, b->cx, i, clr(im, "rev_color", NULL, b, 0)->id); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, b->cx+pp, yy, b->cx+pp, i, clr(im, "rev_color", NULL, b, 0)->id); sign *= -1; } draw_branch_box(im, b, 0, i); } } else { yy = b->y + b->h; for(i = 0; i < b->nrevs; i++) { revision_t *r = b->revs[i]; line[0] = line[3] = clr(im, "rev_color", r, b, 0)->id; gdImageSetStyle(im, line, r->stripped > 0 ? 4 : 1); gdImageLine(im, r->cx, yy, r->cx, r->y, gdStyled); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, r->cx+pp, yy, r->cx+pp, r->y, gdStyled); sign *= -1; } draw_rev(im, r); yy = r->y + r->h; } if(conf.branch_dupbox && b->nrevs) { i = b->y + b->th - b->h; gdImageLine(im, b->cx, yy, b->cx, i, clr(im, "rev_color", NULL, b, 0)->id); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, b->cx+pp, yy, b->cx+pp, i, clr(im, "rev_color", NULL, b, 0)->id); sign *= -1; } draw_branch_box(im, b, 0, i); } } } } static void draw_connector(gdImagePtr im, branch_t *b) { int l; int sign; revision_t *r = b->branchpoint; int x1 = r->cx + r->w/2 + 2; int y1 = r->y + r->h/2; int x2 = b->cx; int y2 = b->y; if(conf.left_right) { x2 = r->cx + r->w/2; y2 = r->y + r->h/2 + 3; x1 = b->cx; y1 = b->y; if(conf.upside_down) x1 += b->w; } else { x1 = r->cx + r->w/2 + 2; y1 = r->y + r->h/2; x2 = b->cx; y2 = b->y; if(conf.upside_down) y2 += b->h; } gdImageLine(im, x1, y1, x2, y1, clr(im, "branch_color", NULL, b, 0)->id); gdImageLine(im, x2, y1, x2, y2, clr(im, "branch_color", NULL, b, 0)->id); for(sign = l = 1; l < conf.thick_lines; l++) { int pp = (l+1)/2*sign; gdImageLine(im, x1, y1+pp, x2, y1+pp, clr(im, "branch_color", NULL, b, 0)->id); gdImageLine(im, x2+pp, y1, x2+pp, y2, clr(im, "branch_color", NULL, b, 0)->id); sign *= -1; } } static void draw_merges(gdImagePtr im, rcsfile_t *rcs, int dot) { int i; for(i = 0; i < rcs->nmerges; i++) { revision_t *fr; revision_t *tr; int colorid; int x1, x2, y1, y2; switch(rcs->merges[i].type) { case TR_TAG: fr = rcs->merges[i].from.tag->logrev; tr = rcs->merges[i].to.tag->logrev; colorid = clr(im, "merge_color", NULL, NULL, rcs->merges[i].clr)->id; break; case TR_REVISION: fr = rcs->merges[i].from.rev; tr = rcs->merges[i].to.rev; colorid = clr(im, "merge_cvsnt_color", NULL, NULL, 0)->id; break; default: continue; } if(!fr || !tr || fr == tr) continue; /* This can happen with detached tags and self-references */ if(conf.left_right) { if(fr->branch == tr->branch) { y1 = fr->y - fr->h/2; y2 = tr->y - tr->h/2; } else { if(fr->branch && fr->branch->branchpoint && tr->branch && tr->branch->branchpoint && fr->branch->branchpoint->branch == tr->branch->branchpoint->branch) { y1 = fr->y + fr->h/2; y2 = tr->y + tr->h/2; } else if(fr->y < tr->y) { y1 = fr->y + fr->h/2; y2 = tr->y - tr->h/2; } else { y1 = fr->y - fr->h/2; y2 = tr->y + tr->h/2; } } x1 = fr->cx + fr->w/2; x2 = tr->cx + tr->w/2; } else { if(fr->branch == tr->branch) { x1 = fr->cx - fr->w/2; x2 = tr->cx - tr->w/2; } else { if(fr->branch && fr->branch->branchpoint && tr->branch && tr->branch->branchpoint && fr->branch->branchpoint->branch == tr->branch->branchpoint->branch) { x1 = fr->cx + fr->w/2; x2 = tr->cx + tr->w/2; } else if(fr->cx < tr->cx) { x1 = fr->cx + fr->w/2; x2 = tr->cx - tr->w/2; } else { x1 = fr->cx - fr->w/2; x2 = tr->cx + tr->w/2; } } if(rcs->merges[i].type == TR_TAG) { y1 = fr->y + rcs->merges[i].from.tag->yofs; y2 = tr->y + rcs->merges[i].to.tag->yofs; } else { y1 = fr->y + fr->h/2; y2 = tr->y + tr->h/2; } } if(dot && !conf.merge_arrows) { int o = conf.left_right ? 1 : 0; gdImageArc(im, x2, y2+o, 8, 8, 0, 360, colorid); /* BUG: We clip manually because libgd segfaults on out of bound values */ if(x2+1 >= 0 && x2+1 < gdImageSX(im) && y2+o+1 >= 0 && y2+o+1 < gdImageSY(im)) gdImageFillToBorder(im, x2+1, y2+o+1, colorid, colorid); } else if(dot && conf.merge_arrows) { /* * Arrow patch from Haroon Rafique * Slightly adapted to be more configurable. */ int sx, sy; /* start point coordinates */ int ex, ey; /* end point coordinates */ double theta; double u1, v1, u2, v2; gdPoint p[3]; sx = x1; sy = y1; ex = x2; ey = y2; if(conf.left_right) { if(fr->branch == tr->branch) { int yy = (y1 < y2 ? y1 : y2) - 5; /* line from (x1,yy) to (x2,yy) */ sy = ey = yy; } else { if(fr->branch && fr->branch->branchpoint && tr->branch && tr->branch->branchpoint && fr->branch->branchpoint->branch == tr->branch->branchpoint->branch) { /* line from (x1,y1+3+1) to (x2,y2+3+1) */ sy = y1+3+1; ey = y2+3+1; } else if(y1 > y2) { /* line from (x1,y1-3) to (x2,y2+3+1) */ sy = y1-3; ey = y2+3+1; } else { /* line from (x1,y1+3+1) to (x2,y2-3) */ sy = y1+3+1; ey = y2-3; } } } else { if(fr->branch == tr->branch) { int xx = (x1 < x2 ? x1 : x2) - 5; /* line from (xx,y1) to (xx,y2) */ sx = ex = xx; } else { if(fr->branch && fr->branch->branchpoint && tr->branch && tr->branch->branchpoint && fr->branch->branchpoint->branch == tr->branch->branchpoint->branch) { /* line from (x1+3,y1) to (x2+3,y2) */ sx = x1+3; ex = x2+3; } else if(x1 > x2) { /* line from (x1-3,y1) to (x2+3,y2) */ sx = x1-3; ex = x2+3; } else { /* line from (x1+3,y1) to (x2-3,y2) */ sx = x1+3; ex = x2-3; } } } /* * inspiration for arrow code comes from arrows.c in the * graphviz package. Thank you, AT&T */ /* theta in radians */ theta = atan2((double)(sy-ey), (double)(sx-ex)); u1 = (double)conf.arrow_length * cos(theta); v1 = (double)conf.arrow_length * sin(theta); u2 = (double)conf.arrow_width * cos(theta + M_PI/2.0); v2 = (double)conf.arrow_width * sin(theta + M_PI/2.0); /* points of polygon (triangle) */ p[0].x = ROUND(ex + u1 - u2); p[0].y = ROUND(ey + v1 - v2); p[1].x = ex; p[1].y = ey; p[2].x = ROUND(ex + u1 + u2); p[2].y = ROUND(ey + v1 + v2); /* draw the polygon (triangle) */ gdImageFilledPolygon(im, p, 3, colorid); } else { if(conf.left_right) { if(fr->branch == tr->branch) { int yy = (y1 < y2 ? y1 : y2) - 5; gdImageLine(im, x1, y1, x1, yy, colorid); gdImageLine(im, x2, y2, x2, yy, colorid); gdImageLine(im, x1, yy, x2, yy, colorid); } else { if(fr->branch && fr->branch->branchpoint && tr->branch && tr->branch->branchpoint && fr->branch->branchpoint->branch == tr->branch->branchpoint->branch) { gdImageLine(im, x1, y1, x1, y1+3+1, colorid); gdImageLine(im, x2, y2+1, x2, y2+3+1, colorid); gdImageLine(im, x1, y1+3+1, x2, y2+3+1, colorid); } else if(y1 > y2) { gdImageLine(im, x1, y1, x1, y1-3, colorid); gdImageLine(im, x2, y2+1, x2, y2+3+1, colorid); gdImageLine(im, x1, y1-3, x2, y2+3+1, colorid); } else { gdImageLine(im, x1, y1+1, x1, y1+3+1, colorid); gdImageLine(im, x2, y2, x2, y2-3, colorid); gdImageLine(im, x1, y1+3+1, x2, y2-3, colorid); } } } else { if(fr->branch == tr->branch) { int xx = (x1 < x2 ? x1 : x2) - 5; gdImageLine(im, xx, y1, x1, y1, colorid); gdImageLine(im, xx, y2, x2, y2, colorid); gdImageLine(im, xx, y1, xx, y2, colorid); } else { if(fr->branch && fr->branch->branchpoint && tr->branch && tr->branch->branchpoint && fr->branch->branchpoint->branch == tr->branch->branchpoint->branch) { gdImageLine(im, x1, y1, x1+3, y1, colorid); gdImageLine(im, x2, y2, x2+3, y2, colorid); gdImageLine(im, x1+3, y1, x2+3, y2, colorid); } else if(x1 > x2) { gdImageLine(im, x1, y1, x1-3, y1, colorid); gdImageLine(im, x2, y2, x2+3, y2, colorid); gdImageLine(im, x1-3, y1, x2+3, y2, colorid); } else { gdImageLine(im, x1, y1, x1+3, y1, colorid); gdImageLine(im, x2, y2, x2-3, y2, colorid); gdImageLine(im, x1+3, y1, x2-3, y2, colorid); } } } } } } static void draw_messages(gdImagePtr im, int offset) { int i; for(i = 0; i < nmsg_stack; i++) { draw_stringnl(im, msg_stack[i].msg, &conf.msg_font, conf.margin_left, offset, ALIGN_HL|ALIGN_VT, clr(im, "msg_color", NULL, NULL, 0)); offset += msg_stack[i].h; } } static gdImagePtr make_image(rcsfile_t *rcs) { gdImagePtr im; int i; int bgid; char *cptr; int w, h; int subx = 0, suby = 0; int msgh = 0; if(subtree_branch) { w = 0; h = 0; if(subtree_rev) { for(i = 0; i < subtree_rev->nbranches; i++) calc_subtree_size(subtree_rev->branches[i], &subx, &suby, &w, &h); } else calc_subtree_size(subtree_branch, &subx, &suby, &w, &h); } else { w = rcs->tw; h = rcs->th; } cptr = expand_string(conf.title, rcs, NULL, NULL, NULL, NULL); i = get_swidth(cptr, &conf.title_font); if(i > w) w = i; if(!quiet && nmsg_stack) { int msgw = 0; for(i = 0; i < nmsg_stack; i++) { int ww = msg_stack[i].w = get_swidth(msg_stack[i].msg, &conf.msg_font); int hh = msg_stack[i].h = get_sheight(msg_stack[i].msg, &conf.msg_font); msgh += hh; h += hh; if(ww > msgw) msgw = ww; } if(msgw > w) w = msgw; } w += conf.margin_left + conf.margin_right; h += conf.margin_top + conf.margin_bottom; im = gdImageCreate(w, h); bgid = clr(im, "color_bg", NULL, NULL, 0)->id; /* The background is always a unique color, */ zap_clr(); /* so clear the color ref table */ clr(im, NULL, NULL, NULL, 0); if(conf.transparent_bg) gdImageColorTransparent(im, bgid); if(!conf.merge_front) draw_merges(im, rcs, 0); for(i = 0; i < rcs->nbranches; i++) { if(!rcs->branches[i]->folded && !(subtree_branch && !rcs->branches[i]->subtree_draw)) draw_branch(im, rcs->branches[i]); } draw_merges(im, rcs, 1); /* The dots of the merge dest */ for(i = 0; i < rcs->nbranches; i++) { if(rcs->branches[i]->branchpoint) draw_connector(im, rcs->branches[i]); } /* Clear the margins if we have a partial tree */ if(subtree_branch) { gdImageFilledRectangle(im, 0, 0, w-1, conf.margin_top-1, bgid); gdImageFilledRectangle(im, 0, 0, conf.margin_left-1, h-1, bgid); gdImageFilledRectangle(im, 0, h-conf.margin_bottom, w-1, h-1, bgid); gdImageFilledRectangle(im, w-conf.margin_right, 0, w-1, h-1, bgid); } draw_stringnl(im, cptr, &conf.title_font, conf.title_x, conf.title_y, conf.title_align, clr(im, "title_color", NULL, NULL, 0)); xfree(cptr); if(conf.merge_front) draw_merges(im, rcs, 0); if(!quiet) draw_messages(im, h - conf.margin_bottom/2 - msgh); return im; } /* ************************************************************************** * Layout routines * * Branch BBox: * left = center_x - total_width / 2 (cx-tw)/2 * right = center_x + total_width / 2 (cx+tw)/2 * top = y_pos (y) * bottom = y_pos + total_height (y+th) * * Margins of branches: * * . . * . . * +--------------+ * ^ * | branch_margin . * v . * ----------------+ . * | ^ | * | | branch_connect | * | v | *..-+ +t-----+------+ +------+------+ * | l | | | * | <--> | branch bbox | <--> | branch bbox | * | | | r | | | *..-+ | +------------b+ | +-------------+ * | ^ branch_margin * | | branch_margin * | v * | +-------------+ * | . . * | . . * | * branch_margin * * FIXME: There are probable som +/-1 errors in the code... * (notably shadows are not calculated in the margins) ************************************************************************** */ static void move_branch(branch_t *b, int x, int y) { int i; b->cx += x; b->y += y; for(i = 0; i < b->nrevs; i++) { b->revs[i]->cx += x; b->revs[i]->y += y; } } static void initial_reposition_branch(revision_t *r, int *x, int *w) { int i, j; for(j = 0; j < r->nbranches; j++) { branch_t *b = r->branches[j]; *x += *w + conf.rev_minline + b->tw/2 - b->cx; *w = b->tw/2; move_branch(b, *x, r->y + r->h/2 + conf.branch_connect); *x = b->cx; /* Recurse to move branches of branched revisions */ for(i = b->nrevs-1; i >= 0; i--) { initial_reposition_branch(b->revs[i], x, w); } } } static void initial_reposition_branch_lr(revision_t *r, int *y, int *h) { int i, j; for(j = 0; j < r->nbranches; j++) { branch_t *b = r->branches[j]; *y += *h + conf.rev_minline + b->th/2 - b->y; *h = b->th/2; move_branch(b, r->cx + r->w/2 + conf.branch_connect, *y); *y = b->y; /* Recurse to move branches of branched revisions */ for(i = b->nrevs-1; i >= 0; i--) { initial_reposition_branch_lr(b->revs[i], y, h); } } } static void rect_union(int *x, int *y, int *w, int *h, branch_t *b) { int x1 = *x; int x2 = x1 + *w; int y1 = *y; int y2 = y1 + *h; int xx1; int xx2; int yy1; int yy2; if(conf.left_right) { xx1 = b->cx; yy1 = b->y - b->th/2; } else { xx1 = b->cx - b->tw/2; yy1 = b->y; } xx2 = xx1 + b->tw; yy2 = yy1 + b->th; x1 = MIN(x1, xx1); x2 = MAX(x2, xx2); y1 = MIN(y1, yy1); y2 = MAX(y2, yy2); *x = x1; *y = y1; *w = x2 - x1; *h = y2 - y1; } static void calc_subtree_size(branch_t *b, int *x, int *y, int *w, int *h) { int i, j; rect_union(x, y, w, h, b); for(i = 0; i < b->nrevs; i++) { for(j = 0; j < b->revs[i]->nbranches; j++) calc_subtree_size(b->revs[i]->branches[j], x, y, w, h); } } static int branch_intersects(int top, int bottom, int left, branch_t *b) { int br = b->cx + b->tw/2; int bt = b->y - conf.branch_connect - conf.branch_margin/2; int bb = b->y + b->th + conf.branch_margin/2; return !(bt > bottom || bb < top || br >= left); } static int branch_intersects_lr(int left, int right, int top, branch_t *b) { int bt = b->y + b->th/2; int bl = b->cx - conf.branch_connect - conf.branch_margin/2; int br = b->cx + b->tw + conf.branch_margin/2; return !(bl > right || br < left || bt >= top); } static int kern_branch(rcsfile_t *rcs, branch_t *b) { int left = b->cx - b->tw/2; int top = b->y - conf.branch_connect - conf.branch_margin/2; int bottom = b->y + b->th + conf.branch_margin/2; int i; int xpos = 0; for(i = 0; i < rcs->nbranches; i++) { branch_t *bp = rcs->branches[i]; if(bp == b) continue; if(branch_intersects(top, bottom, left, bp)) { int m = bp->cx + bp->tw/2 + conf.branch_margin; if(m > xpos) xpos = m; } } if(xpos && (b->cx - b->tw/2) - xpos > 0) { move_branch(b, xpos - (b->cx - b->tw/2), 0); return 1; } return 0; } static int kern_branch_lr(rcsfile_t *rcs, branch_t *b) { int top = b->y - b->th/2; int left = b->cx - conf.branch_connect - conf.branch_margin/2; int right = b->cx + b->tw + conf.branch_margin/2; int i; int ypos = 0; for(i = 0; i < rcs->nbranches; i++) { branch_t *bp = rcs->branches[i]; if(bp == b) continue; if(branch_intersects_lr(left, right, top, bp)) { int m = bp->y + bp->th/2 + conf.branch_margin; if(m > ypos) ypos = m; } } if(ypos && (b->y - b->th/2) - ypos > 0) { move_branch(b, 0, ypos - (b->y - b->th/2)); return 1; } return 0; } static int kern_tree(rcsfile_t *rcs) { int i; int moved; int safeguard; int totalmoved = 0; for(moved = 1, safeguard = LOOPSAFEGUARD; moved && safeguard; safeguard--) { moved = 0; for(i = 1; i < rcs->nbranches; i++) { if(conf.left_right) moved += kern_branch_lr(rcs, rcs->branches[i]); else moved += kern_branch(rcs, rcs->branches[i]); } totalmoved += moved; #ifdef DEBUG fprintf(stderr, "kern_tree: moved=%d\n", moved); #endif } if(!safeguard) stack_msg(MSG_WARN, "kern_tree: safeguard terminated possible infinite loop; please report."); return totalmoved; } static int index_of_revision(revision_t *r) { branch_t *b = r->branch; int i; for(i = 0; i < b->nrevs; i++) { if(r == b->revs[i]) return i; } stack_msg(MSG_ERR, "index_of_revision: Cannot find revision in branch\n"); return 0; } static void branch_bbox(branch_t *br, int *l, int *r, int *t, int *b) { if(l) *l = br->cx - br->tw/2; if(r) *r = br->cx + br->tw/2; if(t) *t = br->y; if(b) *b = br->y + br->th + ((conf.branch_dupbox && br->nrevs) ? conf.rev_minline + br->h : 0); } static void branch_ext_bbox(branch_t *br, int *l, int *r, int *t, int *b) { int extra = conf.branch_margin & 1; /* Correct +/-1 error on div 2 */ branch_bbox(br, l, r, t, b); if(l) *l -= conf.branch_margin/2; if(r) *r += conf.branch_margin/2 + extra; if(t) *t -= conf.branch_connect + conf.branch_margin/2; if(b) *b += conf.branch_margin/2 + extra; } static int branch_distance(branch_t *br1, branch_t *br2) { int l1, r1, t1, b1; int l2, r2, t2, b2; assert(br1 != NULL); assert(br2 != NULL); branch_bbox(br1, &l1, &r1, NULL, NULL); branch_bbox(br2, &l2, &r2, NULL, NULL); branch_ext_bbox(br1, NULL, NULL, &t1, &b1); branch_ext_bbox(br2, NULL, NULL, &t2, &b2); /* Return: * - 0 if branches have no horizontal overlap * - positive if b1 is left of b2 * - negative if b2 is left of b1 */ if((t1 > t2 && t1 < b2) || (b1 > t2 && b1 < b2)) return l1 < l2 ? l2 - r1 : -(l1 - r2); else return 0; } static int space_needed(branch_t *br1, branch_t *br2) { int t1, b1; int t2, b2; assert(br1 != NULL); assert(br2 != NULL); assert(br1->cx < br2->cx); /* br1 must be left of br2 */ branch_ext_bbox(br1, NULL, NULL, &t1, &b1); branch_ext_bbox(br2, NULL, NULL, &t2, &b2); /* Return: * - positive if top br1 is located lower than br2 * - negatve is top br2 is located lower than br1 */ if(t1 > t2) return -(t1 - b2); else return t2 - b1; } static void move_yr_branch(branch_t *b, int dy) { int i, j; #ifdef DEBUG /* fprintf(stderr, "move_yr_branch: b=%s, dy=%d\n", b->branch->branch, dy);*/ #endif b->y += dy; for(i = 0; i < b->nrevs; i++) { b->revs[i]->y += dy; for(j = 0; j < b->revs[i]->nbranches; j++) { #ifdef DEBUG /* fprintf(stderr, ".");*/ #endif move_yr_branch(b->revs[i]->branches[j], dy); } } } static void move_trunk(revision_t *r, int dy) { int i, j; branch_t *b = r->branch; b->th += dy; for(i = index_of_revision(r); i < b->nrevs; i++) { #ifdef DEBUG fprintf(stderr, "move_trunk: start %s, moving %s by %d (b's %d)\n", r->rev->rev, b->revs[i]->rev->rev, dy, b->revs[i]->nbranches); #endif b->revs[i]->y += dy; for(j = 0; j < b->revs[i]->nbranches; j++) { move_yr_branch(b->revs[i]->branches[j], dy); } } } static int space_below(rcsfile_t *rcs, revision_t *r) { int i, j; int bl, br, bb; int space = INT_MAX; branch_t *b = r->branch; branch_t *minb = NULL; branch_ext_bbox(b, &bl, &br, NULL, &bb); for(i = 0; i < rcs->nbranches; i++) { int tbl, tbr, tbt; branch_t *tb = rcs->branches[i]; branch_ext_bbox(tb, &tbl, &tbr, &tbt, NULL); if(tb == b) continue; if(tbt > bb) /* Must be below our branch */ { if(tb->branchpoint) /* Take account for the horiz connector */ tbl = tb->branchpoint->cx + tb->branchpoint->branch->tw/2; if((bl >= tbl && bl <= tbr) || (br <= tbr && br >= tbl)) { int s = tbt - bb - conf.branch_connect; if(s < space) { space = s; minb = tb; } } } } if(b->branchpoint) { for(i = index_of_revision(r); i < b->nrevs; i++) { for(j = 0; j < b->revs[i]->nbranches; j++) { int s = space_below(rcs, b->revs[i]->branches[j]->revs[0]); if(s < space) space = s; } } } #ifdef DEBUG fprintf(stderr, "space_below: from %s have %d to %s\n", b->branch->branch, space, minb ? minb->branch->branch : ""); #endif return space; } static int space_available(rcsfile_t *rcs, branch_t *colbr, branch_t *tagbr, int *nl, revision_t **bpcommon) { int i; int space = 0; int nlinks = 0; revision_t *r; branch_t *b; branch_t *ancestor; revision_t *branchpoint; if(!tagbr->branchpoint || !colbr->branchpoint) { stack_msg(MSG_WARN, "space_available: Trying to stretch the top?"); return 0; } r = colbr->branchpoint; b = r->branch; branchpoint = tagbr->branchpoint; ancestor = branchpoint->branch; assert(b != NULL); assert(ancestor != NULL); while(1) { int s; int rtag = b == ancestor ? index_of_revision(branchpoint)+1 : 0; for(i = index_of_revision(r); i >= rtag; i--) { if(i > 0) s = b->revs[i]->y - (b->revs[i-1]->y + b->revs[i-1]->h); else s = b->revs[i]->y - (b->y + b->h); if(s < conf.rev_maxline) { space += conf.rev_maxline - s; nlinks++; } } s = space_below(rcs, r); if(s < space) space = s; #ifdef DEBUG if(space < 0) return -1; #endif if(b == ancestor) break; r = b->branchpoint; if(!r) { /* Not a common ancestor */ r = colbr->branchpoint; b = r->branch; branchpoint = ancestor->branchpoint; if(!branchpoint) { stack_msg(MSG_WARN, "space_available: No common ancestor?"); return 0; } ancestor = branchpoint->branch; assert(ancestor != NULL); nlinks = 0; space = 0; continue; /* Restart with a new ancestor */ } b = r->branch; } if(nl) *nl = nlinks; /* Return the number of links that can stretch */ if(bpcommon) *bpcommon = branchpoint; /* Return the ancestral branchpoint on the common branch */ return space; } static int stretch_branches(rcsfile_t *rcs, branch_t *br1, branch_t *br2, int totalstretch) { revision_t *r; revision_t *bpcommon = NULL; branch_t *ancestor = NULL; branch_t *b; int i; int space; int nlinks = 0; int dy; int rest; space = space_available(rcs, br1, br2, &nlinks, &bpcommon); if(bpcommon) ancestor = bpcommon->branch; #ifdef DEBUG if(space == -1) return 0; fprintf(stderr, "stretch_branches: space available %d over %d links common %s\n", space, nlinks, ancestor->branch->branch); #endif if(space < totalstretch) return 0; dy = totalstretch / nlinks; rest = totalstretch - dy * nlinks; r = br1->branchpoint; b = r->branch; while(1) { int rtag = b == ancestor ? index_of_revision(bpcommon)+1 : 0; for(i = index_of_revision(r); i >= rtag; i--) { int s, q; if(i > 0) s = b->revs[i]->y - (b->revs[i-1]->y + b->revs[i-1]->h); else s = b->revs[i]->y - (b->y + b->h); q = conf.rev_maxline - s; if(q > 0) { int d = rest ? rest/nlinks+1 : 0; if(q >= dy+d) { move_trunk(b->revs[i], dy+d); } else { move_trunk(b->revs[i], q); rest += dy+d - q; } rest -= d; nlinks--; } } if(b == ancestor) break; r = b->branchpoint; assert(r != NULL); /* else 'space_available' wouldn't have returned positively */ b = r->branch; } return 1; } static branch_t *find_collision_branch(rcsfile_t *rcs, branch_t *b) { int i; int dist = INT_MAX; branch_t *col = NULL; for(i = 0; i < rcs->nbranches; i++) { int t = branch_distance(rcs->branches[i], b); if(t > 0 && t < dist) { dist = t; col = rcs->branches[i]; } } return col; } static void auto_stretch(rcsfile_t *rcs) { int i; int safeguard; for(i = 0, safeguard = LOOPSAFEGUARD; i < rcs->nbranches && safeguard; i++) { int bl, pr; branch_t *b = rcs->branches[i]; if(!b->branchpoint) continue; branch_bbox(b, &bl, NULL, NULL, NULL); branch_bbox(b->branchpoint->branch, NULL, &pr, NULL, NULL); if(bl - conf.branch_margin - pr > 0) { branch_t *col; int spaceneeded; /* There is a potential to move branch b further left. * All branches obstructing this one from moving further * left must be originating from revisions below * b->branchpoint until a common ancester. * So, we search all branches for a branch that lies left * of b and is closest to b. This is then the collission * branch that needs to be moved. */ col = find_collision_branch(rcs, b); if(!col) continue; spaceneeded = space_needed(col, b); if(spaceneeded < 0) continue; #ifdef DEBUG fprintf(stderr, "auto_stretch: %s collides %s need %d\n", b->branch->branch, col->branch->branch, spaceneeded); #endif /* Trace the collision branch back to find the common ancester * of both col and b. All revisions encountered while traversing * backwards must be stretched, including all revisions on the * common ancester from where the branches sprout. */ if(stretch_branches(rcs, col, b, spaceneeded)) { if(kern_tree(rcs)) { /* Restart the process because movement can * cause more movement. */ i = 0 - 1; /* -1 for the i++ of the loop */ safeguard--; /* Prevent infinite loop, just in case */ } /*return;*/ } } } if(!safeguard) stack_msg(MSG_ERR, "auto_stretch: safeguard terminated possible infinite loop; please report."); } static void fold_branch(rcsfile_t *rcs, revision_t *r) { int i, j; branch_t *btag = NULL; for(i = 0; i < r->nbranches; i++) { branch_t *b = r->branches[i]; if(!b->nrevs && b->ntags < 2) { /* No commits in this branch and no duplicate tags */ if(!btag) btag = b; else { /* We have consecutive empty branches, fold */ b->folded = 1; b->folded_to = btag; for(j = 0; j < rcs->nbranches; j++) { if(b == rcs->branches[j]) { /* Zap the branch from the admin */ memmove(&rcs->branches[j], &rcs->branches[j+1], (rcs->nbranches - j - 1)*sizeof(rcs->branches[0])); rcs->nbranches--; rcs->nfolds++; break; } } memmove(&r->branches[i], &r->branches[i+1], (r->nbranches - i - 1)*sizeof(r->branches[0])); r->nbranches--; i--; /* We have one less now */ /* Add to the fold-list */ btag->folds = xrealloc(btag->folds, (btag->nfolds+1) * sizeof(btag->folds[0])); btag->folds[btag->nfolds] = b; btag->nfolds++; } } else { if(!conf.branch_foldall) btag = NULL; /* Start a new box */ /* Recursively fold sub-branches */ for(j = 0; j < b->nrevs; j++) fold_branch(rcs, b->revs[j]); } } } static void mark_subtree(branch_t *b) { int i, j; b->subtree_draw = 1; for(i = 0; i < b->nrevs; i++) { for(j = 0; j < b->revs[i]->nbranches; j++) mark_subtree(b->revs[i]->branches[j]); } } static void make_layout(rcsfile_t *rcs) { int i, j; int x, y; int w, h; int w2; /* Remove all unwanted revisions */ if(conf.strip_untagged) { int fr = conf.strip_first_rev ? 0 : 1; for(i = 0; i < rcs->nbranches; i++) { branch_t *bp = rcs->branches[i]; for(j = fr; j < bp->nrevs-1; j++) { if(!bp->revs[j]->ntags && bp->revs[j]->stripped >= 0 && !bp->revs[j]->mergetarget && !bp->revs[j]->nbranches) { bp->revs[j+1]->stripped = 1; memmove(&bp->revs[j], &bp->revs[j+1], (bp->nrevs-j-1) * sizeof(bp->revs[0])); bp->nrevs--; j--; } } } } /* Find the sub-tree(s) we want to see */ if(conf.branch_subtree && conf.branch_subtree[0]) { branch_t **b; revision_t **r; rev_t rev; int k; char *tag = conf.branch_subtree; /* First translate any symbolic tag to a real branch/revision number */ if(rcs->tags) { for(k = 0; k < rcs->tags->ntags; k++) { if(!strcmp(conf.branch_subtree, rcs->tags->tags[k]->tag)) { if(rcs->tags->tags[k]->rev->isbranch) tag = rcs->tags->tags[k]->rev->branch; else tag = rcs->tags->tags[k]->rev->rev; break; } } } /* Find the corresponding branch */ rev.branch = tag; rev.rev = NULL; rev.isbranch = 1; b = bsearch(&rev, rcs->branches, rcs->nbranches, sizeof(rcs->branches[0]), search_branch); if(b) { if((*b)->branchpoint) { subtree_branch = *b; for(k = 0; k < (*b)->branchpoint->nbranches; k++) mark_subtree((*b)->branchpoint->branches[k]); } /* * else -> we want everything. * This happens for the top level branch because it has no * branchpoint. We do not set the subtree_branch, which then * results in drawing the whole tree as if we did not select a * particular branch. */ } else { /* Maybe it is a revision we want all subtrees from */ rev.rev = tag; rev.branch = NULL; rev.isbranch = 0; r = bsearch(&rev, rcs->srev, rcs->nsrev, sizeof(rcs->srev[0]), search_revision); if(r) { if((*r)->nbranches) { subtree_branch = (*r)->branches[0]; subtree_rev = *r; for(k = 0; k < (*r)->nbranches; k++) mark_subtree((*r)->branches[k]); } /* * else -> we select everything. * This happens for the any revision that has no branches. * We do not set the subtree_branch, which then results in * drawing the whole tree as if we did not select a * particular revision's branches. */ } } } /* Fold all empty branches in one box on the same branchpoint */ if(conf.branch_fold) { for(i = 0; i < rcs->branches[0]->nrevs; i++) { if(rcs->branches[0]->revs[i]->nbranches > 0) fold_branch(rcs, rcs->branches[0]->revs[i]); } } /* Remove all unwanted tags */ for(i = 0; i < rcs->nbranches; i++) { branch_t *bp = rcs->branches[i]; for(j = 0; j < bp->nrevs; j++) { revision_t *r = bp->revs[j]; int k; for(k = 0; k < r->ntags; k++) { if(r->tags[k]->ignore > 0) { memmove(&r->tags[k], &r->tags[k+1], (r->ntags-k-1) * sizeof(r->tags[0])); r->ntags--; k--; } } } } /* Calculate the box-sizes of the revisions */ for(i = 0; i < rcs->nsrev; i++) { revision_t *rp; int w; int h; rp = rcs->srev[i]; rp->revtext = expand_string(conf.rev_text.node ? eval_string(conf.rev_text.node, rp) : conf.rev_text.str, rcs, rp, rp->rev, NULL, rp->ntags ? rp->tags[0] : NULL); rp->revidtext = expand_string(conf.rev_idtext.node ? eval_string(conf.rev_idtext.node, rp) : conf.rev_idtext.str, rcs, rp, rp->rev, NULL, rp->ntags ? rp->tags[0] : NULL); w = get_swidth(rp->revtext, &conf.rev_text_font); j = get_swidth(rp->revidtext, &conf.rev_font); if(j > w) w = j; h = get_sheight(rp->revtext, &conf.rev_text_font); if(!conf.rev_hidenumber) h += get_sheight(rp->revidtext, &conf.rev_font); for(j = 0; j < rp->ntags; j++) { int ww = get_swidth(rp->tags[j]->tag, &conf.tag_font); int th; if(ww > w) w = ww; th = get_sheight(rp->tags[j]->tag, &conf.tag_font) + conf.rev_separator; rp->tags[j]->yofs = h + th/2 + conf.rev_tspace; h += th; } rp->w = w + conf.rev_lspace + conf.rev_rspace; rp->h = h + conf.rev_tspace + conf.rev_bspace; } /* Calculate the box-sizes of the branches */ for(i = 0; i < rcs->nbranches; i++) { branch_t *bp = rcs->branches[i]; int w; int h; if(!bp->nfolds) { w = get_swidth(bp->branch->branch, &conf.branch_font); if(conf.rev_hidenumber) h = 0; else h = get_sheight(bp->branch->branch, &conf.branch_font); for(j = 0; j < bp->ntags; j++) { int ww = get_swidth(bp->tags[j]->tag, &conf.branch_tag_font); if(ww > w) w = ww; h += get_sheight(bp->tags[j]->tag, &conf.branch_tag_font); } } else { int h1, h2; int w1, w2; int fw; w1 = get_swidth(bp->branch->branch, &conf.branch_font); w1 += get_swidth(" ", &conf.branch_font); w2 = get_swidth(bp->tags[0]->tag, &conf.branch_tag_font); fw = w1; w = w1 + w2; h1 = get_sheight(bp->branch->branch, &conf.branch_font); h2 = get_sheight(bp->tags[0]->tag, &conf.branch_tag_font); h = MAX(h1, h2); for(j = 0; j < bp->nfolds; j++) { w1 = get_swidth(bp->folds[j]->branch->branch, &conf.branch_font); w1 += get_swidth(" ", &conf.branch_font); w2 = get_swidth(bp->folds[j]->tags[0]->tag, &conf.branch_tag_font); if(w1 > fw) fw = w1; if(w1 + w2 > w) w = w1 + w2; h1 = get_sheight(bp->folds[j]->branch->branch, &conf.branch_font); h2 = get_sheight(bp->folds[j]->tags[0]->tag, &conf.branch_tag_font); h += MAX(h1, h2); } bp->fw = fw; } w += conf.branch_lspace + conf.branch_rspace; h += conf.branch_tspace + conf.branch_bspace; bp->w = w; bp->h = h; if(conf.left_right) { for(j = 0; j < bp->nrevs; j++) { if(bp->revs[j]->h > h) h = bp->revs[j]->h; w += bp->revs[j]->w + conf.rev_minline; } if(conf.branch_dupbox && bp->nrevs) w += bp->w + conf.rev_minline; } else { for(j = 0; j < bp->nrevs; j++) { if(bp->revs[j]->w > w) w = bp->revs[j]->w; h += bp->revs[j]->h + conf.rev_minline; } if(conf.branch_dupbox && bp->nrevs) h += bp->h + conf.rev_minline; } bp->th = h; bp->tw = w; } /* Calculate the relative positions of revs in a branch */ if(conf.left_right) { for(i = 0; i < rcs->nbranches; i++) { branch_t *b = rcs->branches[i]; y = b->th/2; x = b->w; b->y = y; b->cx = 0; for(j = 0; j < b->nrevs; j++) { x += conf.rev_minline; b->revs[j]->y = y; b->revs[j]->cx = x; x += b->revs[j]->w; } } } else { for(i = 0; i < rcs->nbranches; i++) { branch_t *b = rcs->branches[i]; x = b->tw/2; y = b->h; b->cx = x; b->y = 0; for(j = 0; j < b->nrevs; j++) { y += conf.rev_minline; b->revs[j]->cx = x; b->revs[j]->y = y; y += b->revs[j]->h; } } } /* Initially reposition the branches from bottom to top progressively right */ if(conf.left_right) { x = rcs->branches[0]->y; w2 = rcs->branches[0]->th / 2; for(i = rcs->branches[0]->nrevs-1; i >= 0; i--) { initial_reposition_branch_lr(rcs->branches[0]->revs[i], &x, &w2); } } else { x = rcs->branches[0]->cx; w2 = rcs->branches[0]->tw / 2; for(i = rcs->branches[0]->nrevs-1; i >= 0; i--) { initial_reposition_branch(rcs->branches[0]->revs[i], &x, &w2); } } /* Initially move branches left if there is room */ kern_tree(rcs); /* Try to kern the branches more by expanding the inter-revision spacing */ if(conf.auto_stretch && !conf.left_right) auto_stretch(rcs); /* Calculate overall image size */ if(conf.left_right) { x = rcs->branches[0]->cx; y = rcs->branches[0]->y - rcs->branches[0]->th/2; } else { x = rcs->branches[0]->cx - rcs->branches[0]->tw/2; y = rcs->branches[0]->y; } w = rcs->branches[0]->tw; h = rcs->branches[0]->th; for(i = 1; i < rcs->nbranches; i++) rect_union(&x, &y, &w, &h, rcs->branches[i]); rcs->tw = w; rcs->th = h; /* Flip the entire tree */ if(conf.upside_down) { if(conf.left_right) { x += rcs->tw; for(i = 0; i < rcs->nbranches; i++) { branch_t *b = rcs->branches[i]; for(j = 0; j < b->nrevs; j++) { revision_t *r = b->revs[j]; r->cx = x - r->cx - r->w; } b->cx = x - b->cx - b->w; } } else { y += rcs->th; for(i = 0; i < rcs->nbranches; i++) { branch_t *b = rcs->branches[i]; for(j = 0; j < b->nrevs; j++) { revision_t *r = b->revs[j]; r->y = y - r->y - r->h; } b->y = y - b->y - b->h; } } } /* Relocate the lot if we only draw a sub-tree */ if(subtree_branch) { int xx, yy; if(subtree_branch->folded) /* Fix the reference if the branch got folded */ subtree_branch = subtree_branch->folded_to; xx = conf.left_right ? subtree_branch->cx : subtree_branch->cx - subtree_branch->tw/2; yy = conf.left_right ? subtree_branch->y - subtree_branch->th/2 : subtree_branch->y; if(subtree_branch != rcs->branches[0]) { if(conf.left_right) xx -= conf.branch_connect; else yy -= conf.branch_connect; } for(i = 0; i < rcs->nbranches; i++) move_branch(rcs->branches[i], -xx, -yy); } /* Move everything w.r.t. the top-left margin */ for(i = 0; i < rcs->nbranches; i++) move_branch(rcs->branches[i], conf.margin_left, conf.margin_top); } /* ************************************************************************** * Imagemap functions ************************************************************************** */ static void map_merge_box(rcsfile_t *rcs, FILE *fp, revision_t *fr, revision_t *tr, gdImagePtr im, int x1, int y1, int x2, int y2) { char *href = expand_string(conf.map_merge_href, rcs, tr, tr->rev, fr->rev, NULL); char *alt = expand_string(conf.map_merge_alt, rcs, tr, tr->rev, fr->rev, NULL); const char *htp = conf.html_level == HTMLLEVEL_X ? " /" : ""; if(x1 > 0 && x2 > 0 && y1 > 0 && y2 > 0) fprintf(fp, "\t\n", href, x1, y1, x2, y2, alt, htp); xfree(alt); xfree(href); if(im) { gdImageFilledRectangle(im, x1-2, y1-2, x1+2, y1+2, clr(im, "title_color", NULL, NULL, 0)->id); gdImageFilledRectangle(im, x2-2, y2-2, x2+2, y2+2, clr(im, "tag_color.id", NULL, NULL, 0)->id); gdImageLine(im, x1, y1, x2, y2, clr(im, "title_color", NULL, NULL, 0)->id); } } static void map_merges(rcsfile_t *rcs, FILE *fp, gdImagePtr im) { int i; int tagh2 = get_sheight("Hg", &conf.tag_font) / 2; int bm = conf.branch_margin / 2; for(i = 0; i < rcs->nmerges; i++) { revision_t *fr; revision_t *tr; int x1, x2, y1, y2; switch(rcs->merges[i].type) { case TR_TAG: fr = rcs->merges[i].from.tag->logrev; tr = rcs->merges[i].to.tag->logrev; break; case TR_REVISION: fr = rcs->merges[i].from.rev; tr = rcs->merges[i].to.rev; break; default: continue; } if(!fr || !tr || fr == tr) continue; /* This can happen with detached tags and self-references */ if(conf.left_right) { if(fr->branch == tr->branch) { y1 = fr->y - fr->h/2; y2 = tr->y - tr->h/2; } else { if(fr->y < tr->y) { y1 = fr->y + fr->h/2; y2 = tr->y - tr->h/2; } else { y1 = fr->y - fr->h/2; y2 = tr->y + tr->h/2; } } x1 = fr->cx + fr->w/2; x2 = tr->cx + tr->w/2; } else { if(fr->branch == tr->branch) { x1 = fr->cx - fr->w/2; x2 = tr->cx - tr->w/2; } else { if(fr->cx < tr->cx) { x1 = fr->cx + fr->w/2; x2 = tr->cx - tr->w/2; } else { x1 = fr->cx - fr->w/2; x2 = tr->cx + tr->w/2; } } if(rcs->merges[i].type == TR_TAG) { y1 = fr->y + rcs->merges[i].from.tag->yofs; y2 = tr->y + rcs->merges[i].to.tag->yofs; } else { y1 = fr->y + fr->h/2; y2 = tr->y + tr->h/2; } } if(conf.left_right) { if(fr->branch == tr->branch) { map_merge_box(rcs, fp, fr, tr, im, x1-bm, y1-bm, x1+bm, y1); map_merge_box(rcs, fp, fr, tr, im, x2-bm, y2-bm, x2+bm, y2); } else { if(y1 > y2) { map_merge_box(rcs, fp, fr, tr, im, x1-bm, y1-bm, x1+bm, y1); map_merge_box(rcs, fp, fr, tr, im, x2-bm, y2, x2+bm, y2+bm); } else { map_merge_box(rcs, fp, fr, tr, im, x1-bm, y1, x1+bm, y1+bm); map_merge_box(rcs, fp, fr, tr, im, x2-bm, y2-bm, x2+bm, y2); } } } else { if(fr->branch == tr->branch) { map_merge_box(rcs, fp, fr, tr, im, x1-bm, y1-tagh2, x1, y1+tagh2); map_merge_box(rcs, fp, fr, tr, im, x2-bm, y2-tagh2, x2, y2+tagh2); } else { if(x1 > x2) { map_merge_box(rcs, fp, fr, tr, im, x1-bm, y1-tagh2, x1, y1+tagh2); map_merge_box(rcs, fp, fr, tr, im, x2, y2-tagh2, x2+bm, y2+tagh2); } else { map_merge_box(rcs, fp, fr, tr, im, x1, y1-tagh2, x1+bm, y1+tagh2); map_merge_box(rcs, fp, fr, tr, im, x2-bm, y2-tagh2, x2, y2+tagh2); } } } } } static void make_imagemap(rcsfile_t *rcs, FILE *fp, gdImagePtr im) { int i, j; const char *htp = conf.html_level == HTMLLEVEL_X ? " /" : ""; switch(conf.html_level) { case HTMLLEVEL_4: fprintf(fp, "\n", conf.map_name, conf.map_name); break; case HTMLLEVEL_X: fprintf(fp, "\n", conf.map_name); break; default: fprintf(fp, "\n", conf.map_name); } for(i = 0; i < rcs->nbranches; i++) { branch_t *b = rcs->branches[i]; tag_t *tag = b->ntags ? b->tags[0] : NULL; char *bhref; char *balt; int x1; int x2; int y1; int y2; if(subtree_branch && !b->subtree_draw) continue; bhref = expand_string(conf.map_branch_href, rcs, NULL, b->branch, NULL, tag); balt = expand_string(conf.map_branch_alt, rcs, NULL, b->branch, NULL, tag); if(!b->nfolds) { if(conf.left_right) { x1 = b->cx; y1 = b->y - b->h/2; x2 = b->cx + b->w; y2 = b->y + b->h/2; } else { x1 = b->cx - b->w/2; y1 = b->y; x2 = b->cx + b->w/2; y2 = b->y + b->h; } fprintf(fp, "\t\n", bhref, x1, y1, x2, y2, balt, htp); if(im) { gdImageFilledRectangle(im, x1-2, y1-2, x1+2, y1+2, clr(im, "title_color", NULL, NULL, 0)->id); gdImageFilledRectangle(im, x2-2, y2-2, x2+2, y2+2, clr(im, "tag_color", NULL, NULL, 0)->id); gdImageLine(im, x1, y1, x2, y2, clr(im, "title_color", NULL, NULL, 0)->id); } } else { int yy1, yy2, yy; if(conf.left_right) { x1 = b->cx + conf.branch_lspace; y1 = b->y - b->h/2 + conf.branch_tspace; } else { x1 = b->cx - b->w/2 + conf.branch_lspace; y1 = b->y + conf.branch_tspace; } x2 = x1 + b->w - conf.branch_rspace; yy1 = get_sheight(b->branch->branch, &conf.branch_font); yy2 = get_sheight(b->tags[0]->tag, &conf.branch_tag_font); yy = MAX(yy1, yy2); y2 = y1 + yy; fprintf(fp, "\t\n", bhref, x1, y1, x2, y2, balt, htp); y1 += yy; y2 += yy; for(j = 0; j < b->nfolds; j++) { branch_t *fb = b->folds[j]; tag_t *t = fb->tags[0]; xfree(bhref); xfree(balt); bhref = expand_string(conf.map_branch_href, rcs, NULL, fb->branch, NULL, t); balt = expand_string(conf.map_branch_alt, rcs, NULL, fb->branch, NULL, t); fprintf(fp, "\t\n", bhref, x1, y1, x2, y2, balt, htp); yy1 = get_sheight(fb->branch->branch, &conf.branch_font); yy2 = get_sheight(fb->tags[0]->tag, &conf.branch_tag_font); yy = MAX(yy1, yy2); y1 += yy; y2 += yy; } } for(j = 0; j < b->nrevs; j++) { revision_t *r = b->revs[j]; revision_t* r1; int xoff = 1; int yoff = 1; char *href; char *alt; tag = r->ntags ? r->tags[0] : NULL; href = expand_string(conf.map_rev_href, rcs, r, r->rev, NULL, tag); alt = expand_string(conf.map_rev_alt, rcs, r, r->rev, NULL, tag); if(conf.left_right) { x1 = r->cx; y1 = r->y - r->h/2; x2 = r->cx + r->w; y2 = r->y + r->h/2; } else { x1 = r->cx - r->w/2; y1 = r->y; x2 = r->cx + r->w/2; y2 = r->y + r->h; } fprintf(fp, "\t\n", href, x1, y1, x2, y2, alt, htp); if(im) { gdImageFilledRectangle(im, x1-2, y1-2, x1+2, y1+2, clr(im, "title_color", NULL, NULL, 0)->id); gdImageFilledRectangle(im, x2-2, y2-2, x2+2, y2+2, clr(im, "tag_color", NULL, NULL, 0)->id); gdImageLine(im, x1, y1, x2, y2, clr(im, "title_color", NULL, NULL, 0)->id); } xfree(href); xfree(alt); if(j > 0 || b->branchpoint) { if(j > 0) { r1 = b->revs[j-1]; if(conf.left_right) { yoff = MIN(r->h, r1->h)/4; x1 = conf.upside_down ? r1->cx : r1->cx + r1->w; } else { xoff = MIN(r->w, r1->w)/4; y1 = conf.upside_down ? r1->y : r1->y + r1->h; } } else { r1 = b->branchpoint; if(conf.left_right) { yoff = MIN(r->h, b->h)/4; x1 = conf.upside_down ? b->cx : b->cx + b->w; } else { xoff = MIN(r->w, b->w)/4; y1 = conf.upside_down ? b->y : b->y + b->h; } } if(conf.left_right) { y1 = r->y - yoff; y2 = r->y + yoff; x2 = conf.upside_down ? r->cx + r->w : r->cx; yoff = 0; } else { x1 = r->cx - xoff; x2 = r->cx + xoff; y2 = conf.upside_down ? r->y + r->h : r->y; xoff = 0; } if(x1 > x2) { int tt = x1; x1 = x2; x2 = tt; } if(y1 > y2) { int tt = y1; y1 = y2; y2 = tt; } href = expand_string(conf.map_diff_href, rcs, r, r->rev, r1->rev, tag); alt = expand_string(conf.map_diff_alt, rcs, r, r->rev, r1->rev, tag); fprintf(fp, "\t\n", href, x1+xoff, y1+yoff, x2-xoff, y2-yoff, alt, htp); if(im) { gdImageFilledRectangle(im, x1-2, y1-2, x1+2, y1+2, clr(im, "title_color", NULL, NULL, 0)->id); gdImageFilledRectangle(im, x2-2, y2-2, x2+2, y2+2, clr(im, "tag_color", NULL, NULL, 0)->id); gdImageLine(im, x1, y1, x2, y2, clr(im, "title_color", NULL, NULL, 0)->id); } xfree(href); xfree(alt); } } if(conf.branch_dupbox && b->nrevs) { if(conf.left_right) { x1 = conf.upside_down ? b->cx + b->w - b->tw : b->cx - b->w + b->tw; y1 = b->y - b->h/2; x2 = x1 + b->w; y2 = b->y + b->h/2; } else { x1 = b->cx - b->w/2; y1 = conf.upside_down ? b->y + b->h - b->th : b->y - b->h + b->th; x2 = b->cx + b->w/2; y2 = y1 + b->h; } fprintf(fp, "\t\n", bhref, x1, y1, x2, y2, balt, htp); if(im) { gdImageFilledRectangle(im, x1-2, y1-2, x1+2, y1+2, clr(im, "title_color", NULL, NULL, 0)->id); gdImageFilledRectangle(im, x2-2, y2-2, x2+2, y2+2, clr(im, "tag_color", NULL, NULL, 0)->id); gdImageLine(im, x1, y1, x2, y2, clr(im, "title_color", NULL, NULL, 0)->id); } } xfree(bhref); xfree(balt); } map_merges(rcs, fp, im); fprintf(fp, "\n"); } /* ************************************************************************** * Program entry ************************************************************************** */ static const char usage_str[] = "Usage: cvsgraph [options] \n" " -b Add a branch box at both sides of the trunk (config value is negated)\n" " -c Read alternative config from \n" " -d Enable debug mode at \n" " -h This message\n" " -i Generate an imagemap instead of image\n" " -I Also write the imagemap to \n" " -k Auto stretch the tree (config value is negated)\n" " -M Use as imagemap name\n" " -m Use as cvs module\n" " -o Output to \n" " -O Set option opt to value val\n" " -q Be quiet (i.e. no warnings)\n" " -r Use as cvsroot path\n" " -s Strip untagged revisions (config value is negated)\n" " -S Also strip the first revision (config value is negated)\n" " -u Upside down image (mirror vertically; config value is negated)\n" " -V Print version and exit\n" " -x [34x] Specify level of HTML 3.2 (default), 4.0 or XHTML\n" " -[0-9] Use for expansion\n" ; #define VERSION_STR "1.6.1" #define NOTICE_STR "Copyright (c) 2001-2006 B.Stultiens" static void append_slash(char **path) { int l; assert(path != NULL); assert(*path != NULL); l = strlen(*path); if(!l || (*path)[l-1] == '/') return; *path = xrealloc(*path, l+2); strcat(*path, "/"); } int main(int argc, char *argv[]) { extern int rcs_flex_debug; extern int rcsdebug; int optc; char *confpath = NULL; char *outfile = NULL; char *cvsroot = NULL; char *cvsmodule = NULL; int imagemap = 0; int upsidedown = 0; int bdupbox = 0; int stripuntag = 0; int stripfirst = 0; int autostretch = 0; int htmllevel = 0; char *imgmapname = NULL; char *imgmapfile = NULL; int lose = 0; FILE *fp; char *rcsfilename; rcsfile_t *rcs; gdImagePtr im; while((optc = getopt(argc, argv, "0:1:2:3:4:5:6:7:8:9:bc:d:hI:ikM:m:O:o:qr:SsuVx:")) != EOF) { switch(optc) { case 'b': bdupbox = 1; break; case 'c': confpath = xstrdup(optarg); break; case 'd': debuglevel = strtol(optarg, NULL, 0); break; case 'I': imgmapfile = xstrdup(optarg); break; case 'i': imagemap = 1; break; case 'k': autostretch = 1; break; case 'M': imgmapname = xstrdup(optarg); break; case 'm': cvsmodule = xstrdup(optarg); break; case 'O': stack_option(optarg); break; case 'o': outfile = xstrdup(optarg); break; case 'q': quiet = 1; break; case 'r': cvsroot = xstrdup(optarg); break; case 'S': stripfirst = 1; break; case 's': stripuntag = 1; break; case 'u': upsidedown = 1; break; case 'V': fprintf(stdout, "cvsgraph v%s, %s\n", VERSION_STR, NOTICE_STR); return 0; case 'x': switch(optarg[0]) { case '3': htmllevel = HTMLLEVEL_3; break; case '4': htmllevel = HTMLLEVEL_4; break; case 'x': htmllevel = HTMLLEVEL_X; break; default: fprintf(stderr, "Invalid HTML level in -x\n"); lose++; } break; case 'h': fprintf(stdout, "%s", usage_str); return 0; default: if(isdigit(optc)) { conf.expand[optc-'0'] = xstrdup(optarg); } else lose++; } } if(lose) { fprintf(stderr, "%s", usage_str); return 1; } if(debuglevel) { setvbuf(stdout, NULL, 0, _IONBF); setvbuf(stderr, NULL, 0, _IONBF); } rcs_flex_debug = (debuglevel & DEBUG_RCS_LEX) != 0; rcsdebug = (debuglevel & DEBUG_RCS_YACC) != 0; /* Set defaults */ conf.tag_font.gdfont = gdFontTiny; conf.rev_font.gdfont = gdFontTiny; conf.branch_font.gdfont = gdFontTiny; conf.branch_tag_font.gdfont = gdFontTiny; conf.title_font.gdfont = gdFontTiny; conf.rev_text_font.gdfont = gdFontTiny; conf.msg_font.gdfont = gdFontTiny; conf.anti_alias = 1; conf.thick_lines = 1; conf.branch_fold = 1; conf.cvsroot = xstrdup(""); conf.cvsmodule = xstrdup(""); conf.date_format = xstrdup("%d-%b-%Y %H:%M:%S"); conf.title = xstrdup(""); conf.map_name = xstrdup("CvsGraphImageMap"); conf.map_branch_href = xstrdup("href=\"unset: conf.map_branch_href\""); conf.map_branch_alt = xstrdup("alt=\"%B\""); conf.map_rev_href = xstrdup("href=\"unset: conf.map_rev_href\""); conf.map_rev_alt = xstrdup("alt=\"%R\""); conf.map_diff_href = xstrdup("href=\"unset: conf.map_diff_href\""); conf.map_diff_alt = xstrdup("alt=\"%P <-> %R\""); conf.map_merge_href = xstrdup("href=\"unset: conf.map_merge_href\""); conf.map_merge_alt = xstrdup("alt=\"%P <-> %R\""); conf.rev_text.str = xstrdup("%d"); conf.rev_idtext.str = xstrdup("%R"); conf.branch_subtree = xstrdup(""); conf.tag_ignore = xstrdup(""); conf.merge_from.n = 0; conf.merge_from.strs = NULL; conf.merge_to.n = 0; conf.merge_to.strs = NULL; conf.merge_arrows = 1; conf.merge_cvsnt = 1; conf.arrow_width = ARROW_WIDTH; conf.arrow_length = ARROW_LENGTH; conf.color_bg = white_color; conf.branch_bgcolor = white_color; conf.branch_color = black_color; conf.branch_tag_color = black_color; conf.rev_color = black_color; conf.rev_bgcolor = white_color; conf.merge_color.n = 0; conf.merge_color.clrs = NULL; conf.merge_cvsnt_color = black_color; conf.tag_color = black_color; conf.title_color = black_color; conf.rev_text_color = black_color; conf.msg_color = black_color; conf.image_quality = 100; conf.image_compress = -1; /* Use default zlib setting */ conf.rev_maxline = -1; /* Checked later to set to default */ read_config(confpath); if(conf.rev_maxline == -1) conf.rev_maxline = 5 * conf.rev_minline; /* Set overrides */ if(cvsroot) conf.cvsroot = cvsroot; if(cvsmodule) conf.cvsmodule = cvsmodule; if(imgmapname) conf.map_name = imgmapname; if(upsidedown) conf.upside_down = !conf.upside_down; if(bdupbox) conf.branch_dupbox = !conf.branch_dupbox; if(stripuntag) conf.strip_untagged = !conf.strip_untagged; if(stripfirst) conf.strip_first_rev = !conf.strip_first_rev; if(autostretch) conf.auto_stretch = !conf.auto_stretch; if(htmllevel) conf.html_level = htmllevel; if(conf.rev_minline >= conf.rev_maxline) { if(conf.auto_stretch) stack_msg(MSG_WARN, "Auto stretch is only possible if rev_minline < rev_maxline"); conf.auto_stretch = 0; } if(conf.thick_lines < 1) conf.thick_lines = 1; if(conf.thick_lines > 11) conf.thick_lines = 11; if(conf.image_quality < 0 || conf.image_quality > 100) { stack_msg(MSG_WARN, "JPEG quality (image_quality) must be between 0 and 100"); conf.image_quality = 100; } if(conf.image_compress < -1 || conf.image_compress > 9) { stack_msg(MSG_WARN, "PNG compression (image_compress) must be between -1 and 9"); conf.image_compress = -1; } append_slash(&conf.cvsroot); append_slash(&conf.cvsmodule); if(optind >= argc) { #ifdef __WIN32__ /* Bad hack for DOS/Windows */ if(setmode(fileno(stdin), O_BINARY) == -1) { perror("Set binary mode for stdin"); return 1; } #endif rcsfilename = NULL; } else rcsfilename = argv[optind]; rcs = get_rcsfile(conf.cvsroot, conf.cvsmodule, rcsfilename); if(!rcs) return 1; if(debuglevel & DEBUG_RCS_FILE) dump_rcsfile(rcs); if(!reorganise_branches(rcs)) return 1; assign_tags(rcs); find_merges(rcs); find_merges_cvsnt(rcs); if(outfile) { if((fp = fopen(outfile, "wb")) == NULL) { perror(outfile); return 1; } } else { fp = stdout; #ifdef __WIN32__ /* Bad hack for DOS/Windows */ if(setmode(fileno(fp), O_BINARY) == -1) { perror("Set binary mode for stdout"); return 1; } #endif } make_layout(rcs); if(!imagemap) { /* Create an image */ im = make_image(rcs); if(conf.image_interlace) gdImageInterlace(im, 1); #ifdef DEBUG_IMAGEMAP { FILE *nulfile = fopen("/dev/null", "w"); make_imagemap(rcs, nulfile, im); fclose(nulfile); } #endif switch(conf.image_type) { #ifdef HAVE_IMAGE_GIF # ifndef HAVE_IMAGE_PNG default: # endif case IMAGE_GIF: gdImageGif(im, fp); break; #endif #ifdef HAVE_IMAGE_PNG default: case IMAGE_PNG: #ifdef HAVE_GDIMAGEPNGEX gdImagePngEx(im, fp, conf.image_compress); #else gdImagePng(im, fp); #endif break; #endif #ifdef HAVE_IMAGE_JPEG # if !defined(HAVE_IMAGE_GIF) && !defined(HAVE_IMAGE_PNG) default: # endif case IMAGE_JPEG: gdImageJpeg(im, fp, conf.image_quality); break; #endif } gdImageDestroy(im); } else { /* Create an imagemap */ make_imagemap(rcs, fp, NULL); } /* Also create imagemap to file if requested */ if(imgmapfile) { FILE *ifp = fopen(imgmapfile, "wb"); if(!ifp) { perror(imgmapfile); return 1; } make_imagemap(rcs, ifp, NULL); fclose(ifp); } if(outfile) fclose(fp); return 0; }