Index: src/default_css.txt ================================================================== --- src/default_css.txt +++ src/default_css.txt @@ -195,10 +195,25 @@ width: 0px; border-left-width: 2px; border-left-style: dotted; background: rgba(255,255,255,0); } +.tl-tooltip { + background-color: #fecd4b; + color: black; + text-align: center; + padding: 5px 1em; + border: 1px solid black; + border-radius: 6px; + position: absolute; + z-index: 100; +} +.tl-tooltip a { + background-color: #fecd4b; + color: black; +} + span.tagDsp { font-weight: bold; } span.wikiError { font-weight: bold; Index: src/export.c ================================================================== --- src/export.c +++ src/export.c @@ -1061,10 +1061,11 @@ char *zMark; /* The Git-name of the check-in */ Blob sql; /* String of SQL for part of the query */ Blob comment; /* The comment text for the check-in */ int nErr = 0; /* Number of errors */ int bPhantomOk; /* True if phantom files should be ignored */ + char buf[24]; pMan = manifest_get(rid, CFTYPE_MANIFEST, 0); if( pMan==0 ){ /* Must be a phantom. Return without doing anything, and in particular ** without creating a mark for this check-in. */ @@ -1134,13 +1135,15 @@ fprintf(xCmd, "commit refs/heads/%s\n", zBranch); fossil_free(zBranch); zMark = gitmirror_find_mark(zUuid,0,1); fprintf(xCmd, "mark %s\n", zMark); fossil_free(zMark); - fprintf(xCmd, "committer %s <%s@noemail.net> %lld +0000\n", - pMan->zUser, pMan->zUser, + sqlite3_snprintf(sizeof(buf), buf, "%lld", (sqlite3_int64)((pMan->rDate-2440587.5)*86400.0) + ); + fprintf(xCmd, "committer %s <%s@noemail.net> %s +0000\n", + pMan->zUser, pMan->zUser, buf ); blob_init(&comment, pMan->zComment, -1); if( blob_size(&comment)==0 ){ blob_append(&comment, "(no comment)", -1); } Index: src/graph.js ================================================================== --- src/graph.js +++ src/graph.js @@ -72,18 +72,26 @@ style.textContent = css; document.querySelector("head").appendChild(style); } amendCssOnce = 0; } +var tooltipObj = document.createElement("span"); +tooltipObj.className = "tl-tooltip"; +tooltipObj.style.visibility = "hidden"; +document.getElementsByClassName("content")[0].appendChild(tooltipObj); +/* Construct that graph corresponding to the timeline-data-N object */ function TimelineGraph(tx){ var topObj = document.getElementById("timelineTable"+tx.iTableId); amendCss(tx.circleNodes, tx.showArrowheads); + topObj.onclick = clickOnGraph + topObj.ondblclick = dblclickOnGraph var canvasDiv; var railPitch; var mergeOffset; var node, arrow, arrowSmall, line, mArrow, mLine, wArrow, wLine; + function initGraph(){ var parent = topObj.rows[0].cells[1]; parent.style.verticalAlign = "top"; canvasDiv = document.createElement("div"); canvasDiv.className = "tl-canvas"; @@ -163,17 +171,22 @@ n.className = "tl-"+cls; canvasDiv.appendChild(n); return n; } function absoluteY(obj){ - var top = 0; - if( obj.offsetParent ){ - do{ - top += obj.offsetTop; - }while( obj = obj.offsetParent ); - } - return top; + var y = 0; + do{ + y += obj.offsetTop; + }while( obj = obj.offsetParent ); + return y; + } + function absoluteX(obj){ + var x = 0; + do{ + x += obj.offsetLeft; + }while( obj = obj.offsetParent ); + return x; } function miLineY(p){ return p.y + node.h - mLine.w - 1; } function drawLine(elem,color,x0,y0,x1,y1){ @@ -185,28 +198,34 @@ y1 = y0+elem.w; cls += "h"; } return drawBox(cls,color,x0,y0,x1,y1); } - function drawUpArrow(from,to,color){ + function drawUpArrow(from,to,color,id){ 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; var x = to.x + (node.w-line.w)/2; var y0 = from.y + node.h/2; var y1 = Math.ceil(to.y + node.h + arw.h/2); - drawLine(line,color,x,y0,null,y1); + var n = drawLine(line,color,x,y0,null,y1); + addToolTip(n,id) x = to.x + (node.w-arw.w)/2; - var n = drawBox(arw.cls,null,x,y); + n = drawBox(arw.cls,null,x,y); if(color) n.style.borderBottomColor = color; + addToolTip(n,id) } - function drawDotted(from,to,color){ + function drawDotted(from,to,color,id){ var x = to.x + (node.w-line.w)/2; var y0 = from.y + node.h/2; var y1 = Math.ceil(to.y + node.h); var n = drawLine(dotLine,null,x,y0,null,y1) if( color ) n.style.borderColor = color + addToolTip(n,id) + } + function addToolTip(n,id){ + if( id ) n.setAttribute("data-ix",id-tx.iTopRow) } /* Draw thin horizontal or vertical lines representing merges */ function drawMergeLine(x0,y0,x1,y1){ drawLine(mLine,null,x0,y0,x1,y1); } @@ -243,37 +262,39 @@ if(e) e.style.backgroundColor = p.bg; 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 ) drawDotted(p,tx.rowinfo[p.sb-tx.iTopRow],p.fg); + if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg,p.id); + if( p.sb>0 ) drawDotted(p,tx.rowinfo[p.sb-tx.iTopRow],p.fg,p.id); 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; + addToolTip(n,p.id) n.onclick = clickOnNode; + n.ondblclick = dblclickOnNode; n.style.zIndex = 10; if( !tx.omitDescenders ){ 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); + drawUpArrow(p,{x: p.x, y: top.y-node.h}, p.fg, p.id); }else if( p.y>100 ){ - drawUpArrow(p,{x: p.x, y: p.y-50}, p.fg); + drawUpArrow(p,{x: p.x, y: p.y-50}, p.fg, p.id); }else{ - drawUpArrow(p,{x: p.x, y: 0},p.fg); + drawUpArrow(p,{x: p.x, y: 0},p.fg, p.id); } } if( p.hasOwnProperty('d') ){ if( p.y + 150 >= btm ){ - drawUpArrow({x: p.x, y: btm - node.h/2},p,p.fg); + drawUpArrow({x: p.x, y: btm - node.h/2},p,p.fg,p.id); }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); + drawUpArrow({x: p.x, y: p.y+50},p,p.fg,p.id); + drawDotted({x: p.x, y: p.y+63},{x: p.x, y: p.y+50-node.h/2},p.fg,p.id); } } } if( p.hasOwnProperty('mo') ){ var x0 = p.x + node.w/2; @@ -326,11 +347,11 @@ } var y0 = p.y + (node.h-line.w)/2; var u = tx.rowinfo[p.au[i+1]-tx.iTopRow]; if( u.id=0; i-- ){ drawNode(tx.rowinfo[i], btm); } } var selRow; - function clickOnNode(){ + function clickOnNode(e){ + tooltipObj.style.display = "none" var p = tx.rowinfo[parseInt(this.id.match(/\d+$/)[0], 10)-tx.iTopRow]; if( !selRow ){ selRow = p; this.className += " sel"; canvasDiv.className += " sel"; @@ -407,10 +429,72 @@ location.href=tx.baseUrl + "/fdiff?v1="+selRow.h+"&v2="+p.h }else{ location.href=tx.baseUrl + "/vdiff?from="+selRow.h+"&to="+p.h } } + e.stopPropagation() + } + function dblclickOnNode(e){ + var p = tx.rowinfo[parseInt(this.id.match(/\d+$/)[0], 10)-tx.iTopRow]; + window.location.href = tx.baseUrl+"/info/"+p.h + e.stopPropagation() + } + function findTxIndex(e){ + /* Look at all the graph elements. If any graph elements that is near + ** the click-point "e" and has a "data-ix" attribute, then return + ** the value of that attribute. Otherwise return -1 */ + var x = e.x + window.pageXOffset - absoluteX(canvasDiv); + var y = e.y + window.pageYOffset - absoluteY(canvasDiv); + var aNode = canvasDiv.childNodes + var nNode = aNode.length; + var i; + for(i=0;in.offsetLeft+n.offsetWidth+5 ) continue; + if( yn.offsetTop+n.offsetHeight ) continue; + return n.getAttribute("data-ix") + } + return -1 + } + /* Compute the hyperlink for the branch graph for tx.rowinfo[ix] */ + function branchHyperlink(ix){ + var br = tx.rowinfo[ix].br + var dest = tx.baseUrl + "/timeline?r=" + encodeURIComponent(br) + dest += tx.fileDiff ? "&m&cf=" : "&m&c=" + dest += encodeURIComponent(tx.rowinfo[ix].h) + return dest + } + function clickOnGraph(e){ + var ix = findTxIndex(e); + if( ix<0 ){ + tooltipObj.style.display = "none" + }else{ + var br = tx.rowinfo[ix].br + var dest = branchHyperlink(ix) + var hbr = br.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + tooltipObj.innerHTML = ""+hbr+"" + tooltipObj.style.display = "inline" + tooltipObj.style.position = "absolute" + var x = e.x + 4 + window.pageXOffset + tooltipObj.style.left = x+"px" + var y = e.y + window.pageYOffset - tooltipObj.clientHeight - 4 + tooltipObj.style.top = y+"px" + tooltipObj.style.visibility = "visible" + } + } + function dblclickOnGraph(e){ + var ix = findTxIndex(e); + var dest = branchHyperlink(ix) + tooltipObj.style.display = "none" + window.location.href = dest } function changeDisplay(selector,value){ var x = document.getElementsByClassName(selector); var n = x.length; for(var i=0; i=0 && precision\ @ {"delay":%d(nDelay),"mouseover":%d(bMouseover)} } @ style_graph_generator(); @@ -1475,10 +1477,11 @@ ** Query parameters: ** ** a=TIMEORTAG After this event ** b=TIMEORTAG Before this event ** c=TIMEORTAG "Circa" this event +** cf=FILEHASH "Circa" the first use of the file with FILEHASH ** m=TIMEORTAG Mark this event ** n=COUNT Maximum number of events. "all" for no limit ** p=CHECKIN Parents and ancestors of CHECKIN ** d=CHECKIN Children and descendants of CHECKIN ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN @@ -1637,10 +1640,22 @@ cookie_write_parameter("y","y",zType); } cookie_render(); url_initialize(&url, "timeline"); cgi_query_parameters_to_url(&url); + + /* Convert the cf=FILEHASH query parameter into a c=CHECKINHASH value */ + if( P("cf")!=0 ){ + zCirca = db_text(0, + "SELECT (SELECT uuid FROM blob WHERE rid=mlink.mid)" + " FROM mlink, event" + " WHERE mlink.fid=(SELECT rid FROM blob WHERE uuid LIKE '%q%%')" + " AND event.objid=mlink.mid" + " ORDER BY event.mtime LIMIT 1", + P("cf") + ); + } /* Convert r=TAG to t=TAG&rel. */ if( zBrName && !related ){ cgi_delete_query_parameter("r"); cgi_set_query_parameter("t", zBrName); @@ -1994,12 +2009,13 @@ zYearMonth = timeline_expand_datetime(zYearMonth); blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ", zYearMonth); } else if( zYearWeek ){ + char *z; zYearWeek = timeline_expand_datetime(zYearWeek); - char *z = db_text(0, "SELECT strftime('%%Y-%%W',%Q)", zYearWeek); + z = db_text(0, "SELECT strftime('%%Y-%%W',%Q)", zYearWeek); if( z && z[0] ){ zYearWeekStart = db_text(0, "SELECT date(%Q,'-6 days','weekday 1')", zYearWeek); zYearWeek = z; }else{ Index: src/user.c ================================================================== --- src/user.c +++ src/user.c @@ -498,10 +498,11 @@ ** (8) Check if the user can be extracted from the remote URL. ** ** The user name is stored in g.zLogin. The uid is in g.userUid. */ void user_select(void){ + UrlData url; if( g.userUid ) return; if( g.zLogin ){ if( attempt_user(g.zLogin)==0 ){ fossil_fatal("no such user: %s", g.zLogin); }else{ @@ -519,12 +520,13 @@ if( attempt_user(fossil_getenv("LOGNAME")) ) return; if( attempt_user(fossil_getenv("USERNAME")) ) return; - url_parse(0, 0); - if( g.url.user && attempt_user(g.url.user) ) return; + memset(&url, 0, sizeof(url)); + url_parse_local(0, 0, &url); + if( url.user && attempt_user(url.user) ) return; fossil_print( "Cannot figure out who you are! Consider using the --user\n" "command line option, setting your USER environment variable,\n" "or setting a default user with \"fossil user default USER\".\n" Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -8,10 +8,12 @@ * Improved handling of relative hyperlinks on the [/help?cmd=/artifact|/artifact] pages for wiki. For example, hyperlinks and the lizard <img> now work correctly for both [/artifact/2ff24ab0887cf522] and [/doc/0d7ac90d575004c2415/www/index.wiki]. + * Enhancements to the timeline graph layout, to show more information + with less clutter. * Many documentation enhancements. * For the "[/help?cmd=update|fossil update]" and "[/help?cmd=checkout|fossil checkout]" commands, if a managed file is removed because it is no longer part of the target check-in and the directory containing the file is empty after the @@ -27,26 +29,29 @@ * Added the [/help?cmd=/secureraw|/secureraw] page that requires the complete SHA1 or SHA3 hash, not just a prefix, before it will deliver content. * Accept purely numeric ISO8601 date/time strings as long as they do not conflict with a hash. Example: "20190510134217" instead of - "2019-05-10 13:42:17". This is very useful for query parameters. + "2019-05-10 13:42:17". This helps keep URLs shorter and less + complicated * Support both "1)" and "1." for numbered lists in markdown, as commonmark does. * The sync and clone HTTP requests omit the extra /xfer path element from the end of the request URI. All servers since 2010 know that the HTTP request is for a sync or clone from the mimetype so the extra path element is not needed. - * If an automatic sync gets a 301 or 302 redirect request, then update + * If an automatic sync gets a permanent redirect request, then update the saved remote URL to the new address. * Temporary filenames (for example used for external "diff" commands) try to preserve the suffix of the original file. * Added the [/help?cmd=/thisdayinhistory|/thisdayinhistory] web page. * Enhanced parsing of [/help?cmd=/timeline|/timeline] query parameters "ymd=", "ym=", and "yw=". All arguments are option (in which case they default to the current time) and all accept ISO8601 date/times without punctuation. + * Automatically disapprove pending moderation requests for a user when + that user is deleted. This helps in dealing with spam-bots. * Improvements to the "Capability Summary" section in the [/help?cmd=/secaudit0|Security Audit] web-page.

Changes for Version 2.8 (2019-02-20)