Index: src/default_css.txt ================================================================== --- src/default_css.txt +++ src/default_css.txt @@ -189,14 +189,14 @@ border-left: 7px solid #600000; } .tl-line.warp { background: #600000; } -.tl-line.dotted { +.tl-line.dotted.v { width: 0px; - border-top: 0px dotted #888; - border-left: 2px dotted #888; + border-left-width: 2px; + border-left-style: dotted; background: rgba(255,255,255,0); } span.tagDsp { font-weight: bold; } Index: src/graph.c ================================================================== --- src/graph.c +++ src/graph.c @@ -18,10 +18,30 @@ ** This file contains code to compute a revision history graph. */ #include "config.h" #include "graph.h" #include + +/* Notes: +** +** The graph is laid out in 1 or more "rails". A "rail" is a vertical +** band in the graph in which one can place nodes or arrows connecting +** nodes. There can be between 1 and GR_MAX_RAIL rails. If the graph +** is to complex to be displayed in GR_MAX_RAIL rails, it is omitted. +** +** A "riser" is the thick line that comes out of the top of a node and +** goes up to the next node on the branch, or to the top of the screen. +** A "descender" is a thick line that comes out of the bottom of a node +** and proceeds down to the bottom of the page. +** +** Invoke graph_init() to create a new GraphContext object. Then +** call graph_add_row() to add nodes, one by one, to the graph. +** Nodes must be added in display order, from top to bottom. +** Then invoke graph_render() to run the layout algorithm. The +** layout algorithm computes which rails all of the nodes sit on, and +** the rails used for merge arrows. +*/ #if INTERFACE #define GR_MAX_RAIL 40 /* Max number of "rails" to display */ @@ -33,11 +53,11 @@ ** The nParent field is -1 for entires that do not participate in the graph ** but which are included just so that we can capture their background color. */ struct GraphRow { int rid; /* The rid for the check-in */ - i8 nParent; /* Number of parents. -1 for technote lines */ + i8 nParent; /* Number of parents. */ i8 nCherrypick; /* Subset of aParent that are cherrypicks */ i8 nNonCherrypick; /* Number of non-cherrypick parents */ int *aParent; /* Array of parents. 0 element is primary .*/ char *zBranch; /* Branch name */ char *zBgClr; /* Background Color */ @@ -44,11 +64,11 @@ char zUuid[HNAME_MAX+1]; /* Check-in for file ID */ GraphRow *pNext; /* Next row down in the list of all rows */ GraphRow *pPrev; /* Previous row */ - int idx; /* Row index. First is 1. 0 used for "none" */ + int idx; /* Row index. Top row is smallest. */ int idxTop; /* Direct descendent highest up on the graph */ GraphRow *pChild; /* Child immediately above this node */ u8 isDup; /* True if this is duplicate of a prior entry */ u8 isLeaf; /* True if this is a leaf node */ u8 isStepParent; /* pChild is actually a step-child */ @@ -61,11 +81,10 @@ int aiRiser[GR_MAX_RAIL]; /* Risers from this node to a higher row. */ int mergeUpto; /* Draw the mergeOut rail up to this level */ int cherrypickUpto; /* Continue the mergeOut rail up to here */ u64 mergeDown; /* Draw merge lines up from bottom of graph */ u64 cherrypickDown; /* Draw cherrypick lines up from bottom */ - u64 railInUse; /* Mask of occupied rails at this row */ }; /* Context while building a graph */ @@ -83,10 +102,17 @@ #endif /* The N-th bit */ #define BIT(N) (((u64)1)<<(N)) + +/* +** Number of rows before and answer a node with a riser or descender +** that goes off-screen before we can reuse that rail. +*/ +#define RISER_MARGIN 4 + /* ** Malloc for zeroed space. Panic if unable to provide the ** requested space. */ @@ -246,11 +272,11 @@ for(pRow=p->pFirst; pRow && pRow->idxpNext){} while( pRow && pRow->idx<=btm ){ inUseMask |= pRow->railInUse; pRow = pRow->pNext; } - for(i=0; i<32; i++){ + for(i=0; iiRail; GraphRow *pCurrent; GraphRow *pPrior; u64 mask = ((u64)1)<idx > pCurrent->idx ){ pPrior->railInUse |= mask; pPrior = pPrior->pPrev; assert( pPrior!=0 ); } + } + /* Mask of additional rows for the riser to infinity */ + if( !pPrior->isLeaf && (tmFlags & TIMELINE_DISJOINT)==0 ){ + int n = RISER_MARGIN; + GraphRow *p; + for(p=pPrior; p && (n--)>0; p=p->pPrev){ + p->railInUse |= mask; + } } } /* ** Create a merge-arrow riser going from pParent up to pChild. @@ -305,20 +339,21 @@ u64 mask; GraphRow *pLoop; if( pParent->mergeOut<0 ){ u = pParent->aiRiser[pParent->iRail]; - if( u>=0 && uidx ){ + if( u>0 && uidx ){ /* The thick arrow up to the next primary child of pDesc goes ** further up than the thin merge arrow riser, so draw them both ** on the same rail. */ pParent->mergeOut = pParent->iRail; - }else{ + }else{ /* The thin merge arrow riser is taller than the thick primary ** child riser, so use separate rails. */ int iTarget = pParent->iRail; - pParent->mergeOut = findFreeRail(p, pChild->idx, pParent->idx-1, iTarget); + int iBtm = pParent->idx - (u==0 ? RISER_MARGIN : 1); + pParent->mergeOut = findFreeRail(p, pChild->idx, iBtm, iTarget); mask = BIT(pParent->mergeOut); for(pLoop=pChild->pNext; pLoop && pLoop->rid!=pParent->rid; pLoop=pLoop->pNext){ pLoop->railInUse |= mask; } @@ -353,16 +388,18 @@ } } } /* -** Draw a riser from pRow to the top of the graph +** Draw a riser from pRow upward to indicate that it is going +** to a node that is off the graph to the top. */ static void riser_to_top(GraphRow *pRow){ u64 mask = BIT(pRow->iRail); + int n = RISER_MARGIN; pRow->aiRiser[pRow->iRail] = 0; - while( pRow ){ + while( pRow && (n--)>0 ){ pRow->railInUse |= mask; pRow = pRow->pPrev; } } @@ -378,10 +415,11 @@ ** The tmFlags parameter is zero or more of the TIMELINE_* constants. ** Only the following are honored: ** ** TIMELINE_DISJOINT: Omit descenders ** TIMELINE_FILLGAPS: Use step-children +** TIMELINE_XMERGE: Omit off-graph merge lines */ void graph_finish(GraphContext *p, u32 tmFlags){ GraphRow *pRow, *pDesc, *pDup, *pLoop, *pParent; int i, j; u64 mask; @@ -421,11 +459,11 @@ ** A merge parent is a prior check-in from which changes were merged into ** the current check-in. If a merge parent is not in the visible section ** of this graph, then no arrows will be drawn for it, so remove it from ** the aParent[] array. */ - if( omitDescenders ){ + if( (tmFlags & (TIMELINE_DISJOINT|TIMELINE_XMERGE))!=0 ){ for(pRow=p->pFirst; pRow; pRow=pRow->pNext){ for(i=1; inParent; i++){ if( hashFind(p, pRow->aParent[i])==0 ){ memmove(pRow->aParent+i, pRow->aParent+i+1, sizeof(pRow->aParent[0])*(pRow->nParent-i-1)); @@ -477,71 +515,72 @@ if( pRow->nParent<=0 ) continue; /* Root node */ pParent = hashFind(p, pRow->aParent[0]); if( pParent==0 ) continue; /* Parent off-screen */ if( pParent->zBranch!=pRow->zBranch ) continue; /* Different branch */ if( pParent->idx <= pRow->idx ){ - pParent->timeWarp = 1; - continue; /* Time-warp */ - } - if( pRow->idxTop < pParent->idxTop ){ + pParent->timeWarp = 1; + }else if( pRow->idx < pParent->idx ){ pParent->pChild = pRow; - pParent->idxTop = pRow->idxTop; } } if( tmFlags & TIMELINE_FILLGAPS ){ - /* If a node has no pChild, and there is a later node (a node higher - ** up on the graph) in the same branch that has no parent, then make - ** the lower node a step-child of the upper node. + /* If a node has no pChild but there is a node higher up in the graph + ** that is in the same branch and that other node has no parent in + ** the graph, the lower node a step-child of the upper node. This will + ** be represented on the graph by a thick dotted line without an arrowhead. */ for(pRow=p->pFirst; pRow; pRow=pRow->pNext){ if( pRow->pChild ) continue; for(pLoop=pRow->pPrev; pLoop; pLoop=pLoop->pPrev){ if( pLoop->nParent>0 && pLoop->zBranch==pRow->zBranch && hashFind(p,pLoop->aParent[0])==0 ){ pRow->pChild = pLoop; - pRow->idxTop = pLoop->idxTop; pRow->isStepParent = 1; pLoop->aParent[0] = pRow->rid; break; } } } } + + /* Set the idxTop values for all entries. The idxTop value is the + ** "idx" value for the top entry in its stack of children. + */ + for(pRow=p->pFirst; pRow; pRow=pRow->pNext){ + GraphRow *pChild = pRow->pChild; + if( pChild && pRow->idxTop>pChild->idxTop ){ + pRow->idxTop = pChild->idxTop; + } + } /* Identify rows where the primary parent is off screen. Assign - ** each to a rail and draw descenders to the bottom of the screen. + ** each to a rail and draw descenders downward. ** ** Strive to put the "trunk" branch on far left. */ zTrunk = persistBranchName(p, "trunk"); for(i=0; i<2; i++){ for(pRow=p->pLast; pRow; pRow=pRow->pPrev){ + if( i==0 && pRow->zBranch!=zTrunk ) continue; + if( pRow->iRail>=0 ) continue; if( pRow->isDup ) continue; if( pRow->nParent<0 ) continue; - if( i==0 ){ - if( pRow->zBranch!=zTrunk ) continue; - }else { - if( pRow->iRail>=0 ) continue; - } if( pRow->nParent==0 || hashFind(p,pRow->aParent[0])==0 ){ - if( omitDescenders ){ - pRow->iRail = findFreeRail(p, pRow->idxTop, pRow->idx, 0); - }else{ - pRow->iRail = ++p->mxRail; - } + pRow->iRail = findFreeRail(p, pRow->idxTop, pRow->idx+RISER_MARGIN, 0); if( p->mxRail>=GR_MAX_RAIL ) return; mask = BIT(pRow->iRail); if( !omitDescenders ){ + int n = RISER_MARGIN; pRow->bDescender = pRow->nParent>0; - for(pLoop=pRow; pLoop; pLoop=pLoop->pNext){ + for(pLoop=pRow; pLoop && (n--)>0; pLoop=pLoop->pNext){ pLoop->railInUse |= mask; } } - assignChildrenToRail(pRow); + assignChildrenToRail(pRow, tmFlags); } } } /* Assign rails to all rows that are still unassigned. @@ -570,11 +609,12 @@ continue; } if( pParent->idx>pRow->idx ){ /* Common case: Child occurs after parent and is above the ** parent in the timeline */ - pRow->iRail = findFreeRail(p, 0, pParent->idx, pParent->iRail); + pRow->iRail = findFreeRail(p, pRow->idxTop, pParent->idx, + pParent->iRail); if( p->mxRail>=GR_MAX_RAIL ) return; pParent->aiRiser[pRow->iRail] = pRow->idx; }else{ /* Timewarp case: Child occurs earlier in time than parent and ** appears below the parent in the timeline. */ @@ -591,11 +631,11 @@ } } mask = BIT(pRow->iRail); pRow->railInUse |= mask; if( pRow->pChild ){ - assignChildrenToRail(pRow); + assignChildrenToRail(pRow, tmFlags); }else if( !omitDescenders && count_nonbranch_children(pRow->rid)!=0 ){ if( !pRow->timeWarp ) riser_to_top(pRow); } if( pParent ){ for(pLoop=pParent->pPrev; pLoop && pLoop!=pRow; pLoop=pLoop->pPrev){ Index: src/graph.js ================================================================== --- src/graph.js +++ src/graph.js @@ -183,11 +183,11 @@ cls += "v"; }else{ y1 = y0+elem.w; cls += "h"; } - drawBox(cls,color,x0,y0,x1,y1); + return drawBox(cls,color,x0,y0,x1,y1); } function drawUpArrow(from,to,color){ var y = to.y + node.h; var arrowSpace = from.y - y + (!from.id || from.r!=to.r ? node.h/2 : 0); var arw = arrowSpace < arrow.h*1.5 ? arrowSmall : arrow; @@ -197,15 +197,16 @@ drawLine(line,color,x,y0,null,y1); x = to.x + (node.w-arw.w)/2; var n = drawBox(arw.cls,null,x,y); if(color) n.style.borderBottomColor = color; } - function drawUpDotted(from,to,color){ + function drawDotted(from,to,color){ var x = to.x + (node.w-line.w)/2; var y0 = from.y + node.h/2; var y1 = Math.ceil(to.y + node.h); - drawLine(dotLine,color,x,y0,null,y1); + var n = drawLine(dotLine,null,x,y0,null,y1) + if( color ) n.style.borderColor = color } /* Draw thin horizontal or vertical lines representing merges */ function drawMergeLine(x0,y0,x1,y1){ drawLine(mLine,null,x0,y0,x1,y1); } @@ -243,29 +244,51 @@ e = document.getElementById("md"+p.id); if(e) e.style.backgroundColor = p.bg; } if( p.r<0 ) return; if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg); - if( p.sb>0 ) drawUpDotted(p,tx.rowinfo[p.sb-tx.iTopRow],null); + if( p.sb>0 ) drawDotted(p,tx.rowinfo[p.sb-tx.iTopRow],p.fg); var cls = node.cls; if( p.hasOwnProperty('mi') && p.mi.length ) cls += " merge"; if( p.f&1 ) cls += " leaf"; var n = drawBox(cls,p.bg,p.x,p.y); n.id = "tln"+p.id; n.onclick = clickOnNode; n.style.zIndex = 10; if( !tx.omitDescenders ){ - if( p.u==0 ) drawUpArrow(p,{x: p.x, y: -node.h},p.fg); - if( p.hasOwnProperty('d') ) drawUpArrow({x: p.x, y: btm-node.h/2},p,p.fg); + if( p.u==0 ){ + if( p.hasOwnProperty('mo') && p.r==p.mo ){ + var ix = p.hasOwnProperty('cu') ? p.cu : p.mu; + var top = tx.rowinfo[ix-tx.iTopRow] + drawUpArrow(p,{x: p.x, y: top.y-node.h}, p.fg); + }else if( p.y>100 ){ + drawUpArrow(p,{x: p.x, y: p.y-50}, p.fg); + }else{ + drawUpArrow(p,{x: p.x, y: 0},p.fg); + } + } + if( p.hasOwnProperty('d') ){ + if( p.y + 150 >= btm ){ + drawUpArrow({x: p.x, y: btm - node.h/2},p,p.fg); + }else{ + drawUpArrow({x: p.x, y: p.y+50},p,p.fg); + drawDotted({x: p.x, y: p.y+63},{x: p.x, y: p.y+50-node.h/2},p.fg); + } + } } if( p.hasOwnProperty('mo') ){ var x0 = p.x + node.w/2; var x1 = p.mo*railPitch + node.w/2; var u = tx.rowinfo[p.mu-tx.iTopRow]; var y1 = miLineY(u); - if( p.u<0 || p.mo!=p.r ){ - x1 += mergeLines[p.mo] = -mLine.w/2; + if( p.u<=0 || p.mo!=p.r ){ + if( p.u==0 && p.mo==p.r ){ + mergeLines[p.mo] = u.r + @ \ + @ blob_zero(&comment); while( db_step(pQuery)==SQLITE_ROW ){ int rid = db_column_int(pQuery, 0); const char *zUuid = db_column_text(pQuery, 1); int isLeaf = db_column_int(pQuery, 5); @@ -867,11 +868,12 @@ ** is iTopRow and numbers increase moving down the timeline. ** bg: The background color for this row ** r: The "rail" that the node for this row sits on. The left-most ** rail is 0 and the number increases to the right. ** d: If exists and true then there is a "descender" - an arrow - ** coming from the bottom of the page straight up to this node. + ** coming from the bottom of the page or further down on the page + ** straight up to this node. ** mo: "merge-out". If it exists, this is the rail position ** for the upward portion of a merge arrow. The merge arrow goes as ** a solid normal merge line up to the row identified by "mu" and ** then as a dashed cherrypick merge line up further to "cu". ** If this value is omitted if there are no merge children. @@ -879,11 +881,12 @@ ** Only exists if "mo" exists. ** cu: Extend the mu merge arrow up to this row as a cherrypick ** merge line, if this value exists. ** u: Draw a thick child-line out of the top of this node and up to ** the node with an id equal to this value. 0 if it is straight to - ** the top of the page, -1 if there is no thick-line riser. + ** the top of the page or just up a little wasy, -1 if there is + ** no thick-line riser (if the node is a leaf). ** sb: Draw a dotted child-line out of the top of this node up to the ** node with the id equal to the value. This is like "u" except ** that the line is dotted instead of solid and has no arrow. ** Mnemonic: "Same Branch". ** f: 0x01: a leaf node. @@ -1494,11 +1497,11 @@ ** nd Do not highlight the focus check-in ** v Show details of files changed ** f=CHECKIN Show family (immediate parents and children) of CHECKIN ** from=CHECKIN Path from... ** to=CHECKIN ... to this -** shorest ... show only the shortest path +** shortest ... show only the shortest path ** rel ... also show related checkins ** uf=FILE_HASH Show only check-ins that contain the given file version ** chng=GLOBLIST Show only check-ins that involve changes to a file whose ** name matches one of the comma-separate GLOBLIST ** brbg Background color from branch name @@ -1691,12 +1694,12 @@ tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK; }else{ tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK; } if( related ){ - tmFlags |= TIMELINE_FILLGAPS; -// tmFlags &= ~TIMELINE_DISJOINT; + tmFlags |= TIMELINE_FILLGAPS | TIMELINE_XMERGE; + tmFlags &= ~TIMELINE_DISJOINT; } if( PB("ncp") ){ tmFlags &= ~TIMELINE_CHPICK; } if( PB("ng") || zSearch!=0 ){ @@ -1858,10 +1861,11 @@ db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related"); } blob_append_sql(&sql, " AND event.objid IN pathnode"); addFileGlobExclusion(zChng, &sql); tmFlags |= TIMELINE_DISJOINT; + tmFlags &= ~TIMELINE_CHPICK; db_multi_exec("%s", blob_sql_text(&sql)); if( advancedMenu ){ style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0); } blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath); @@ -1879,11 +1883,11 @@ }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){ /* If p= or d= is present, ignore all other parameters other than n= */ char *zUuid; int np, nd; - tmFlags |= TIMELINE_DISJOINT; + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; if( p_rid && d_rid ){ if( p_rid!=d_rid ) p_rid = d_rid; if( P("n")==0 ) nEntry = 10; } db_multi_exec( @@ -1947,11 +1951,11 @@ db_multi_exec("%s", blob_sql_text(&sql)); if( useDividers ) selectedRid = f_rid; blob_appendf(&desc, "Parents and children of check-in "); zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid); blob_appendf(&desc, "%z[%S]", href("%R/info/%!S", zUuid), zUuid); - tmFlags |= TIMELINE_DISJOINT; + tmFlags |= TIMELINE_XMERGE; if( advancedMenu ){ style_submenu_checkbox("unhide", "Unhide", 0, 0); style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0); } }else{ @@ -1959,13 +1963,14 @@ int n; const char *zEType = "event"; char *zDate; Blob cond; blob_zero(&cond); + tmFlags |= TIMELINE_FILLGAPS; if( zChng && *zChng ){ addFileGlobExclusion(zChng, &cond); - tmFlags |= TIMELINE_DISJOINT; + tmFlags |= TIMELINE_XMERGE; } if( zUses ){ blob_append_sql(&cond, " AND event.objid IN usesfile "); } if( renameOnly ){ @@ -2220,15 +2225,15 @@ } if( zUses ){ char *zFilenames = names_of_file(zUses); blob_appendf(&desc, " using file %s version %z%S", zFilenames, href("%R/artifact/%!S",zUses), zUses); - tmFlags |= TIMELINE_DISJOINT; + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; } if( renameOnly ){ blob_appendf(&desc, " that contain filename changes"); - tmFlags |= TIMELINE_DISJOINT|TIMELINE_FRENAMES; + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; } if( forkOnly ){ blob_appendf(&desc, " associated with forks"); tmFlags |= TIMELINE_DISJOINT; } @@ -2240,11 +2245,11 @@ blob_appendf(&desc, " that participate in a cherrypick merge"); tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT; } if( zUser ){ blob_appendf(&desc, " by user %h", zUser); - tmFlags |= TIMELINE_DISJOINT; + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; } if( zTagSql ){ if( matchStyle==MS_EXACT ){ if( related ){ blob_appendf(&desc, " related to %h", zMatchDesc); @@ -2256,11 +2261,11 @@ blob_appendf(&desc, " related to tags matching %h", zMatchDesc); }else{ blob_appendf(&desc, " with tags matching %h", zMatchDesc); } } - tmFlags |= TIMELINE_DISJOINT; + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; } addFileGlobDescription(zChng, &desc); if( rAfter>0.0 ){ if( rBefore>0.0 ){ blob_appendf(&desc, " occurring between %h and %h.
",