ADDED fossil.1 Index: fossil.1 ================================================================== --- /dev/null +++ fossil.1 @@ -0,0 +1,100 @@ +.TH FOSSIL "1" "February 2015" "http://fossil-scm.org" "User Commands" +.SH NAME +fossil \- Distributed Version Control System +.SH SYNOPSIS +.B fossil +\fIhelp\fR +.br +.B fossil +\fIhelp COMMAND\fR +.br +.B fossil +\fICOMMAND [OPTIONS]\fR +.SH DESCRIPTION +Fossil is a distributed version control system (DVCS) with built-in +wiki, ticket tracker, CGI/http interface, and http server. + +.SH Common COMMANDs: + +add clean import pull stash +.br +addremove clone info purge status +.br +all commit init push sync +.br +annotate diff json rebuild tag +.br +bisect export ls remote-url timeline +.br +blame extras merge revert ui +.br +branch finfo mv rm undo +.br +bundle fusefs open rss unpublish +.br +cat gdiff praise settings update +.br +changes help publish sqlite3 version + +.SH FEATURES + +Features as described on the fossil home page. + +.HP +1. +.B Integrated Bug Tracking, Wiki, & Technotes +- In addition to doing distributed version control like Git and +Mercurial, Fossil also supports bug tracking, wiki, and technotes. + +.HP +2. +.B Built-in Web Interface +- Fossil has a built-in and intuitive web interface that promotes +project situational awareness. Type "fossil ui" and Fossil automatically +opens a web browser to a page that shows detailed graphical history and +status information on that project. + +.HP +3. +.B Self-Contained +- Fossil is a single self-contained stand-alone executable. To install, +simply download a precompiled binary for Linux, Mac, OpenBSD, or Windows +and put it on your $PATH. Easy-to-compile source code is available for +users on other platforms. + +.HP +4. +.B Simple Networking +- No custom protocols or TCP ports. Fossil uses plain old HTTP (or HTTPS +or SSH) for all network communications, so it works fine from behind +restrictive firewalls, including proxies. The protocol is bandwidth +efficient to the point that Fossil can be used comfortably over dial-up. + +.HP +5. +.B CGI/SCGI Enabled +- No server is required, but if you want to set one up, Fossil supports +four simple server configurations. + +.HP +6. +.B Autosync +- Fossil supports "autosync" mode which helps to keep projects moving +forward by reducing the amount of needless forking and merging often +associated with distributed projects. + +.HP +7. +.B Robust & Reliable +- Fossil stores content using an enduring file format in an SQLite +database so that transactions are atomic even if interrupted by a +power loss or system crash. Automatic self-checks verify that all +aspects of the repository are consistent prior to each commit. In +over seven years of operation, no work has ever been lost after +having been committed to a Fossil repository. + +.SH DOCUMENTATION +http://www.fossil-scm.org/ +.br +.B fossil +\fIui\fR Index: skins/README.md ================================================================== --- skins/README.md +++ skins/README.md @@ -1,11 +1,11 @@ Built-in Skins ============== Each subdirectory under this folder describes a built-in "skin". There are three files in each subdirectory for the CSS, the header, -and the footer for the skin. +and the footer for that skin. To improve an existing built-in skin, simply edit the appropriate files and recompile. To add a new skin: @@ -23,5 +23,21 @@ 4. Edit the BuiltinSkin[] array near the top of the src/skins.c source file so that it describes and references the "newskin" skin. 5. Type "make" to rebuild. + +Development Hints +----------------- + +One way to develop a new skin is to copy the baseline files (css.txt, +footer.txt, and header.txt) into a working directory $WORKDIR then +launch Fossil with a command-line option "--skin $WORKDIR". Example: + + cp -r skins/default newskin + fossil ui --skin ./newskin + +When the argument to --skin contains one or more '/' characters, the +appropriate skin files are read from disk from the directory specified. +So after launching fossil as shown above, you can edit the newskin/css.txt, +newskin/header.txt, and newskin/footer.txt files using your favorite +text editor, then press Reload on your browser to see immediate results. Index: skins/black_and_white/css.txt ================================================================== --- skins/black_and_white/css.txt +++ skins/black_and_white/css.txt @@ -3,10 +3,13 @@ margin:0px 0px 0px 0px; padding:0px; font-family:verdana, arial, helvetica, "sans serif"; color:#333; background-color:white; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* consistent colours */ h2 { color: #333; Index: skins/black_and_white/header.txt ================================================================== --- skins/black_and_white/header.txt +++ skins/black_and_white/header.txt @@ -26,21 +26,21 @@ html "Home\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: skins/default/css.txt ================================================================== --- skins/default/css.txt +++ skins/default/css.txt @@ -2,10 +2,13 @@ body { margin: 0ex 1ex; padding: 0px; background-color: white; font-family: sans-serif; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* The project logo in the upper left-hand corner of each page */ div.logo { display: table-cell; Index: skins/default/header.txt ================================================================== --- skins/default/header.txt +++ skins/default/header.txt @@ -25,21 +25,21 @@ html "Home\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: skins/eagle/css.txt ================================================================== --- skins/eagle/css.txt +++ skins/eagle/css.txt @@ -3,10 +3,13 @@ margin: 0ex 1ex; padding: 0px; background-color: #485D7B; font-family: sans-serif; color: white; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* The project logo in the upper left-hand corner of each page */ div.logo { display: table-cell; Index: skins/eagle/header.txt ================================================================== --- skins/eagle/header.txt +++ skins/eagle/header.txt @@ -106,21 +106,21 @@ html "Home\n" html "Help\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: skins/enhanced1/css.txt ================================================================== --- skins/enhanced1/css.txt +++ skins/enhanced1/css.txt @@ -2,10 +2,13 @@ body { margin: 0ex 1ex; padding: 0px; background-color: white; font-family: sans-serif; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* The project logo in the upper left-hand corner of each page */ div.logo { display: table-cell; Index: skins/enhanced1/header.txt ================================================================== --- skins/enhanced1/header.txt +++ skins/enhanced1/header.txt @@ -106,21 +106,21 @@ html "Home\n" html "Help\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: skins/etienne1/css.txt ================================================================== --- skins/etienne1/css.txt +++ skins/etienne1/css.txt @@ -1,10 +1,14 @@ body { margin: 0 auto; - width: 960px; + min-width: 800px; + padding: 0px 20px; font-family: sans-serif; font-size:14pt; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } a { color: #4183C4; text-decoration: none; @@ -13,11 +17,11 @@ color: #4183C4; text-decoration: underline; } hr { - border: 0px; + color: #eee; } .title { color: #4183C4; float:left; @@ -100,30 +104,29 @@ padding: 10px; border-bottom: 1px solid #ccc; } .submenu a { - padding: 10px; + padding: 10px 11px; text-decoration:none; color: #777; } .submenu a:hover { + padding: 6px 10px; border: 1px solid #ccc; - border-bottom: 1px solid #fff; - border-top-left-radius: 5px; - border-top-right-radius: 5px; + border-radius: 5px; + color: #000; } .content { padding-top: 10px; font-size:.8em; color: #444; } -.udiff, .sbsdiff, -.content blockquote { +.udiff, .sbsdiff { font-size: .85em !important; overflow: auto; border: 1px solid #ccc; border-radius: 5px; } @@ -164,18 +167,19 @@ .report thead+tbody tr:hover { background-color: #f5f9fc !important; } td.tktDspLabel { - max-width: 70px; + width: 70px; text-align: right; + overflow: hidden; } td.tktDspValue { - max-width: 800px; text-align: left; vertical-align: top; - background-color: #f5f9fc; + background-color: #f8f8f8; + border: 1px solid #ccc; } td.tktDspValue pre { white-space: pre-wrap; } @@ -183,6 +187,13 @@ border-top: 1px solid #ccc; padding: 10px; font-size:.7em; margin-top: 10px; color: #ccc; +} +div.timelineDate { + font-weight: bold; + white-space: nowrap; +} +span.submenuctrl, span.submenuctrl input, select.submenuctrl { + color: #777; } Index: skins/etienne1/header.txt ================================================================== --- skins/etienne1/header.txt +++ skins/etienne1/header.txt @@ -19,70 +19,38 @@ } Index: skins/khaki/css.txt ================================================================== --- skins/khaki/css.txt +++ skins/khaki/css.txt @@ -2,10 +2,13 @@ body { margin: 0ex 0ex; padding: 0px; background-color: #fef3bc; font-family: sans-serif; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* The project logo in the upper left-hand corner of each page */ div.logo { display: inline; Index: skins/khaki/header.txt ================================================================== --- skins/khaki/header.txt +++ skins/khaki/header.txt @@ -24,21 +24,21 @@ html "Home\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: skins/plain_gray/css.txt ================================================================== --- skins/plain_gray/css.txt +++ skins/plain_gray/css.txt @@ -2,10 +2,13 @@ body { margin: 0ex 1ex; padding: 0px; background-color: white; font-family: sans-serif; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* The project logo in the upper left-hand corner of each page */ div.logo { display: table-row; Index: skins/plain_gray/header.txt ================================================================== --- skins/plain_gray/header.txt +++ skins/plain_gray/header.txt @@ -22,21 +22,21 @@ html "Home\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: skins/rounded1/css.txt ================================================================== --- skins/rounded1/css.txt +++ skins/rounded1/css.txt @@ -7,10 +7,13 @@ padding: 0px; background-color: white; color: #333; font-family: Verdana, sans-serif; font-size: 0.8em; + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; + -mx-text-size-adjust: none; } /* The project logo in the upper left-hand corner of each page */ div.logo { display: table-cell; @@ -181,15 +184,14 @@ cursor: pointer; } table.report tr td { padding: 3px 5px; - cursor: pointer; } textarea { font-size: 1em; } .fullsize-text { font-size: 1.25em; } Index: skins/rounded1/header.txt ================================================================== --- skins/rounded1/header.txt +++ skins/rounded1/header.txt @@ -26,21 +26,21 @@ html "Home\n" if {[anycap jor]} { html "Timeline\n" } -if {[hascap oh]} { +if {[anoncap oh]} { html "Files\n" } -if {[hascap o]} { +if {[anoncap o]} { html "Branches\n" html "Tags\n" } -if {[hascap r]} { +if {[anoncap r]} { html "Tickets\n" } -if {[hascap j]} { +if {[anoncap j]} { html "Wiki\n" } if {[hascap s]} { html "Admin\n" } elseif {[hascap a]} { Index: src/allrepo.c ================================================================== --- src/allrepo.c +++ src/allrepo.c @@ -105,20 +105,12 @@ ** ** extras Shows "extra" files from all local checkouts. The command ** line options supported by the extra command itself, if any ** are present, are passed along verbatim. ** -** ignore Arguments are repositories that should be ignored by -** subsequent clean, extras, list, pull, push, rebuild, and -** sync operations. The -c|--ckout option causes the listed -** local checkouts to be ignored instead. -** ** info Run the "info" command on all repositories. ** -** list | ls Display the location of all repositories. The -c|--ckout -** option causes all local checkouts to be listed instead. -** ** pull Run a "pull" operation on all repositories. Only the ** --verbose option is supported. ** ** push Run a "push" on all repositories. Only the --verbose ** option is supported. @@ -134,10 +126,25 @@ ** setting Run the "setting", "set", or "unset" commands on all ** set repositories. These command are particularly useful in ** unset conjunction with the "max-loadavg" setting which cannot ** otherwise be set globally. ** +** In addition, the following maintenance operations are supported: +** +** add Add all the repositories named to the set of repositories +** tracked by Fossil. Normally Fossil is able to keep up with +** this list by itself, but sometime it can benefit from this +** hint if you rename repositories. +** +** ignore Arguments are repositories that should be ignored by +** subsequent clean, extras, list, pull, push, rebuild, and +** sync operations. The -c|--ckout option causes the listed +** local checkouts to be ignored instead. +** +** list | ls Display the location of all repositories. The -c|--ckout +** option causes all local checkouts to be listed instead. +** ** Repositories are automatically added to the set of known repositories ** when one of the following commands are run against the repository: ** clone, info, pull, push, or sync. Even previously ignored repositories ** are added back to the list of repositories by these commands. ** @@ -259,26 +266,55 @@ useCheckouts = 1; stopOnError = 0; quiet = 1; }else if( strncmp(zCmd, "ignore", n)==0 ){ int j; + Blob fn = BLOB_INITIALIZER; + Blob sql = BLOB_INITIALIZER; useCheckouts = find_option("ckout","c",0)!=0; verify_all_options(); db_begin_transaction(); - for(j=3; j @@ -86,11 +89,11 @@ zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename); }else{ zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); } @
  • - @ Attachment %z(href("%R/ainfo/%s",zUuid))%S(zUuid) + @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid) if( moderation_pending(attachid) ){ @ *** Awaiting Moderator Approval *** } @
    %h(zFilename) @ [download]
    @@ -103,14 +106,14 @@ zSrc = "Deleted from"; }else { zSrc = "Added to"; } if( strlen(zTarget)==UUID_SIZE && validate16(zTarget, UUID_SIZE) ){ - @ %s(zSrc) ticket + @ %s(zSrc) ticket @ %S(zTarget) }else{ - @ %s(zSrc) wiki page + @ %s(zSrc) wiki page @ %h(zTarget) } }else{ if( zSrc==0 || zSrc[0]==0 ){ @ Deleted @@ -150,14 +153,14 @@ if( zPage && zTkt ) zTkt = 0; if( zFile==0 ) fossil_redirect_home(); login_check_credentials(); if( zPage ){ - if( g.perm.RdWiki==0 ) login_needed(); + if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } zTarget = zPage; }else if( zTkt ){ - if( g.perm.RdTkt==0 ) login_needed(); + if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; } zTarget = zTkt; }else{ fossil_redirect_home(); } if( attachid>0 ){ @@ -243,27 +246,33 @@ if( P("cancel") ) cgi_redirect(zFrom); if( zPage && zTkt ) fossil_redirect_home(); if( zPage==0 && zTkt==0 ) fossil_redirect_home(); login_check_credentials(); if( zPage ){ - if( g.perm.ApndWiki==0 || g.perm.Attach==0 ) login_needed(); + if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ + login_needed(g.anon.ApndWiki && g.anon.Attach); + return; + } if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){ fossil_redirect_home(); } zTarget = zPage; - zTargetType = mprintf("Wiki Page %h", - g.zTop, zPage, zPage); + zTargetType = mprintf("Wiki Page %h", + zPage, zPage); }else{ - if( g.perm.ApndTkt==0 || g.perm.Attach==0 ) login_needed(); + if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ + login_needed(g.anon.ApndTkt && g.anon.Attach); + return; + } if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTkt) ){ zTkt = db_text(0, "SELECT substr(tagname,5) FROM tag" " WHERE tagname GLOB 'tkt-%q*'", zTkt); if( zTkt==0 ) fossil_redirect_home(); } zTarget = zTkt; - zTargetType = mprintf("Ticket %S", - g.zTop, zTkt, zTkt); + zTargetType = mprintf("Ticket %S", + zTkt, zTkt); } if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop); if( P("cancel") ){ cgi_redirect(zFrom); } @@ -369,11 +378,14 @@ Blob attach; /* Content of the attachment */ int fShowContent = 0; const char *zLn = P("ln"); login_check_credentials(); - if( !g.perm.RdTkt && !g.perm.RdWiki ){ login_needed(); return; } + if( !g.perm.RdTkt && !g.perm.RdWiki ){ + login_needed(g.anon.RdTkt || g.anon.RdWiki); + return; + } rid = name_to_rid_www("name"); if( rid==0 ){ fossil_redirect_home(); } zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); #if 0 /* Shunning here needs to get both the attachment control artifact and @@ -399,17 +411,17 @@ fShowContent = zMime ? strncmp(zMime,"text/", 5)==0 : 0; if( validate16(zTarget, strlen(zTarget)) && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget) ){ zTktUuid = zTarget; - if( !g.perm.RdTkt ){ login_needed(); return; } + if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } if( g.perm.WrTkt ){ style_submenu_element("Delete","Delete","%R/ainfo/%s?del", zUuid); } }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){ zWikiName = zTarget; - if( !g.perm.RdWiki ){ login_needed(); return; } + if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } if( g.perm.WrWiki ){ style_submenu_element("Delete","Delete","%R/ainfo/%s?del", zUuid); } } zDate = db_text(0, "SELECT datetime(%.12f)", pAttach->rDate); @@ -443,11 +455,11 @@ } if( P("del") && ((zTktUuid && g.perm.WrTkt) || (zWikiName && g.perm.WrWiki)) ){ - form_begin(0, "%R/ainfo/%s", zUuid); + form_begin(0, "%R/ainfo/%!S", zUuid); @

    Confirm you want to delete the attachment shown below. @ @ } @@ -456,11 +468,11 @@ (zWikiName && g.perm.ModWiki); if( isModerator && (zModAction = P("modaction"))!=0 ){ if( strcmp(zModAction,"delete")==0 ){ moderation_disapprove(rid); if( zTktUuid ){ - cgi_redirectf("%R/tktview/%s", zTktUuid); + cgi_redirectf("%R/tktview/%!S", zTktUuid); }else{ cgi_redirectf("%R/wiki?name=%t", zWikiName); } return; } @@ -477,11 +489,11 @@ } @

    Overview
    @

    @ - @ fossil_free(zAge); @ if( zMergeTo ){ @ + @ %z(href("%R/timeline?f=%!S",zLastCkin))%h(zMergeTo) }else{ @ } @ } @@ -408,11 +408,11 @@ if( showClosed==0 && showAll==0 && showOpen==0 && colorTest==0 ){ new_brlist_page(); return; } login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } if( colorTest ){ showClosed = 0; showAll = 1; } if( showAll ) brFlags = BRL_BOTH; @@ -515,11 +515,11 @@ */ void brtimeline_page(void){ Stmt q; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } style_header("Branches"); style_submenu_element("List", "List", "brlist"); login_anonymous_available(); @

    The initial check-in for each branch:

    Index: src/browse.c ================================================================== --- src/browse.c +++ src/browse.c @@ -85,11 +85,11 @@ for(i=0; zPath[i]; i=j){ for(j=i; zPath[j] && zPath[j]!='/'; j++){} if( zPath[j] && g.perm.Hyperlink ){ if( zCI ){ - char *zLink = href("%R/%s?name=%#T%s&ci=%s", zURI, j, zPath, zREx, zCI); + char *zLink = href("%R/%s?name=%#T%s&ci=%!S", zURI, j, zPath, zREx,zCI); blob_appendf(pOut, "%s%z%#h", zSep, zLink, j-i, &zPath[i]); }else{ char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx); blob_appendf(pOut, "%s%z%#h", @@ -130,11 +130,11 @@ int linkTip = 1; HQuery sURI; if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; } login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } style_header("File List"); style_adunit_config(ADUNIT_RIGHT_OK); sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0, pathelementFunc, 0, 0); @@ -179,15 +179,15 @@ if( linkTip ){ style_submenu_element("Tip", "Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0)); } if( zCI ){ - @

    Files of check-in [%z(href("vinfo?name=%s",zUuid))%S(zUuid)] + @

    Files of check-in [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)] @ %s(blob_str(&dirname))

    - zSubdirLink = mprintf("%R/dir?ci=%s&name=%T", zUuid, zPrefix); + zSubdirLink = mprintf("%R/dir?ci=%!S&name=%T", zUuid, zPrefix); if( nD==0 ){ - style_submenu_element("File Ages", "File Ages", "%R/fileage?name=%s", + style_submenu_element("File Ages", "File Ages", "%R/fileage?name=%!S", zUuid); } }else{ @

    The union of all files from all check-ins @ %s(blob_str(&dirname))

    @@ -281,11 +281,11 @@ @
  • %z(href("%s%T",zSubdirLink,zFN))%h(zFN)
  • }else{ const char *zLink; if( zCI ){ const char *zUuid = db_column_text(&q, 1); - zLink = href("%R/artifact/%s",zUuid); + zLink = href("%R/artifact/%!S",zUuid); }else{ zLink = href("%R/finfo?name=%T%T",zPrefix,zFN); } @
  • %z(zLink)%h(zFN)
  • } @@ -542,11 +542,11 @@ char *zProjectName = db_get("project-name", 0); if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; } memset(&sTree, 0, sizeof(sTree)); login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0, pathelementFunc, 0, 0); url_initialize(&sURI, "tree"); cgi_query_parameters_to_url(&sURI); @@ -693,11 +693,11 @@ if( zCI ){ @

    %s(zObjType) from if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){ @ "%h(zCI)" } - @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)] %s(blob_str(&dirname)) + @ [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)] %s(blob_str(&dirname)) }else{ int n = db_int(0, "SELECT count(*) FROM plink"); @

    %s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname)) } if( useMtime ){ @@ -755,11 +755,11 @@ nDir++; }else if( !showDirOnly ){ const char *zFileClass = fileext_class(p->zName); char *zLink; if( zCI ){ - zLink = href("%R/artifact/%.16s",p->zUuid); + zLink = href("%R/artifact/%!S",p->zUuid); }else{ zLink = href("%R/finfo?name=%T",p->zFullName); } @
  • @ %z(zLink)%h(p->zName) @@ -905,11 +905,11 @@ @ AND filename.name=foci.filename @ AND blob.uuid=foci.uuid @ AND mlink.fid=blob.rid @ AND mlink.fid!=mlink.pid @ AND mlink.mid IN (SELECT x FROM ckin) -@ AND event.objid=mlink.mid +@ AND event.objid=mlink.mid @ ORDER BY event.mtime ASC; ; /* ** Look at all file containing in the version "vid". Construct a @@ -1002,11 +1002,11 @@ const char *zNow; /* Time of checkin */ int showId = PB("showid"); Stmt q1, q2; double baseTime; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } zName = P("name"); if( zName==0 ) zName = "tip"; rid = symbolic_name_to_rid(zName, "ci"); if( rid==0 ){ fossil_fatal("not a valid check-in: %s", zName); @@ -1022,18 +1022,18 @@ zGlob = P("glob"); compute_fileage(rid,zGlob); db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); @

    Files in - @ %z(href("%R/info?name=%T",zUuid))[%S(zUuid)] + @ %z(href("%R/info/%!S",zUuid))[%S(zUuid)] if( zGlob && zGlob[0] ){ @ that match "%h(zGlob)" and } @ ordered by check-in time

    @ @

    Times are relative to the checkin time for - @ %z(href("%R/ci/%s",zUuid))[%S(zUuid)] which is + @ %z(href("%R/ci/%!S",zUuid))[%S(zUuid)] which is @ %z(href("%R/timeline?c=%t",zNow))%s(zNow).

    @ @
  • Artifact ID:%z(href("%R/artifact/%s",zUuid))%s(zUuid) + @ %z(href("%R/artifact/%!S",zUuid))%s(zUuid) if( g.perm.Setup ){ @ (%d(rid)) } modPending = moderation_pending(rid); if( modPending ){ @@ -579,17 +591,17 @@ if( cnt==0 ){ @ %s(zHeader) } cnt++; @
  • - @ %z(href("%R/artifact/%s",zSrc))%h(zFile) + @ %z(href("%R/artifact/%!S",zSrc))%h(zFile) @ added by %h(zDispUser) on hyperlink_to_date(zDate, "."); - @ [%z(href("%R/ainfo/%s",zUuid))details] + @ [%z(href("%R/ainfo/%!S",zUuid))details] @
  • } if( cnt ){ @ } db_finalize(&q); } Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -753,11 +753,11 @@ ** Initialize a blob to be the content of a file. If the filename ** is blank or "-" then read from standard input. ** ** Any prior content of the blob is discarded, not freed. ** -** Return the number of bytes read. Calls fossil_fatal() error (i.e. +** Return the number of bytes read. Calls fossil_fatal() on error (i.e. ** it exit()s and does not return). */ int blob_read_from_file(Blob *pBlob, const char *zFilename){ int size, got; FILE *in; Index: src/branch.c ================================================================== --- src/branch.c +++ src/branch.c @@ -159,11 +159,11 @@ fossil_fatal("%s\n", g.zErrMsg); } assert( blob_is_reset(&branch) ); content_deltify(rootid, brid, 0); zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", brid); - fossil_print("New branch: %s\n", zUuid); + fossil_print("New branch: %S\n", zUuid); if( g.argc==3 ){ fossil_print( "\n" "Note: the local check-out has not been updated to the new\n" " branch. To begin working on the new branch, do this:\n" @@ -339,11 +339,11 @@ */ static void new_brlist_page(void){ Stmt q; double rNow; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } style_header("Branches"); style_adunit_config(ADUNIT_RIGHT_OK); login_anonymous_available(); db_prepare(&q, brlistQuery/*works-like:""*/); @@ -372,11 +372,11 @@ @
    %d(nCkin)%s(isClosed?"closed":"")merged into - @ %z(href("%R/timeline?f=%s",zLastCkin))%h(zMergeTo)
    @ db_prepare(&q1, @@ -1069,30 +1069,30 @@ while( db_step(&q2)==SQLITE_ROW ){ const char *zFUuid = db_column_text(&q2,0); const char *zFile = db_column_text(&q2,1); int fid = db_column_int(&q2,2); if( showId ){ - @ %z(href("%R/artifact/%s",zFUuid))%h(zFile) (%d(fid))
    + @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile) (%d(fid))
    }else{ - @ %z(href("%R/artifact/%s",zFUuid))%h(zFile)
    + @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)
    } } db_reset(&q2); @ @ @ fossil_free(zAge); } @
    TimeFilesCheckin
    - @ %z(href("%R/info/%s",zUuid))[%S(zUuid)] + @ %z(href("%R/info/%!S",zUuid))[%S(zUuid)] if( showId ){ @ (%d(mid)) } @ %W(zComment) (user: - @ %z(href("%R/timeline?u=%t&c=%t&nd&n=200",zUser,zUuid))%h(zUser), + @ %z(href("%R/timeline?u=%t&c=%!S&nd&n=200",zUser,zUuid))%h(zUser), @ branch: - @ %z(href("%R/timeline?r=%t&c=%t&nd&n=200",zBranch,zUuid))%h(zBranch)) + @ %z(href("%R/timeline?r=%t&c=%!S&nd&n=200",zBranch,zUuid))%h(zBranch)) @
    db_finalize(&q1); db_finalize(&q2); style_footer(); } Index: src/cache.c ================================================================== --- src/cache.c +++ src/cache.c @@ -330,11 +330,11 @@ sqlite3 *db; sqlite3_stmt *pStmt; char zBuf[100]; login_check_credentials(); - if( !g.perm.Setup ){ login_needed(); return; } + if( !g.perm.Setup ){ login_needed(0); return; } style_header("Web Cache Status"); db = cacheOpen(0); if( db==0 ){ @ The web-page cache is disabled for this repository }else{ @@ -378,11 +378,11 @@ void cache_getpage(void){ const char *zKey; Blob content; login_check_credentials(); - if( !g.perm.Setup ){ login_needed(); return; } + if( !g.perm.Setup ){ login_needed(0); return; } zKey = PD("key",""); blob_zero(&content); if( cache_read(&content, zKey)==0 ){ style_header("Cache Download Error"); @ The cache does not contain any entry with this key: "%h(zKey)" Index: src/cgi.c ================================================================== --- src/cgi.c +++ src/cgi.c @@ -465,10 +465,11 @@ if( g.fHttpTrace ){ fprintf(stderr, "# cgi: %s = [%s]\n", zName, zValue); } aParamQP[nUsedQP].seq = seqQP++; aParamQP[nUsedQP].isQP = isQP; + aParamQP[nUsedQP].cTag = 0; nUsedQP++; sortQP = 1; } /* @@ -1680,10 +1681,11 @@ */ #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */ #define HTTP_SERVER_SCGI 0x0002 /* SCGI instead of HTTP */ #define HTTP_SERVER_HAD_REPOSITORY 0x0004 /* Was the repository open? */ #define HTTP_SERVER_HAD_CHECKOUT 0x0008 /* Was a checkout open? */ +#define HTTP_SERVER_REPOLIST 0x0010 /* Allow repo listing */ #endif /* INTERFACE */ /* ** Maximum number of child processes that we can have running Index: src/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -1936,18 +1936,18 @@ db_prepare(&q, "SELECT uuid,merge FROM vmerge JOIN blob ON merge=rid" " WHERE id=-4"); while( db_step(&q)==SQLITE_ROW ){ const char *zIntegrateUuid = db_column_text(&q, 0); if( is_a_leaf(db_column_int(&q, 1)) ){ - fossil_print("Closed: %s\n", zIntegrateUuid); + fossil_print("Closed: %S\n", zIntegrateUuid); }else{ - fossil_print("Not_Closed: %s (not a leaf any more)\n", zIntegrateUuid); + fossil_print("Not_Closed: %S (not a leaf any more)\n", zIntegrateUuid); } } db_finalize(&q); - fossil_print("New_Version: %s\n", zUuid); + fossil_print("New_Version: %S\n", zUuid); if( outputManifest ){ zManifestFile = mprintf("%smanifest.uuid", g.zLocalRoot); blob_zero(&muuid); blob_appendf(&muuid, "%s\n", zUuid); blob_write_to_file(&muuid, zManifestFile); Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -65,14 +65,10 @@ */ static void db_err(const char *zFormat, ...){ va_list ap; char *z; int rc = 1; - static const char zRebuildMsg[] = - "If you have recently updated your fossil executable, you might\n" - "need to run \"fossil all rebuild\" to bring the repository\n" - "schemas up to date.\n"; va_start(ap, zFormat); z = vmprintf(zFormat, ap); va_end(ap); #ifdef FOSSIL_ENABLE_JSON if( g.json.isJsonMode ){ @@ -88,15 +84,14 @@ @ error Database\serror:\s%F(z) cgi_reply(); } else if( g.cgiOutput ){ g.cgiOutput = 0; - cgi_printf("

    Database Error

    \n" - "
    %h
    \n

    %s

    \n", z, zRebuildMsg); + cgi_printf("

    Database Error

    \n

    %h

    \n", z); cgi_reply(); }else{ - fprintf(stderr, "%s: %s\n\n%s", g.argv[0], z, zRebuildMsg); + fprintf(stderr, "%s: %s\n", g.argv[0], z); } free(z); db_force_rollback(); fossil_exit(rc); } @@ -164,14 +159,16 @@ if( rollbackFlag ) db.doRollback = 1; db.nBegin--; if( db.nBegin==0 ){ int i; if( db.doRollback==0 && db.nPriorChanges20 ){ style_submenu_element("20 Ancestors", "20 Ancestors", "%s", url_render(&url, "limit", "20", 0, 0)); } - if( db_get_boolean("white-foreground", 0) ){ + if( skin_white_foreground() ){ clr1 = 0xa04040; clr2 = 0x4059a0; }else{ clr1 = 0xffb5b5; /* Recent changes: red (hot) */ clr2 = 0xb5e0ff; /* Older changes: blue (cold) */ @@ -2307,17 +2307,17 @@ clr = gradient_color(clr1, clr2, ann.nVers-1, i); ann.aVers[i].zBgColor = mprintf("#%06x", clr); } if( showLog ){ - char *zLink = href("%R/finfo?name=%t&ci=%s",zFilename,zCI); + char *zLink = href("%R/finfo?name=%t&ci=%!S",zFilename,zCI); @

    Ancestors of %z(zLink)%h(zFilename) analyzed:

    @
      for(p=ann.aVers, i=0; i%s(p->zDate) - @ check-in %z(href("%R/info/%s",p->zMUuid))%S(p->zMUuid) - @ artifact %z(href("%R/artifact/%s",p->zFUuid))%S(p->zFUuid) + @ check-in %z(href("%R/info/%!S",p->zMUuid))%S(p->zMUuid) + @ artifact %z(href("%R/artifact/%!S",p->zFUuid))%S(p->zFUuid) @ #if 0 if( i>0 ){ char *zLink = xhref("target='infowindow'", "%R/fdiff?v1=%S&v2=%S&sbs=1", @@ -2335,17 +2335,17 @@ @
    @
    } if( !ann.bLimit ){ @

    Origin for each line in - @ %z(href("%R/finfo?name=%h&ci=%s", zFilename, zCI))%h(zFilename) - @ from check-in %z(href("%R/info/%s",zCI))%S(zCI):

    + @ %z(href("%R/finfo?name=%h&ci=%!S", zFilename, zCI))%h(zFilename) + @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI): iLimit = ann.nVers+10; }else{ @

    Lines added by the %d(iLimit) most recent ancestors of - @ %z(href("%R/finfo?name=%h&ci=%s", zFilename, zCI))%h(zFilename) - @ from check-in %z(href("%R/info/%s",zCI))%S(zCI):

    + @ %z(href("%R/finfo?name=%h&ci=%!S", zFilename, zCI))%h(zFilename) + @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI): } @
       for(i=0; iann.nVers && iVers<0 ) iVers = ann.nVers-1;
     
         if( bBlame ){
           if( iVers>=0 ){
             struct AnnVers *p = ann.aVers+iVers;
    -        char *zLink = xhref("target='infowindow'", "%R/info/%s", p->zMUuid);
    +        char *zLink = xhref("target='infowindow'", "%R/info/%!S", p->zMUuid);
             sqlite3_snprintf(sizeof(zPrefix), zPrefix,
                  ""
                  "%s%.10s %s %13.13s:",
                  p->zBgColor, zLink, p->zMUuid, p->zDate, p->zUser);
             fossil_free(zLink);
    @@ -2367,11 +2367,11 @@
             sqlite3_snprintf(sizeof(zPrefix), zPrefix, "%36s", "");
           }
         }else{
           if( iVers>=0 ){
             struct AnnVers *p = ann.aVers+iVers;
    -        char *zLink = xhref("target='infowindow'", "%R/info/%s", p->zMUuid);
    +        char *zLink = xhref("target='infowindow'", "%R/info/%!S", p->zMUuid);
             sqlite3_snprintf(sizeof(zPrefix), zPrefix,
                  ""
                  "%s%.10s %s %4d:",
                  p->zBgColor, zLink, p->zMUuid, p->zDate, i+1);
             fossil_free(zLink);
    @@ -2434,11 +2434,11 @@
       if( find_option("ignore-all-space","w",0)!=0 ){
         annFlags = DIFF_IGNORE_ALLWS; /* stronger than DIFF_IGNORE_EOLWS */
       }
       fileVers = find_option("filevers",0,0)!=0;
       db_must_be_within_tree();
    - 
    +
       /* We should be done with options.. */
       verify_all_options();
     
       if( g.argc<3 ) {
         usage("FILENAME");
    
    Index: src/diffcmd.c
    ==================================================================
    --- src/diffcmd.c
    +++ src/diffcmd.c
    @@ -841,11 +841,11 @@
     */
     void vpatch_page(void){
       const char *zFrom = P("from");
       const char *zTo = P("to");
       login_check_credentials();
    -  if( !g.perm.Read ){ login_needed(); return; }
    +  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
       if( zFrom==0 || zTo==0 ) fossil_redirect_home();
     
       cgi_set_content_type("text/plain");
       diff_all_two_versions(zFrom, zTo, 0, 0, 0, DIFF_VERBOSE);
     }
    
    Index: src/doc.c
    ==================================================================
    --- src/doc.c
    +++ src/doc.c
    @@ -106,10 +106,11 @@
       { "com",        3, "application/x-msdos-program"       },
       { "cpio",       4, "application/x-cpio"                },
       { "cpt",        3, "application/mac-compactpro"        },
       { "csh",        3, "application/x-csh"                 },
       { "css",        3, "text/css"                          },
    +  { "csv",        3, "text/csv"                          },
       { "dcr",        3, "application/x-director"            },
       { "deb",        3, "application/x-debian-package"      },
       { "dir",        3, "application/x-director"            },
       { "dl",         2, "video/dl"                          },
       { "dms",        3, "application/octet-stream"          },
    @@ -291,10 +292,24 @@
       { "xpm",        3, "image/x-xpixmap"                   },
       { "xwd",        3, "image/x-xwindowdump"               },
       { "xyz",        3, "chemical/x-pdb"                    },
       { "zip",        3, "application/zip"                   },
     };
    +
    +/*
    +** Verify that all entries in the aMime[] table are in sorted order.
    +** Abort with a fatal error if any is out-of-order.
    +*/
    +static void mimetype_verify(void){
    +  int i;
    +  for(i=1; i=0 ){
    +      fossil_fatal("mimetypes out of sequence: %s before %s",
    +                   aMime[i-1].zSuffix, aMime[i].zSuffix);
    +    }
    +  }
    +}
     
     /*
     ** Guess the mime-type of a document based on its name.
     */
     const char *mimetype_from_name(const char *zName){
    @@ -308,16 +323,11 @@
     #ifdef FOSSIL_DEBUG
       /* This is test code to make sure the table above is in the correct
       ** order
       */
       if( fossil_strcmp(zName, "mimetype-test")==0 ){
    -    for(i=1; i=0 ){
    -        fossil_fatal("mimetypes out of sequence: %s before %s",
    -                     aMime[i-1].zSuffix, aMime[i].zSuffix);
    -      }
    -    }
    +    mimetype_verify();
         return "ok";
       }
     #endif
     
       z = zName;
    @@ -356,10 +366,11 @@
     ** filename is special and verifies the integrity of the mimetype table.
     ** It should return "ok".
     */
     void mimetype_test_cmd(void){
       int i;
    +  mimetype_verify();
       for(i=2; i %s\n", g.argv[i], mimetype_from_name(g.argv[i]));
       }
     }
     
    @@ -369,10 +380,11 @@
     ** Show the built-in table used to guess embedded document mimetypes
     ** from file suffixes.
     */
     void mimetype_list_page(void){
       int i;
    +  mimetype_verify();
       style_header("Mimetype List");
       @ 

    The Fossil /doc page uses filename @ suffixes and the following table to guess at the appropriate mimetype @ for each document.

    @ @@ -531,11 +543,11 @@ static const char *const azSuffix[] = { "index.html", "index.wiki", "index.md" }; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } blob_init(&title, 0, 0); db_begin_transaction(); while( rid==0 && (++nMiss)<=ArraySize(azSuffix) ){ zName = PD("name", "tip/index.wiki"); for(i=0; zName[i] && zName[i]!='/'; i++){} Index: src/event.c ================================================================== --- src/event.c +++ src/event.c @@ -15,78 +15,90 @@ ** ******************************************************************************* ** ** This file contains code to do formatting of event messages: ** +** Technical Notes ** Milestones ** Blog posts ** New articles ** Process checkpoints ** Announcements +** +** Do not confuse "event" artifacts with the "event" table in the +** repository database. An "event" artifact is a technical-note: a +** wiki- or blog-like essay that appears on the timeline. The "event" +** table records all entries on the timeline, including tech-notes. +** +** (2015-02-14): Changing the name to "tech-note" most everywhere. */ #include "config.h" #include #include #include "event.h" /* -** Output a hyperlink to an event given its tagid. +** Output a hyperlink to an technote given its tagid. */ void hyperlink_to_event_tagid(int tagid){ - char *zEventId; - zEventId = db_text(0, "SELECT substr(tagname, 7) FROM tag WHERE tagid=%d", + char *zId; + zId = db_text(0, "SELECT substr(tagname, 7) FROM tag WHERE tagid=%d", tagid); - @ [%z(href("%R/event/%s",zEventId))%S(zEventId)] - free(zEventId); + @ [%z(href("%R/technote/%s",zId))%S(zId)] + free(zId); } /* +** WEBPAGE: technote ** WEBPAGE: event -** URL: /event +** +** Display a "technical note" or "tech-note" (formerly called an "event"). +** ** PARAMETERS: ** -** name=EVENTID // Identify the event to display EVENTID must be complete -** aid=ARTIFACTID // Which specific version of the event. Optional. -** v=BOOLEAN // Show details if TRUE. Default is FALSE. Optional. +** name=ID // Identify the tech-note to display. ID must be complete +** aid=ARTIFACTID // Which specific version of the tech-note. Optional. +** v=BOOLEAN // Show details if TRUE. Default is FALSE. Optional. ** ** Display an existing event identified by EVENTID */ void event_page(void){ int rid = 0; /* rid of the event artifact */ char *zUuid; /* UUID corresponding to rid */ - const char *zEventId; /* Event identifier */ + const char *zId; /* Event identifier */ const char *zVerbose; /* Value of verbose option */ - char *zETime; /* Time of the event */ + char *zETime; /* Time of the tech-note */ char *zATime; /* Time the artifact was created */ int specRid; /* rid specified by aid= parameter */ - int prevRid, nextRid; /* Previous or next edits of this event */ - Manifest *pEvent; /* Parsed event artifact */ - Blob fullbody; /* Complete content of the event body */ - Blob title; /* Title extracted from the event body */ + int prevRid, nextRid; /* Previous or next edits of this tech-note */ + Manifest *pTNote; /* Parsed technote artifact */ + Blob fullbody; /* Complete content of the technote body */ + Blob title; /* Title extracted from the technote body */ Blob tail; /* Event body that comes after the title */ - Stmt q1; /* Query to search for the event */ + Stmt q1; /* Query to search for the technote */ int verboseFlag; /* True to show details */ + const char *zMimetype = 0; /* Mimetype of the document */ - /* wiki-read privilege is needed in order to read events. + /* wiki-read privilege is needed in order to read tech-notes. */ login_check_credentials(); if( !g.perm.RdWiki ){ - login_needed(); + login_needed(g.anon.RdWiki); return; } - zEventId = P("name"); - if( zEventId==0 ){ fossil_redirect_home(); return; } + zId = P("name"); + if( zId==0 ){ fossil_redirect_home(); return; } zUuid = (char*)P("aid"); specRid = zUuid ? uuid_to_rid(zUuid, 0) : 0; rid = nextRid = prevRid = 0; db_prepare(&q1, "SELECT rid FROM tagxref" " WHERE tagid=(SELECT tagid FROM tag WHERE tagname GLOB 'event-%q*')" " ORDER BY mtime DESC", - zEventId + zId ); while( db_step(&q1)==SQLITE_ROW ){ nextRid = rid; rid = db_column_int(&q1, 0); if( specRid==0 || specRid==rid ){ @@ -96,12 +108,12 @@ break; } } db_finalize(&q1); if( rid==0 || (specRid!=0 && specRid!=rid) ){ - style_header("No Such Event"); - @ Cannot locate specified event + style_header("No Such Tech-Note"); + @ Cannot locate a technical note called %h(zId). style_footer(); return; } zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); zVerbose = P("v"); @@ -113,156 +125,194 @@ } verboseFlag = (zVerbose!=0) && !is_false(zVerbose); /* Extract the event content. */ - pEvent = manifest_get(rid, CFTYPE_EVENT, 0); - if( pEvent==0 ){ - fossil_fatal("Object #%d is not an event", rid); + pTNote = manifest_get(rid, CFTYPE_EVENT, 0); + if( pTNote==0 ){ + fossil_fatal("Object #%d is not a tech-note", rid); } - blob_init(&fullbody, pEvent->zWiki, -1); - if( wiki_find_title(&fullbody, &title, &tail) ){ - style_header("%s", blob_str(&title)); + zMimetype = wiki_filter_mimetypes(PD("mimetype",pTNote->zMimetype)); + blob_init(&fullbody, pTNote->zWiki, -1); + blob_init(&title, 0, 0); + blob_init(&tail, 0, 0); + if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ + if( !wiki_find_title(&fullbody, &title, &tail) ){ + blob_appendf(&title, "Tech-note %S", zId); + tail = fullbody; + } + }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ + markdown_to_html(&fullbody, &title, &tail); + if( blob_size(&title)==0 ){ + blob_appendf(&title, "Tech-note %S", zId); + } }else{ - style_header("Event %S", zEventId); + blob_appendf(&title, "Tech-note %S", zId); tail = fullbody; } + style_header("%s", blob_str(&title)); if( g.perm.WrWiki && g.perm.Write && nextRid==0 ){ - style_submenu_element("Edit", "Edit", "%s/eventedit?name=%s", - g.zTop, zEventId); + style_submenu_element("Edit", 0, "%R/technoteedit?name=%!S", zId); } - zETime = db_text(0, "SELECT datetime(%.17g)", pEvent->rEventDate); - style_submenu_element("Context", 0, "%R/timeline?c=%.20s", zEventId); + zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); + style_submenu_element("Context", 0, "%R/timeline?c=%.20s", zId); if( g.perm.Hyperlink ){ if( verboseFlag ){ - style_submenu_element("Plain", 0, "%R/event?name=%.20s&aid=%s", - zEventId, zUuid); + style_submenu_element("Plain", 0, + "%R/technote?name=%!S&aid=%s&mimetype=text/plain", + zId, zUuid); if( nextRid ){ char *zNext; zNext = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", nextRid); - style_submenu_element("Next", 0,"%R/event?name=%.20s&aid=%s&v", - zEventId, zNext); + style_submenu_element("Next", 0,"%R/technote?name=%!S&aid=%s&v", + zId, zNext); free(zNext); } if( prevRid ){ char *zPrev; zPrev = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", prevRid); - style_submenu_element("Prev", 0, "%R/event?name=%s&aid=%s&v", - zEventId, zPrev); + style_submenu_element("Prev", 0, "%R/technote?name=%!S&aid=%s&v", + zId, zPrev); free(zPrev); } }else{ - style_submenu_element("Detail", 0, "%R/event?name=%.20s&aid=%s&v", - zEventId, zUuid); + style_submenu_element("Detail", 0, "%R/technote?name=%!S&aid=%s&v", + zId, zUuid); } } if( verboseFlag && g.perm.Hyperlink ){ int i; const char *zClr = 0; Blob comment; - zATime = db_text(0, "SELECT datetime(%.17g)", pEvent->rDate); - @

    Event [%z(href("%R/artifact/%s",zUuid))%S(zUuid)] at + zATime = db_text(0, "SELECT datetime(%.17g)", pTNote->rDate); + @

    Tech-note [%z(href("%R/artifact/%!S",zUuid))%S(zUuid)] at @ [%z(href("%R/timeline?c=%T",zETime))%s(zETime)] - @ entered by user %h(pEvent->zUser) on + @ entered by user %h(pTNote->zUser) on @ [%z(href("%R/timeline?c=%T",zATime))%s(zATime)]:

    @
    - for(i=0; inTag; i++){ - if( fossil_strcmp(pEvent->aTag[i].zName,"+bgcolor")==0 ){ - zClr = pEvent->aTag[i].zValue; + for(i=0; inTag; i++){ + if( fossil_strcmp(pTNote->aTag[i].zName,"+bgcolor")==0 ){ + zClr = pTNote->aTag[i].zValue; } } if( zClr && zClr[0]==0 ) zClr = 0; if( zClr ){ @
    }else{ @
    } - blob_init(&comment, pEvent->zComment, -1); + blob_init(&comment, pTNote->zComment, -1); wiki_convert(&comment, 0, WIKI_INLINE); blob_reset(&comment); @
    @

    } - wiki_convert(&tail, 0, 0); + if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ + wiki_convert(&fullbody, 0, 0); + }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ + cgi_append_content(blob_buffer(&tail), blob_size(&tail)); + }else{ + @
    +    @ %h(blob_str(&fullbody))
    +    @ 
    + } style_footer(); - manifest_destroy(pEvent); + manifest_destroy(pTNote); } /* +** WEBPAGE: technoteedit ** WEBPAGE: eventedit -** URL: /eventedit?name=EVENTID +** +** Revise or create a technical note (formerly called an 'event'). +** +** Parameters: ** -** Edit an event. If name is omitted, create a new event. +** name=ID Hex hash ID of the tech-note. If omitted, a new +** tech-note is created. */ void eventedit_page(void){ char *zTag; int rid = 0; Blob event; - const char *zEventId; + const char *zId; int n; const char *z; char *zBody = (char*)P("w"); char *zETime = (char*)P("t"); const char *zComment = P("c"); const char *zTags = P("g"); const char *zClr; + const char *zMimetype = P("mimetype"); + int isNew = 0; if( zBody ){ zBody = mprintf("%s", zBody); } login_check_credentials(); - zEventId = P("name"); - if( zEventId==0 ){ - zEventId = db_text(0, "SELECT lower(hex(randomblob(20)))"); + zId = P("name"); + if( zId==0 ){ + zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); + isNew = 1; }else{ - int nEventId = strlen(zEventId); - if( nEventId!=40 || !validate16(zEventId, 40) ){ + int nId = strlen(zId); + if( !validate16(zId, nId) ){ fossil_redirect_home(); return; } } - zTag = mprintf("event-%s", zEventId); + zTag = mprintf("event-%s", zId); rid = db_int(0, "SELECT rid FROM tagxref" - " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" + " WHERE tagid=(SELECT tagid FROM tag WHERE tagname GLOB '%q*')" " ORDER BY mtime DESC", zTag ); + if( rid && strlen(zId)<40 ){ + zId = db_text(0, + "SELECT substr(tagname,7) FROM tag WHERE tagname GLOB '%q*'", + zTag + ); + } free(zTag); /* Need both check-in and wiki-write or wiki-create privileges in order ** to edit/create an event. */ if( !g.perm.Write || (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ - login_needed(); + login_needed(g.anon.Write && (rid ? g.anon.WrWiki : g.anon.NewWiki)); return; } /* Figure out the color */ if( rid ){ - zClr = db_text("", "SELECT bgcolor FROM event WHERE objid=%d", rid); + zClr = db_text("", "SELECT bgcolor FROM event WHERE objid=%d", rid); }else{ zClr = ""; + isNew = 1; } zClr = PD("clr",zClr); if( fossil_strcmp(zClr,"##")==0 ) zClr = PD("cclr",""); /* If editing an existing event, extract the key fields to use as ** a starting point for the edit. */ - if( rid && (zBody==0 || zETime==0 || zComment==0 || zTags==0) ){ - Manifest *pEvent; - pEvent = manifest_get(rid, CFTYPE_EVENT, 0); - if( pEvent && pEvent->type==CFTYPE_EVENT ){ - if( zBody==0 ) zBody = pEvent->zWiki; + if( rid + && (zBody==0 || zETime==0 || zComment==0 || zTags==0 || zMimetype==0) + ){ + Manifest *pTNote; + pTNote = manifest_get(rid, CFTYPE_EVENT, 0); + if( pTNote && pTNote->type==CFTYPE_EVENT ){ + if( zBody==0 ) zBody = pTNote->zWiki; if( zETime==0 ){ - zETime = db_text(0, "SELECT datetime(%.17g)", pEvent->rEventDate); + zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); } - if( zComment==0 ) zComment = pEvent->zComment; + if( zComment==0 ) zComment = pTNote->zComment; + if( zMimetype==0 ) zMimetype = pTNote->zMimetype; } if( zTags==0 ){ zTags = db_text(0, "SELECT group_concat(substr(tagname,5),', ')" " FROM tagxref, tag" @@ -276,11 +326,11 @@ zETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", zETime); if( P("submit")!=0 && (zBody!=0 && zComment!=0) ){ char *zDate; Blob cksum; int nrid, n; - blob_zero(&event); + blob_init(&event, 0, 0); db_begin_transaction(); login_verify_csrf_secret(); while( fossil_isspace(zComment[0]) ) zComment++; n = strlen(zComment); while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } @@ -289,17 +339,20 @@ } zDate = date_in_standard_format("now"); blob_appendf(&event, "D %s\n", zDate); free(zDate); zETime[10] = 'T'; - blob_appendf(&event, "E %s %s\n", zETime, zEventId); + blob_appendf(&event, "E %s %s\n", zETime, zId); zETime[10] = ' '; if( rid ){ char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); blob_appendf(&event, "P %s\n", zUuid); free(zUuid); } + if( zMimetype && zMimetype[0] ){ + blob_appendf(&event, "N %s\n", zMimetype); + } if( zClr && zClr[0] ){ blob_appendf(&event, "T +bgcolor * %F\n", zClr); } if( zTags && zTags[0] ){ Blob tags, one; @@ -346,26 +399,37 @@ md5sum_blob(&event, &cksum); blob_appendf(&event, "Z %b\n", &cksum); blob_reset(&cksum); nrid = content_put(&event); db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); - manifest_crosslink(nrid, &event, MC_NONE); + if( manifest_crosslink(nrid, &event, MC_NONE)==0 ){ + db_end_transaction(1); + style_header("Error"); + @ Internal error: Fossil tried to make an invalid artifact for + @ the edited technode. + style_footer(); + return; + } assert( blob_is_reset(&event) ); content_deltify(rid, nrid, 0); db_end_transaction(0); - cgi_redirectf("event?name=%T", zEventId); + cgi_redirectf("technote?name=%T", zId); } if( P("cancel")!=0 ){ - cgi_redirectf("event?name=%T", zEventId); + cgi_redirectf("technote?name=%T", zId); return; } if( zBody==0 ){ - zBody = mprintf("Event Text"); + zBody = mprintf("Insert new content here..."); } - style_header("Edit Event %S", zEventId); + if( isNew ){ + style_header("New Tech-note %S", zId); + }else{ + style_header("Edit Tech-note %S", zId); + } if( P("preview")!=0 ){ - Blob title, tail, com; + Blob com; @

    Timeline comment preview:

    @
    @
    if( zClr && zClr[0] ){ @
    @@ -377,55 +441,55 @@ wiki_convert(&com, 0, WIKI_INLINE|WIKI_NOBADLINKS); @
    @ @

    Page content preview:

    @

    - blob_zero(&event); + blob_init(&event, 0, 0); blob_append(&event, zBody, -1); - if( wiki_find_title(&event, &title, &tail) ){ - @

    %h(blob_str(&title))

    - wiki_convert(&tail, 0, 0); - }else{ - wiki_convert(&event, 0, 0); - } + wiki_render_by_mimetype(&event, zMimetype); @

    blob_reset(&event); } for(n=2, z=zBody; z[0]; z++){ if( z[0]=='\n' ) n++; } if( n<20 ) n = 20; if( n>40 ) n = 40; - @
    + @
    login_insert_csrf_secret(); - @ + @ @ - @ + @ @ - @ + @ @ - @ + @ @ @ @ + + @ + @ @ @ @ } db_finalize(&q); Index: src/info.c ================================================================== --- src/info.c +++ src/info.c @@ -403,32 +403,32 @@ } }else{ if( zOld && zNew ){ if( fossil_strcmp(zOld, zNew)!=0 ){ @

    Modified %z(href("%R/finfo?name=%T",zName))%h(zName) - @ from %z(href("%R/artifact/%s",zOld))[%S(zOld)] - @ to %z(href("%R/artifact/%s",zNew))[%S(zNew)]. + @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)] + @ to %z(href("%R/artifact/%!S",zNew))[%S(zNew)]. }else if( zOldName!=0 && fossil_strcmp(zName,zOldName)!=0 ){ @

    Name change @ from %z(href("%R/finfo?name=%T",zOldName))%h(zOldName) @ to %z(href("%R/finfo?name=%T",zName))%h(zName). }else{ @

    Execute permission %s(( mperm==PERM_EXE )?"set":"cleared") for @ %z(href("%R/finfo?name=%T",zName))%h(zName) } }else if( zOld ){ - @

    Deleted %z(href("%s/finfo?name=%T",g.zTop,zName))%h(zName) - @ version %z(href("%R/artifact/%s",zOld))[%S(zOld)] + @

    Deleted %z(href("%R/finfo?name=%T",zName))%h(zName) + @ version %z(href("%R/artifact/%!S",zOld))[%S(zOld)] }else{ @

    Added %z(href("%R/finfo?name=%T",zName))%h(zName) - @ version %z(href("%R/artifact/%s",zNew))[%S(zNew)] + @ version %z(href("%R/artifact/%!S",zNew))[%S(zNew)] } if( diffFlags ){ append_diff(zOld, zNew, diffFlags, pRe); }else if( zOld && zNew && fossil_strcmp(zOld,zNew)!=0 ){ @    - @ %z(href("%R/fdiff?v1=%s&v2=%s&sbs=1",zOld,zNew))[diff] + @ %z(href("%R/fdiff?v1=%!S&v2=%!S&sbs=1",zOld,zNew))[diff] } } } /* @@ -527,11 +527,11 @@ const char *zW; /* URL param for ignoring whitespace */ const char *zPage = "vinfo"; /* Page that shows diffs */ const char *zPageHide = "ci"; /* Page that hides diffs */ login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } zName = P("name"); rid = name_to_rid_www("name"); if( rid==0 ){ style_header("Check-in Information Error"); @ No such object: %h(g.argv[2]) @@ -635,19 +635,19 @@ if( (zPJ[jj]>0 && zPJ[jj]<' ') || strchr("\"*/:<>?\\|", zPJ[jj]) ){ zPJ[jj] = '_'; } } @

    @ @ @ @ blob_reset(&projName); } @@ -725,11 +725,11 @@ @ Show Unified Diffs @ %z(xhref("class='button'","%R/%s/%T?sbs=1",zPage,zName)) @ Show Side-by-Side Diffs } if( zParent ){ - @ %z(xhref("class='button'","%R/vpatch?from=%s&to=%s",zParent,zUuid)) + @ %z(xhref("class='button'","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) @ Patch } @ if( pRe ){ @

    Only differences that match regular expression "%h(zRe)" @@ -775,11 +775,11 @@ Blob wiki; int modPending; const char *zModAction; login_check_credentials(); - if( !g.perm.RdWiki ){ login_needed(); return; } + if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } rid = name_to_rid_www("name"); if( rid==0 || (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))==0 ){ style_header("Wiki Page Information Error"); @ No such object: %h(P("name")) style_footer(); @@ -815,11 +815,11 @@ pWiki->zWikiTitle); login_anonymous_available(); @

    Overview
    @

    Event Time (UTC):
    Timestamp (UTC): @ @
    Timeline Comment:
    Timeline Comment: - @ @
    Background Color:
    Timeline Background Color: render_color_chooser(0, zClr, 0, "clr", "cclr"); @
    Tags: @ @
    Markup Style: + mimetype_option_menu(zMimetype); + @
    Page Content: - @ @
    @ Index: src/finfo.c ================================================================== --- src/finfo.c +++ src/finfo.c @@ -215,11 +215,11 @@ zCiUuid, zCom, zUser, zFileUuid, zBr); comment_print(zOut, zCom, 11, iWidth, g.comFmtFlags); fossil_free(zOut); }else{ blob_reset(&line); - blob_appendf(&line, "%.10s ", zCiUuid); + blob_appendf(&line, "%S ", zCiUuid); blob_appendf(&line, "%.10s ", zDate); blob_appendf(&line, "%8.8s ", zUser); blob_appendf(&line, "%8.8s ", zBr); blob_appendf(&line,"%-39.39s", zCom ); comment_print(blob_str(&line), zCom, 0, iWidth, g.comFmtFlags); @@ -305,11 +305,11 @@ int uBg = P("ubg")!=0; int fDebug = atoi(PD("debug","0")); int fShowId = P("showid")!=0; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } style_header("File History"); login_anonymous_available(); url_initialize(&url, "finfo"); if( brBg ) url_add_parameter(&url, "brbg", 0); if( uBg ) url_add_parameter(&url, "ubg", 0); @@ -383,11 +383,11 @@ } blob_reset(&sql); blob_zero(&title); if( baseCheckin ){ char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", baseCheckin); - char *zLink = href("%R/info/%s", zUuid); + char *zLink = href("%R/info/%!S", zUuid); blob_appendf(&title, "Ancestors of file "); hyperlinked_path(zFilename, &title, zUuid, "tree", ""); if( fShowId ) blob_appendf(&title, " (%d)", fnid); blob_appendf(&title, " from check-in %z%S", zLink, zUuid); if( fShowId ) blob_appendf(&title, " (%d)", baseCheckin); @@ -469,11 +469,11 @@ char *zPrevName = db_text(0, "SELECT name FROM filename WHERE fnid=%d", pfnid); @ Renamed from @ %z(href("%R/finfo?name=%t", zPrevName))%h(zPrevName) } - @ %z(href("%R/artifact/%s",zUuid))[%S(zUuid)] + @ %z(href("%R/artifact/%!S",zUuid))[%S(zUuid)] if( fShowId ){ @ (%d(frid)) } @ part of check-in }else{ @@ -503,25 +503,27 @@ const char *z = zFilename; @ %z(href("%R/annotate?filename=%h&checkin=%s",z,zCkin)) @ [annotate] @ %z(href("%R/blame?filename=%h&checkin=%s",z,zCkin)) @ [blame] - @ %z(href("%R/timeline?n=200&uf=%s",zUuid))[checkins using] + @ %z(href("%R/timeline?n=200&uf=%!S",zUuid))[checkins using] if( fpid ){ - @ %z(href("%R/fdiff?sbs=1&v1=%s&v2=%s",zPUuid,zUuid))[diff] + @ %z(href("%R/fdiff?sbs=1&v1=%!S&v2=%!S",zPUuid,zUuid))[diff] } } if( fDebug & FINFO_DEBUG_MLINK ){ int ii; + char *zAncLink; @
    fid=%d(frid) pid=%d(fpid) mid=%d(fmid) if( nParent>0 ){ @ parents=%d(aParent[0]) for(ii=1; ii + zAncLink = href("%R/finfo?name=%T&ci=%!S&debug=1",zFilename,zCkin); + @ %z(zAncLink)[ancestry] } tag_private_status(frid); @
    Timelines: - @ %z(href("%R/timeline?f=%s&unhide",zUuid))family + @ %z(href("%R/timeline?f=%!S&unhide",zUuid))family if( zParent ){ - @ | %z(href("%R/timeline?p=%s&unhide",zUuid))ancestors + @ | %z(href("%R/timeline?p=%!S&unhide",zUuid))ancestors } if( !isLeaf ){ - @ | %z(href("%R/timeline?d=%s&unhide",zUuid))descendants + @ | %z(href("%R/timeline?d=%!S&unhide",zUuid))descendants } if( zParent && !isLeaf ){ - @ | %z(href("%R/timeline?dp=%s&unhide",zUuid))both + @ | %z(href("%R/timeline?dp=%!S&unhide",zUuid))both } db_prepare(&q2,"SELECT substr(tag.tagname,5) FROM tagxref, tag " " WHERE rid=%d AND tagtype>0 " " AND tag.tagid=tagxref.tagid " " AND +tag.tagname GLOB 'sym-*'", rid); @@ -657,31 +657,31 @@ } db_finalize(&q2); /* The Download: line */ - if( g.perm.Zip ){ + if( g.anon.Zip ){ char *zUrl = mprintf("%R/tarball/%t-%S.tar.gz?uuid=%s", zPJ, zUuid, zUuid); @
    Downloads: @ %z(href("%s",zUrl))Tarball - @ | %z(href("%R/zip/%t-%S.zip?uuid=%s",zPJ,zUuid,zUuid)) + @ | %z(href("%R/zip/%t-%S.zip?uuid=%!S",zPJ,zUuid,zUuid)) @ ZIP archive fossil_free(zUrl); } @
    Other Links: - @ %z(href("%R/tree?ci=%S",zUuid))files - @ | %z(href("%R/fileage?name=%S",zUuid))file ages - @ | %z(href("%R/tree?nofiles&type=tree&ci=%S",zUuid))folders - @ | %z(href("%R/artifact/%S",zUuid))manifest - @ | %z(href("%R/vdiff?from=pbranch:%S&to=%S",zUuid,zUuid)) + @ %z(href("%R/tree?ci=%!S",zUuid))files + @ | %z(href("%R/fileage?name=%!S",zUuid))file ages + @ | %z(href("%R/tree?nofiles&type=tree&ci=%!S",zUuid))folders + @ | %z(href("%R/artifact/%!S",zUuid))manifest + @ | %z(href("%R/vdiff?from=pbranch:%!S&to=%!S",zUuid,zUuid)) @ branch diff - if( g.perm.Write ){ - @ | %z(href("%R/ci_edit?r=%S",zUuid))edit + if( g.anon.Write ){ + @ | %z(href("%R/ci_edit?r=%!S",zUuid))edit } @
    @ - @ @ "); @ "); + if( pWiki->zMimetype ){ + @ + } if( pWiki->nParent>0 ){ int i; @ } @
    Artifact ID:%z(href("%R/artifact/%s",zUuid))%s(zUuid) + @ %z(href("%R/artifact/%!S",zUuid))%s(zUuid) if( g.perm.Setup ){ @ (%d(rid)) } modPending = moderation_pending(rid); if( modPending ){ @@ -829,16 +829,19 @@ @
    Page Name:%h(pWiki->zWikiTitle)
    Date: hyperlink_to_date(zDate, "
    Original User: hyperlink_to_user(pWiki->zUser, zDate, "
    Mimetype:%h(pWiki->zMimetype)
    Parent%s(pWiki->nParent==1?"":"s"): for(i=0; inParent; i++){ char *zParent = pWiki->azParent[i]; - @ %z(href("info/%s",zParent))%s(zParent) + @ %z(href("info/%!S",zParent))%s(zParent) } @
    @@ -856,11 +859,11 @@ } @
    Content
    blob_init(&wiki, pWiki->zWiki, -1); - wiki_convert(&wiki, 0, 0); + wiki_render_by_mimetype(&wiki, pWiki->zMimetype); blob_reset(&wiki); manifest_destroy(pWiki); style_footer(); } @@ -993,11 +996,11 @@ const char *zW; const char *zVerbose; const char *zGlob; ReCompiled *pRe = 0; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } login_anonymous_available(); zRe = P("regex"); if( zRe ) re_compile(&pRe, zRe, 0); zBranch = P("branch"); if( zBranch && zBranch[0] ){ @@ -1208,11 +1211,11 @@ int mPerm = db_column_int(&q, 5); const char *zBr = db_column_text(&q, 6); int sameFilename = prevName!=0 && fossil_strcmp(zName,prevName)==0; if( sameFilename && !showDetail ){ if( cnt==1 ){ - @ %z(href("%R/whatis/%s",zUuid))[more...] + @ %z(href("%R/whatis/%!S",zUuid))[more...] } cnt++; continue; } if( !sameFilename ){ @@ -1251,14 +1254,14 @@ @ on branch %z(href("%R/timeline?r=%T",zBr))%h(zBr) } @ — %!w(zCom) (user: hyperlink_to_user(zUser,zDate,")"); if( g.perm.Hyperlink ){ - @ %z(href("%R/finfo?name=%T&ci=%s",zName,zVers))[ancestry] - @ %z(href("%R/annotate?filename=%T&checkin=%s",zName,zVers)) + @ %z(href("%R/finfo?name=%T&ci=%!S",zName,zVers))[ancestry] + @ %z(href("%R/annotate?filename=%T&checkin=%!S",zName,zVers)) @ [annotate] - @ %z(href("%R/blame?filename=%T&checkin=%s",zName,zVers)) + @ %z(href("%R/blame?filename=%T&checkin=%!S",zName,zVers)) @ [blame] } cnt++; if( pDownloadName && blob_size(pDownloadName)==0 ){ blob_append(pDownloadName, zName, -1); @@ -1338,11 +1341,11 @@ } @ - %!w(zCom) by hyperlink_to_user(zUser,zDate," on"); hyperlink_to_date(zDate, "."); if( pDownloadName && blob_size(pDownloadName)==0 ){ - blob_appendf(pDownloadName, "%.10s.txt", zUuid); + blob_appendf(pDownloadName, "%S.txt", zUuid); } tag_private_status(rid); cnt++; } db_finalize(&q); @@ -1365,17 +1368,17 @@ }else{ @ Attachment "%h(zFilename)" to } objType |= OBJTYPE_ATTACHMENT; if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){ - if( g.perm.Hyperlink && g.perm.RdTkt ){ - @ ticket [%z(href("%R/tktview?name=%s",zTarget))%S(zTarget)] + if( g.perm.Hyperlink && g.anon.RdTkt ){ + @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)] }else{ @ ticket [%S(zTarget)] } }else{ - if( g.perm.Hyperlink && g.perm.RdWiki ){ + if( g.perm.Hyperlink && g.anon.RdWiki ){ @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)] }else{ @ wiki page [%h(zTarget)] } } @@ -1390,11 +1393,11 @@ } db_finalize(&q); if( cnt==0 ){ @ Control artifact. if( pDownloadName && blob_size(pDownloadName)==0 ){ - blob_appendf(pDownloadName, "%.10s.txt", zUuid); + blob_appendf(pDownloadName, "%S.txt", zUuid); } tag_private_status(rid); } return objType; } @@ -1423,11 +1426,11 @@ ReCompiled *pRe = 0; u64 diffFlags; u32 objdescFlags = 0; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } v1 = name_to_rid_www("v1"); v2 = name_to_rid_www("v2"); if( v1==0 || v2==0 ) fossil_redirect_home(); zRe = P("regex"); if( zRe ) re_compile(&pRe, zRe, 0); @@ -1474,17 +1477,17 @@ g.zTop, P("v1"), P("v2"), zW); } if( P("smhdr")!=0 ){ @

    Differences From Artifact - @ %z(href("%R/artifact/%s",zV1))[%S(zV1)] To - @ %z(href("%R/artifact/%s",zV2))[%S(zV2)].

    + @ %z(href("%R/artifact/%!S",zV1))[%S(zV1)] To + @ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]. }else{ @

    Differences From - @ Artifact %z(href("%R/artifact/%s",zV1))[%S(zV1)]:

    + @ Artifact %z(href("%R/artifact/%!S",zV1))[%S(zV1)]: object_description(v1, objdescFlags, 0); - @

    To Artifact %z(href("%R/artifact/%s",zV2))[%S(zV2)]:

    + @

    To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]:

    object_description(v2, objdescFlags, 0); } if( pRe ){ @ Only differences that match regular expression "%h(zRe)" @ are shown. @@ -1508,11 +1511,11 @@ const char *zMime; Blob content; rid = name_to_rid_www("name"); login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } if( rid==0 ) fossil_redirect_home(); zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); if( fossil_strcmp(P("name"), zUuid)==0 && login_is_nobody() ){ g.isConst = 1; } @@ -1605,11 +1608,11 @@ char *zUuid; u32 objdescFlags = 0; rid = name_to_rid_www("name"); login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } if( rid==0 ) fossil_redirect_home(); if( g.perm.Admin ){ const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ style_submenu_element("Unshun","Unshun", "%s/shun?accept=%s&sub=1#delshun", @@ -1791,11 +1794,11 @@ if( rid==0 ){ rid = name_to_rid_www("name"); } login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } if( rid==0 ) fossil_redirect_home(); if( g.perm.Admin ){ const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ style_submenu_element("Unshun","Unshun", "%s/shun?accept=%s&sub=1#accshun", @@ -1906,11 +1909,11 @@ Manifest *pTktChng; int modPending; const char *zModAction; char *zTktTitle; login_check_credentials(); - if( !g.perm.RdTkt ){ login_needed(); return; } + if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } rid = name_to_rid_www("name"); if( rid==0 ){ fossil_redirect_home(); } zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); if( g.perm.Admin ){ if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ @@ -1961,11 +1964,11 @@ } @
    Overview
    @

    @ - @ @
    Artifact ID:%z(href("%R/artifact/%s",zUuid))%s(zUuid) + @ %z(href("%R/artifact/%!S",zUuid))%s(zUuid) if( g.perm.Setup ){ @ (%d(rid)) } modPending = moderation_pending(rid); if( modPending ){ @@ -2278,11 +2281,11 @@ Blob comment; char *zBranchName = 0; Stmt q; login_check_credentials(); - if( !g.perm.Write ){ login_needed(); return; } + if( !g.perm.Write ){ login_needed(g.anon.Write); return; } rid = name_to_typed_rid(P("r"), "ci"); zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); zComment = db_text(0, "SELECT coalesce(ecomment,comment)" " FROM event WHERE objid=%d", rid); if( zComment==0 ) fossil_redirect_home(); @@ -2480,11 +2483,11 @@ @ @
    blob_reset(&suffix); } @

    Make changes to attributes of check-in - @ [%z(href("%R/ci/%s",zUuid))%s(zUuid)]:

    + @ [%z(href("%R/ci/%!S",zUuid))%s(zUuid)]:

    form_begin(0, "%R/ci_edit"); login_insert_csrf_secret(); @
    @ @@ -2598,12 +2601,11 @@ @ @ } } if( zBranchName ) fossil_free(zBranchName); Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -352,16 +352,12 @@ login_cookie_path(), -86400); db_multi_exec("UPDATE user SET cookie=NULL, ipaddr=NULL, " " cexpire=0 WHERE uid=%d" " AND login NOT IN ('anonymous','nobody'," " 'developer','reader')", g.userUid); - cgi_replace_parameter(cookie, NULL) - /* At the time of this writing, cgi_replace_parameter() was - ** "NULL-value-safe", and I'm hoping the NULL doesn't cause any - ** downstream problems here. We could alternately use "" here. - */ - ; + cgi_replace_parameter(cookie, NULL); + cgi_replace_parameter("anon", NULL); } } /* ** Return true if the prefix of zStr matches zPattern. Return false if @@ -451,27 +447,46 @@ } sqlite3_result_int(context, rc); } /* -** WEBPAGE: login -** WEBPAGE: logout -** WEBPAGE: my -** -** Generate the login page. -** +** Return true if the current page was reached by a redirect from the /login +** page. +*/ +int referred_from_login(void){ + const char *zReferer = P("HTTP_REFERER"); + char *zPattern; + int rc; + if( zReferer==0 ) return 0; + zPattern = mprintf("%s/login*", g.zBaseURL); + rc = sqlite3_strglob(zPattern, zReferer)==0; + fossil_free(zPattern); + return rc; +} + +/* ** There used to be a page named "my" that was designed to show information ** about a specific user. The "my" page was linked from the "Logged in as USER" ** line on the title bar. The "my" page was never completed so it is now ** removed. Use this page as a placeholder in older installations. +** +** WEBPAGE: login +** WEBPAGE: logout +** WEBPAGE: my +** +** The login/logout page. Parameters: +** +** g=URL Jump back to this URL after login completes +** anon The g=URL is not accessible by "nobody" but is +** accessible by "anonymous" */ void login_page(void){ const char *zUsername, *zPasswd; const char *zNew1, *zNew2; const char *zAnonPw = 0; const char *zGoto = P("g"); - int anonFlag; + int anonFlag; /* Login as "anonymous" would be useful */ char *zErrMsg = ""; int uid; /* User id logged in user */ char *zSha1Pw; const char *zIpAddr; /* IP address of requestor */ const char *zReferer; @@ -489,15 +504,20 @@ } sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0, constant_time_cmp_function, 0, 0); zUsername = P("u"); zPasswd = P("p"); - anonFlag = P("anon")!=0; - if( P("out")!=0 ){ + anonFlag = g.zLogin==0 && PB("anon"); + + /* Handle log-out requests */ + if( P("out") ){ login_clear_login_data(); redirect_to_g(); + return; } + + /* Deal with password-change requests */ if( g.perm.Password && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ /* The user requests a password change */ zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin, 0); @@ -577,19 +597,40 @@ } } style_header("Login/Logout"); style_adunit_config(ADUNIT_OFF); @ %s(zErrMsg) - if( zGoto && P("anon")==0 ){ - @

    A login is required for %h(zGoto).

    + if( zGoto ){ + char *zAbbrev = fossil_strdup(zGoto); + int i; + for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){} + zAbbrev[i] = 0; + if( g.zLogin ){ + @

    Use a different login with greater privilege than %h(g.zLogin) + @ to access %h(zAbbrev). + }else if( anonFlag ){ + @

    Login as anonymous or any named user + @ to access page %h(zAbbrev). + }else{ + @

    Login as a named user to access page %h(zAbbrev). + } } form_begin(0, "%R/login"); if( zGoto ){ @ }else if( zReferer && strncmp(g.zBaseURL, zReferer, strlen(g.zBaseURL))==0 ){ @ } + if( anonFlag ){ + @ + } + if( g.zLogin ){ + @

    Currently logged in as %h(g.zLogin). + @

    + @
    + @

    Change user: + } @

    Branch Closure: @ + @ as "closed". @
    @ @ if( anonFlag ){ @ @@ -599,11 +640,11 @@ @ @ @ @ @ - if( g.zLogin==0 ){ + if( g.zLogin==0 && (anonFlag || zGoto==0) ){ zAnonPw = db_text(0, "SELECT pw FROM user" " WHERE login='anonymous'" " AND cap!=''"); } @ @@ -624,23 +665,14 @@ @ form.action = "%h(zSSL)/login"; @ } } @ } @ - if( g.zLogin==0 ){ - @

    Enter - }else{ - @

    You are currently logged in as %h(g.zLogin)

    - @

    To change your login to a different user, enter - } - @ your user-id and password at the left and press the - @ "Login" button. Your user name will be stored in a browser cookie. - @ You must configure your web browser to accept cookies in order for - @ the login to take.

    + @

    Pressing the Login button grants permission to store a cookie.

    if( db_get_boolean("self-register", 0) ){ @

    If you do not have an account, you can - @ create one. + @ create one. } if( zAnonPw ){ unsigned int uSeed = captcha_seed(); const char *zDecoded = captcha_decode(uSeed); int bAutoCaptcha = db_get_boolean("auto-captcha", 0); @@ -657,22 +689,14 @@ @ onclick="gebi('u').value='anonymous'; gebi('p').value='%s(zDecoded)';" /> } @ free(zCaptcha); } - if( g.zLogin ){ - @


    - @

    To log off the system (and delete your login cookie) - @ press the following button:
    - @

    - } @ if( g.perm.Password ){ @
    - @

    To change your password, enter your old password and your - @ new password twice below then press the "Change Password" - @ button.

    + @

    Change Password for user %h(g.zLogin):

    form_begin(0, "%R/login"); @ @ @ @ @@ -811,10 +835,11 @@ ** variables appropriately. ** ** g.userUid Database USER.UID value. Might be -1 for "nobody" ** g.zLogin Database USER.LOGIN value. NULL for user "nobody" ** g.perm Permissions granted to this user +** g.anon Permissions that would be available to anonymous ** g.isHuman True if the user is human, not a spider or robot ** */ void login_check_credentials(void){ int uid = 0; /* User id */ @@ -1002,23 +1027,32 @@ ** Memory of settings */ static int login_anon_once = 1; /* -** Add the default privileges of users "nobody" and "anonymous" as appropriate -** for the user g.zLogin. +** Add to g.perm the default privileges of users "nobody" and/or "anonymous" +** as appropriate for the user g.zLogin. +** +** This routine also sets up g.anon to be either a copy of g.perm for +** all logged in uses, or the privileges that would be available to "anonymous" +** if g.zLogin==0 (meaning that the user is "nobody"). */ void login_set_anon_nobody_capabilities(void){ - if( g.zLogin && login_anon_once ){ + if( login_anon_once ){ const char *zCap; - /* All logged-in users inherit privileges from "nobody" */ + /* All users get privileges from "nobody" */ zCap = db_text("", "SELECT cap FROM user WHERE login = 'nobody'"); login_set_capabilities(zCap, 0); - if( fossil_strcmp(g.zLogin, "nobody")!=0 ){ + zCap = db_text("", "SELECT cap FROM user WHERE login = 'anonymous'"); + if( g.zLogin && fossil_strcmp(g.zLogin, "nobody")!=0 ){ /* All logged-in users inherit privileges from "anonymous" */ - zCap = db_text("", "SELECT cap FROM user WHERE login = 'anonymous'"); login_set_capabilities(zCap, 0); + g.anon = g.perm; + }else{ + /* Record the privileges of anonymous in g.anon */ + g.anon = g.perm; + login_set_capabilities(zCap, LOGIN_ANON); } login_anon_once = 0; } } @@ -1025,55 +1059,57 @@ /* ** Flags passed into the 2nd argument of login_set/replace_capabilities(). */ #if INTERFACE #define LOGIN_IGNORE_UV 0x01 /* Ignore "u" and "v" */ +#define LOGIN_ANON 0x02 /* Use g.anon instead of g.perm */ #endif /* -** Adds all capability flags in zCap to g.perm. +** Adds all capability flags in zCap to g.perm or g.anon. */ void login_set_capabilities(const char *zCap, unsigned flags){ int i; + FossilUserPerms *p = (flags & LOGIN_ANON) ? &g.anon : &g.perm; if(NULL==zCap){ return; } for(i=0; zCap[i]; i++){ switch( zCap[i] ){ - case 's': g.perm.Setup = 1; /* Fall thru into Admin */ - case 'a': g.perm.Admin = g.perm.RdTkt = g.perm.WrTkt = g.perm.Zip = - g.perm.RdWiki = g.perm.WrWiki = g.perm.NewWiki = - g.perm.ApndWiki = g.perm.Hyperlink = g.perm.Clone = - g.perm.NewTkt = g.perm.Password = g.perm.RdAddr = - g.perm.TktFmt = g.perm.Attach = g.perm.ApndTkt = - g.perm.ModWiki = g.perm.ModTkt = 1; + case 's': p->Setup = 1; /* Fall thru into Admin */ + case 'a': p->Admin = p->RdTkt = p->WrTkt = p->Zip = + p->RdWiki = p->WrWiki = p->NewWiki = + p->ApndWiki = p->Hyperlink = p->Clone = + p->NewTkt = p->Password = p->RdAddr = + p->TktFmt = p->Attach = p->ApndTkt = + p->ModWiki = p->ModTkt = 1; /* Fall thru into Read/Write */ - case 'i': g.perm.Read = g.perm.Write = 1; break; - case 'o': g.perm.Read = 1; break; - case 'z': g.perm.Zip = 1; break; - - case 'd': g.perm.Delete = 1; break; - case 'h': g.perm.Hyperlink = 1; break; - case 'g': g.perm.Clone = 1; break; - case 'p': g.perm.Password = 1; break; - - case 'j': g.perm.RdWiki = 1; break; - case 'k': g.perm.WrWiki = g.perm.RdWiki = g.perm.ApndWiki =1; break; - case 'm': g.perm.ApndWiki = 1; break; - case 'f': g.perm.NewWiki = 1; break; - case 'l': g.perm.ModWiki = 1; break; - - case 'e': g.perm.RdAddr = 1; break; - case 'r': g.perm.RdTkt = 1; break; - case 'n': g.perm.NewTkt = 1; break; - case 'w': g.perm.WrTkt = g.perm.RdTkt = g.perm.NewTkt = - g.perm.ApndTkt = 1; break; - case 'c': g.perm.ApndTkt = 1; break; - case 'q': g.perm.ModTkt = 1; break; - case 't': g.perm.TktFmt = 1; break; - case 'b': g.perm.Attach = 1; break; - case 'x': g.perm.Private = 1; break; + case 'i': p->Read = p->Write = 1; break; + case 'o': p->Read = 1; break; + case 'z': p->Zip = 1; break; + + case 'd': p->Delete = 1; break; + case 'h': p->Hyperlink = 1; break; + case 'g': p->Clone = 1; break; + case 'p': p->Password = 1; break; + + case 'j': p->RdWiki = 1; break; + case 'k': p->WrWiki = p->RdWiki = p->ApndWiki =1; break; + case 'm': p->ApndWiki = 1; break; + case 'f': p->NewWiki = 1; break; + case 'l': p->ModWiki = 1; break; + + case 'e': p->RdAddr = 1; break; + case 'r': p->RdTkt = 1; break; + case 'n': p->NewTkt = 1; break; + case 'w': p->WrTkt = p->RdTkt = p->NewTkt = + p->ApndTkt = 1; break; + case 'c': p->ApndTkt = 1; break; + case 'q': p->ModTkt = 1; break; + case 't': p->TktFmt = 1; break; + case 'b': p->Attach = 1; break; + case 'x': p->Private = 1; break; /* The "u" privileges is a little different. It recursively ** inherits all privileges of the user named "reader" */ case 'u': { if( (flags & LOGIN_IGNORE_UV)==0 ){ @@ -1110,42 +1146,43 @@ /* ** If the current login lacks any of the capabilities listed in ** the input, then return 0. If all capabilities are present, then ** return 1. */ -int login_has_capability(const char *zCap, int nCap){ +int login_has_capability(const char *zCap, int nCap, u32 flgs){ int i; int rc = 1; + FossilUserPerms *p = (flgs & LOGIN_ANON) ? &g.anon : &g.perm; if( nCap<0 ) nCap = strlen(zCap); for(i=0; iAdmin; break; + case 'b': rc = p->Attach; break; + case 'c': rc = p->ApndTkt; break; + case 'd': rc = p->Delete; break; + case 'e': rc = p->RdAddr; break; + case 'f': rc = p->NewWiki; break; + case 'g': rc = p->Clone; break; + case 'h': rc = p->Hyperlink; break; + case 'i': rc = p->Write; break; + case 'j': rc = p->RdWiki; break; + case 'k': rc = p->WrWiki; break; + case 'l': rc = p->ModWiki; break; + case 'm': rc = p->ApndWiki; break; + case 'n': rc = p->NewTkt; break; + case 'o': rc = p->Read; break; + case 'p': rc = p->Password; break; + case 'q': rc = p->ModTkt; break; + case 'r': rc = p->RdTkt; break; + case 's': rc = p->Setup; break; + case 't': rc = p->TktFmt; break; /* case 'u': READER */ /* case 'v': DEVELOPER */ - case 'w': rc = g.perm.WrTkt; break; - case 'x': rc = g.perm.Private; break; + case 'w': rc = p->WrTkt; break; + case 'x': rc = p->Private; break; /* case 'y': */ - case 'z': rc = g.perm.Zip; break; + case 'z': rc = p->Zip; break; default: rc = 0; break; } } return rc; } @@ -1195,11 +1232,11 @@ /* ** Call this routine when the credential check fails. It causes ** a redirect to the "login" page. */ -void login_needed(void){ +void login_needed(int anonOk){ #ifdef FOSSIL_ENABLE_JSON if(g.json.isJsonMode){ json_err( FSL_JSON_E_DENIED, NULL, 1 ); fossil_exit(0); /* NOTREACHED */ @@ -1206,11 +1243,23 @@ assert(0); }else #endif /* FOSSIL_ENABLE_JSON */ { const char *zUrl = PD("REQUEST_URI", "index"); - cgi_redirect(mprintf("login?g=%T", zUrl)); + const char *zQS = P("QUERY_STRING"); + Blob redir; + blob_init(&redir, 0, 0); + if( login_wants_https_redirect() ){ + blob_appendf(&redir, "%s/login?g=%T", g.zHttpsURL, zUrl); + }else{ + blob_appendf(&redir, "%R/login?g=%T", zUrl); + } + if( anonOk ) blob_append(&redir, "&anon", 5); + if( zQS && zQS[0] ){ + blob_appendf(&redir, "&%s", zQS); + } + cgi_redirect(blob_str(&redir)); /* NOTREACHED */ assert(0); } } @@ -1219,17 +1268,14 @@ ** the anonymous user has Hyperlink permission, then paint a mesage ** to inform the user that much more information is available by ** logging in as anonymous. */ void login_anonymous_available(void){ - if( !g.perm.Hyperlink && - db_exists("SELECT 1 FROM user" - " WHERE login='anonymous'" - " AND cap LIKE '%%h%%'") ){ + if( !g.perm.Hyperlink && g.anon.Hyperlink ){ const char *zUrl = PD("REQUEST_URI", "index"); @

    Many hyperlinks are disabled.
    - @ Use anonymous login + @ Use anonymous login @ to enable hyperlinks.

    } } /* Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -193,12 +193,17 @@ /* Information used to populate the RCVFROM table */ int rcvid; /* The rcvid. 0 if not yet defined. */ char *zIpAddr; /* The remote IP address */ char *zNonce; /* The nonce used for login */ - /* permissions used by the server */ + /* permissions available to current user */ struct FossilUserPerms perm; + + /* permissions available to current user or to "anonymous". + ** This is the logical union of perm permissions above with + ** the value that perm would take if g.zLogin were "anonymous". */ + struct FossilUserPerms anon; #ifdef FOSSIL_ENABLE_TCL /* all Tcl related context necessary for integration */ struct TclContext tcl; #endif @@ -1173,11 +1178,11 @@ const char *z = aCommand[i].zName; if( '/'==*z || strncmp(z,"test",4)==0 ) continue; if( j==0 ){ @
    j = 0; } @@ -1201,11 +1206,11 @@ if( '/'!=*z ) continue; if( j==0 ){ @ - @ + @ @ if( isPriv ){ @ } @ @@ -1080,57 +1095,19 @@ char *branchName = 0; /* Name of the branch at rid */ char *parentBranchName = 0; /* Name of the parent branch */ int rid; /* Get the name of the current branch */ - branchName = db_text(0, - "SELECT value FROM tagxref" - " WHERE tagid=%d" - " AND tagxref.tagtype>0" - " AND rid=%d", - TAG_BRANCH, branchRid - ); + branchName = name_of_branch(branchRid); if( !branchName ) return 0; - /* Find the name of the branch this was forked from */ - db_prepare(&s, - "SELECT pid, tagxref.value FROM plink JOIN tagxref" - " WHERE cid=:rid" - " AND isprim=1" - " AND tagxref.tagid=%d" - " AND tagxref.tagtype>0" - " AND tagxref.rid=pid", - TAG_BRANCH - ); - rid = branchRid; - while( rid>0 ){ - db_bind_int(&s, ":rid", rid); - if( db_step(&s)==SQLITE_ROW ){ - const char *zValue; /* Branch name of the pid */ - rid = db_column_int(&s, 0); - zValue = db_column_text(&s, 1); - if( !zValue ){ - rid = 0; - break; - } - if( fossil_strcmp(zValue,branchName) ){ - parentBranchName = fossil_strdup(zValue); - break; - } - }else{ - rid = 0; - break; - } - db_reset(&s); - } - db_finalize(&s); - - if( rid==0 ){ - fossil_free(branchName); - fossil_free(parentBranchName); + parentBranchName = name_of_branch(start_of_branch(branchRid, 0)); + + if( !parentBranchName ){ + fossil_free(branchName); return 0; } /* Find the last checkin coming from the parent branch */ db_prepare(&s, @@ -1170,5 +1147,75 @@ fossil_free(branchName); fossil_free(parentBranchName); return 0; } + +/* Maximum number of collision examples to remember */ +#define MAX_COLLIDE 25 + +/* +** WEBPAGE: hash-collisions +** +** Show the number of hash collisions for hash prefixes of various lengths. +*/ +void hash_collisions_webpage(void){ + int i, j, kk; + int nHash = 0; + Stmt q; + char zPrev[UUID_SIZE+1]; + struct { + int cnt; + char *azHit[MAX_COLLIDE]; + char z[UUID_SIZE+1]; + } aCollide[UUID_SIZE+1]; + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + memset(aCollide, 0, sizeof(aCollide)); + memset(zPrev, 0, sizeof(zPrev)); + db_prepare(&q,"SELECT uuid FROM blob ORDER BY 1"); + while( db_step(&q)==SQLITE_ROW ){ + const char *zUuid = db_column_text(&q,0); + int n = db_column_bytes(&q,0); + int i; + nHash++; + for(i=0; zPrev[i] && zPrev[i]==zUuid[i]; i++){} + if( i>0 && i<=UUID_SIZE ){ + if( i>=4 && aCollide[i].cnt + @ + @ + for(i=1; i<=UUID_SIZE; i++){ + if( aCollide[i].cnt==0 ) continue; + @ + } + @
      } if( aCmdHelp[i].zText && *aCmdHelp[i].zText ){ - @
    • %s(z+1)
    • + @
    • %s(z+1)
    • }else{ @
    • %s(z+1)
    • } j++; if( j>=n ){ @@ -1231,11 +1236,11 @@ if( strncmp(z,"test",4)!=0 ) continue; if( j==0 ){ @
      } if( aCmdHelp[i].zText && *aCmdHelp[i].zText ){ - @
    • %s(z)
    • + @
    • %s(z)
    • }else{ @
    • %s(z)
    • } j++; if( j>=n ){ @@ -1352,12 +1357,15 @@ ** zRepo might be a directory itself. In that case chroot into ** the directory zRepo. ** ** Assume the user-id and group-id of the repository, or if zRepo ** is a directory, of that directory. +** +** The noJail flag means that the chroot jail is not entered. But +** privileges are still lowered to that of the the user-id and group-id. */ -static char *enter_chroot_jail(char *zRepo){ +static char *enter_chroot_jail(char *zRepo, int noJail){ #if !defined(_WIN32) if( getuid()==0 ){ int i; struct stat sStat; Blob dir; @@ -1366,26 +1374,28 @@ db_close(1); } file_canonical_name(zRepo, &dir, 0); zDir = blob_str(&dir); - if( file_isdir(zDir)==1 ){ - if( file_chdir(zDir, 1) ){ - fossil_fatal("unable to chroot into %s", zDir); - } - zRepo = "/"; - }else{ - for(i=strlen(zDir)-1; i>0 && zDir[i]!='/'; i--){} - if( zDir[i]!='/' ) fossil_fatal("bad repository name: %s", zRepo); - if( i>0 ){ - zDir[i] = 0; + if( !noJail ){ + if( file_isdir(zDir)==1 ){ if( file_chdir(zDir, 1) ){ fossil_fatal("unable to chroot into %s", zDir); } - zDir[i] = '/'; + zRepo = "/"; + }else{ + for(i=strlen(zDir)-1; i>0 && zDir[i]!='/'; i--){} + if( zDir[i]!='/' ) fossil_fatal("bad repository name: %s", zRepo); + if( i>0 ){ + zDir[i] = 0; + if( file_chdir(zDir, 1) ){ + fossil_fatal("unable to chroot into %s", zDir); + } + zDir[i] = '/'; + } + zRepo = &zDir[i]; } - zRepo = &zDir[i]; } if( stat(zRepo, &sStat)!=0 ){ fossil_fatal("cannot stat() repository: %s", zRepo); } i = setgid(sStat.st_gid); @@ -1398,10 +1408,48 @@ } } #endif return zRepo; } + +/* +** Generate a web-page that lists all repositories located under the +** g.zRepositoryName directory and return non-zero. +** +** Or, if no repositories can be located beneath g.zRepositoryName, +** return 0. +*/ +static int repo_list_page(void){ + Blob base; + int n = 0; + + assert( g.db==0 ); + blob_init(&base, g.zRepositoryName, -1); + sqlite3_open(":memory:", &g.db); + db_multi_exec("CREATE TABLE sfile(x TEXT);"); + db_multi_exec("CREATE TABLE vfile(pathname);"); + vfile_scan(&base, blob_size(&base), 0, 0, 0); + db_multi_exec("DELETE FROM sfile WHERE x NOT GLOB '*.fossil'"); + n = db_int(0, "SELECT count(*) FROM sfile"); + if( n>0 ){ + Stmt q; + @

      Available Repositories:

      + @
        + db_prepare(&q, "SELECT x, substr(x,-7,-100000)||'/home'" + " FROM sfile ORDER BY x COLLATE nocase;"); + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q, 0); + const char *zUrl = db_column_text(&q, 1); + @
      1. %h(zName)
      2. + } + @
      + cgi_reply(); + } + sqlite3_close(g.db); + g.db = 0; + return n; +} /* ** Preconditions: ** ** * Environment variables are set up according to the CGI standard. @@ -1421,11 +1469,15 @@ ** $prefix can be determined from its suffix, then the file $prefix is ** returned as static text. ** ** If no suitable webpage is found, try to redirect to zNotFound. */ -static void process_one_web_page(const char *zNotFound, Glob *pFileGlob){ +static void process_one_web_page( + const char *zNotFound, /* Redirect here on a 404 if not NULL */ + Glob *pFileGlob, /* Deliver static files matching */ + int allowRepoList /* Send repo list for "/" URL */ +){ const char *zPathInfo; char *zPath = NULL; int idx; int i; @@ -1496,10 +1548,14 @@ if( szFile<1024 ){ set_base_url(0); if( zNotFound ){ cgi_redirect(zNotFound); + }else if( strcmp(zPathInfo,"/")==0 + && allowRepoList + && repo_list_page() ){ + /* Will return a list of repositories */ }else{ #ifdef FOSSIL_ENABLE_JSON if(g.json.isJsonMode){ json_err(FSL_JSON_E_RESOURCE_NOT_FOUND,NULL,1); return; @@ -1795,10 +1851,11 @@ const char *zFile; const char *zNotFound = 0; char **azRedirect = 0; /* List of repositories to redirect to */ int nRedirect = 0; /* Number of entries in azRedirect */ Glob *pFileGlob = 0; /* Pattern for files */ + int allowRepoList = 0; /* Allow lists of repository files */ Blob config, line, key, value, value2; if( g.argc==3 && fossil_strcmp(g.argv[1],"cgi")==0 ){ zFile = g.argv[2]; }else{ zFile = g.argv[1]; @@ -1852,10 +1909,19 @@ ** Grant "administrator" privileges to users connecting with HTTP ** from IP address 127.0.0.1. Do not bother checking credentials. */ g.useLocalauth = 1; continue; + } + if( blob_eq(&key, "repolist") ){ + /* repolist + ** + ** If using "directory:" and the URL is "/" then generate a page + ** showing a list of available repositories. + */ + allowRepoList = 1; + continue; } if( blob_eq(&key, "redirect:") && blob_token(&line, &value) && blob_token(&line, &value2) ){ /* See the header comment on the redirect_web_page() function ** above for details. */ @@ -1921,20 +1987,32 @@ */ cgi_setenv("HOME", blob_str(&value)); blob_reset(&value); continue; } + if( blob_eq(&key, "skin:") && blob_token(&line, &value) ){ + /* skin: LABEL + ** + ** Use one of the built-in skins defined by LABEL. LABEL is the + ** name of the subdirectory under the skins/ directory that holds + ** the elements of the built-in skin. If LABEL does not match, + ** this directive is a silent no-op. + */ + skin_use_alternative(blob_str(&value)); + blob_reset(&value); + continue; + } } blob_reset(&config); if( g.db==0 && g.zRepositoryName==0 && nRedirect==0 ){ cgi_panic("Unable to find or open the project repository"); } cgi_init(); if( nRedirect ){ redirect_web_page(nRedirect, azRedirect); }else{ - process_one_web_page(zNotFound, pFileGlob); + process_one_web_page(zNotFound, pFileGlob, allowRepoList); } } /* ** If g.argv[arg] exists then it is either the name of a repository @@ -1948,24 +2026,17 @@ ** Open the repository to be served if it is known. If g.argv[arg] is ** a directory full of repositories, then set g.zRepositoryName to ** the name of that directory and the specific repository will be ** opened later by process_one_web_page() based on the content of ** the PATH_INFO variable. -** -** If disallowDir is set, then the directory full of repositories method -** is disallowed. */ -static void find_server_repository(int disallowDir, int arg){ +static void find_server_repository(int arg){ if( g.argc<=arg ){ db_must_be_within_tree(); }else if( file_isdir(g.argv[arg])==1 ){ - if( disallowDir ){ - fossil_fatal("\"%s\" is a directory, not a repository file", g.argv[arg]); - }else{ - g.zRepositoryName = mprintf("%s", g.argv[arg]); - file_simplify_name(g.zRepositoryName, -1, 0); - } + g.zRepositoryName = mprintf("%s", g.argv[arg]); + file_simplify_name(g.zRepositoryName, -1, 0); }else{ db_open_repository(g.argv[arg]); } } @@ -2006,18 +2077,21 @@ ** If the --localauth option is given, then automatic login is performed ** for requests coming from localhost, if the "localauth" setting is not ** enabled. ** ** Options: +** --baseurl URL base URL (useful with reverse proxies) +** --files GLOB comma-separate glob patterns for static file to serve ** --localauth enable automatic login for local connections ** --host NAME specify hostname of the server ** --https signal a request coming in via https +** --nojail drop root privilege but do not enter the chroot jail ** --nossl signal that no SSL connections are available ** --notfound URL use URL as "HTTP 404, object not found" page. -** --files GLOB comma-separate glob patterns for static file to serve -** --baseurl URL base URL (useful with reverse proxies) +** --repolist If REPOSITORY is directory, URL "/" lists all repos ** --scgi Interpret input as SCGI rather than HTTP +** --skin LABEL Use override skin LABEL ** ** See also: cgi, server, winsrv */ void cmd_http(void){ const char *zIpAddr = 0; @@ -2024,10 +2098,12 @@ const char *zNotFound; const char *zHost; const char *zAltBase; const char *zFileGlob; int useSCGI; + int noJail; + int allowRepoList; /* The winhttp module passes the --files option as --files-urlenc with ** the argument being URL encoded, to avoid wildcard expansion in the ** shell. This option is for internal use and is undocumented. */ @@ -2037,11 +2113,14 @@ dehttpize(z); zFileGlob = z; }else{ zFileGlob = find_option("files",0,1); } + skin_override(); zNotFound = find_option("notfound", 0, 1); + noJail = find_option("nojail",0,0)!=0; + allowRepoList = find_option("repolist",0,0)!=0; g.useLocalauth = find_option("localauth", 0, 0)!=0; g.sslNotAvailable = find_option("nossl", 0, 0)!=0; useSCGI = find_option("scgi", 0, 0)!=0; zAltBase = find_option("baseurl", 0, 1); if( zAltBase ) set_base_url(zAltBase); @@ -2062,41 +2141,41 @@ g.fullHttpReply = 1; if( g.argc>=5 ){ g.httpIn = fossil_fopen(g.argv[2], "rb"); g.httpOut = fossil_fopen(g.argv[3], "wb"); zIpAddr = g.argv[4]; - find_server_repository(0, 5); + find_server_repository(5); }else{ g.httpIn = stdin; g.httpOut = stdout; - find_server_repository(0, 2); + find_server_repository(2); } if( zIpAddr==0 ){ zIpAddr = cgi_ssh_remote_addr(0); if( zIpAddr && zIpAddr[0] ){ g.fSshClient |= CGI_SSH_CLIENT; } } - g.zRepositoryName = enter_chroot_jail(g.zRepositoryName); + g.zRepositoryName = enter_chroot_jail(g.zRepositoryName, noJail); if( useSCGI ){ cgi_handle_scgi_request(); }else if( g.fSshClient & CGI_SSH_CLIENT ){ ssh_request_loop(zIpAddr, glob_create(zFileGlob)); }else{ cgi_handle_http_request(zIpAddr); } - process_one_web_page(zNotFound, glob_create(zFileGlob)); + process_one_web_page(zNotFound, glob_create(zFileGlob), allowRepoList); } /* ** Process all requests in a single SSH connection if possible. */ void ssh_request_loop(const char *zIpAddr, Glob *FileGlob){ blob_zero(&g.cgiIn); do{ cgi_handle_ssh_http_request(zIpAddr); - process_one_web_page(0, FileGlob); + process_one_web_page(0, FileGlob, 0); blob_reset(&g.cgiIn); } while ( g.fSshClient & CGI_SSH_FOSSIL || g.fSshClient & CGI_SSH_COMPAT ); } @@ -2113,21 +2192,21 @@ Th_InitTraceLog(); login_set_capabilities("sx", 0); g.useLocalauth = 1; g.httpIn = stdin; g.httpOut = stdout; - find_server_repository(0, 2); + find_server_repository(2); g.cgiOutput = 1; g.fullHttpReply = 1; zIpAddr = cgi_ssh_remote_addr(0); if( zIpAddr && zIpAddr[0] ){ g.fSshClient |= CGI_SSH_CLIENT; ssh_request_loop(zIpAddr, 0); }else{ cgi_set_parameter("REMOTE_ADDR", "127.0.0.1"); cgi_handle_http_request(0); - process_one_web_page(0, 0); + process_one_web_page(0, 0, 0); } } #if !defined(_WIN32) #if !defined(__DARWIN__) && !defined(__APPLE__) && !defined(__HAIKU__) @@ -2184,26 +2263,29 @@ ** "*.fossil*" will be served as static content. With the "ui" command, ** the REPOSITORY can only be a directory if the --notfound option is ** also present. ** ** By default, the "ui" command provides full administrative access without -** having to log in. This can be disabled by setting turning off the -** "localauth" setting. Automatic login for the "server" command is available -** if the --localauth option is present and the "localauth" setting is off -** and the connection is from localhost. The optional REPOSITORY argument -** to "ui" may be a directory and will function as "server" if and only if -** the --notfound option is used. +** having to log in. This can be disabled by turning off the "localauth" +** setting. Automatic login for the "server" command is available if the +** --localauth option is present and the "localauth" setting is off and the +** connection is from localhost. The "ui" command also enables --repolist +** by default. ** ** Options: +** --baseurl URL Use URL as the base (useful for reverse proxies) +** --files GLOBLIST Comma-separated list of glob patterns for static files ** --localauth enable automatic login for requests from localhost ** --localhost listen on 127.0.0.1 only (always true for "ui") +** --nojail Drop root privileges but do not enter the chroot jail +** --notfound URL Redirect ** -P|--port TCPPORT listen to request on port TCPPORT ** --th-trace trace TH1 execution (for debugging purposes) -** --baseurl URL Use URL as the base (useful for reverse proxies) -** --notfound URL Redirect -** --files GLOBLIST Comma-separated list of glob patterns for static files +** --repolist If REPOSITORY is dir, URL "/" lists repos. ** --scgi Accept SCGI rather than HTTP +** --skin LABEL Use override skin LABEL + ** ** See also: cgi, http, winsrv */ void cmd_webserver(void){ int iPort, mxPort; /* Range of TCP ports allowed */ @@ -2211,10 +2293,14 @@ const char *zBrowser; /* Name of web browser program */ char *zBrowserCmd = 0; /* Command to launch the web browser */ int isUiCmd; /* True if command is "ui", not "server' */ const char *zNotFound; /* The --notfound option or NULL */ int flags = 0; /* Server flags */ +#if !defined(_WIN32) + int noJail; /* Do not enter the chroot jail */ +#endif + int allowRepoList; /* List repositories on URL "/" */ const char *zAltBase; /* Argument to the --baseurl option */ const char *zFileGlob; /* Static content must match this */ char *zIpAddr = 0; /* Bind to this IP address */ #if defined(_WIN32) @@ -2228,14 +2314,19 @@ dehttpize(z); zFileGlob = z; }else{ zFileGlob = find_option("files",0,1); } + skin_override(); +#if !defined(_WIN32) + noJail = find_option("nojail",0,0)!=0; +#endif g.useLocalauth = find_option("localauth", 0, 0)!=0; Th_InitTraceLog(); zPort = find_option("port", "P", 1); zNotFound = find_option("notfound", 0, 1); + allowRepoList = find_option("repolist",0,0)!=0; zAltBase = find_option("baseurl", 0, 1); if( find_option("scgi", 0, 0)!=0 ) flags |= HTTP_SERVER_SCGI; if( zAltBase ){ set_base_url(zAltBase); } @@ -2247,14 +2338,15 @@ verify_all_options(); if( g.argc!=2 && g.argc!=3 ) usage("?REPOSITORY?"); isUiCmd = g.argv[1][0]=='u'; if( isUiCmd ){ - flags |= HTTP_SERVER_LOCALHOST; + flags |= HTTP_SERVER_LOCALHOST|HTTP_SERVER_REPOLIST; g.useLocalauth = 1; + allowRepoList = 1; } - find_server_repository(isUiCmd && zNotFound==0, 2); + find_server_repository(2); if( zPort ){ int i; for(i=strlen(zPort)-1; i>=0 && zPort[i]!=':'; i--){} if( i>0 ){ zIpAddr = mprintf("%.*s", i, zPort); @@ -2304,20 +2396,21 @@ g.httpOut = stdout; if( g.fHttpTrace || g.fSqlTrace ){ fprintf(stderr, "====== SERVER pid %d =======\n", getpid()); } g.cgiOutput = 1; - find_server_repository(isUiCmd && zNotFound==0, 2); - g.zRepositoryName = enter_chroot_jail(g.zRepositoryName); + find_server_repository(2); + g.zRepositoryName = enter_chroot_jail(g.zRepositoryName, noJail); if( flags & HTTP_SERVER_SCGI ){ cgi_handle_scgi_request(); }else{ cgi_handle_http_request(0); } - process_one_web_page(zNotFound, glob_create(zFileGlob)); + process_one_web_page(zNotFound, glob_create(zFileGlob), allowRepoList); #else /* Win32 implementation */ + (void)allowRepoList; /* Suppress warning */ if( isUiCmd ){ zBrowser = db_get("web-browser", "start"); if( zIpAddr ){ zBrowserCmd = mprintf("%s http://%s:%%d/ &", zBrowser, zIpAddr); }else{ Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -450,11 +450,12 @@ -DSQLITE_ENABLE_LOCKING_STYLE=0 \ -DSQLITE_THREADSAFE=0 \ -DSQLITE_DEFAULT_FILE_FORMAT=4 \ -DSQLITE_OMIT_DEPRECATED \ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \ - -DSQLITE_ENABLE_FTS4 + -DSQLITE_ENABLE_FTS4 \ + -DSQLITE_ENABLE_FTS3_PARENTHESIS # Setup the options used to compile the included SQLite shell. SHELL_OPTIONS = -Dmain=sqlite3_shell \ -DSQLITE_OMIT_LOAD_EXTENSION=1 \ -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) \ Index: src/makemake.tcl ================================================================== --- src/makemake.tcl +++ src/makemake.tcl @@ -158,10 +158,11 @@ -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_OMIT_DEPRECATED -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 + -DSQLITE_ENABLE_FTS3_PARENTHESIS } #lappend SQLITE_OPTIONS -DSQLITE_ENABLE_FTS3=1 #lappend SQLITE_OPTIONS -DSQLITE_ENABLE_STAT4 #lappend SQLITE_OPTIONS -DSQLITE_WIN32_NO_ANSI #lappend SQLITE_OPTIONS -DSQLITE_WINNT_MAX_PATH_CHARS=4096 Index: src/manifest.c ================================================================== --- src/manifest.c +++ src/manifest.c @@ -1621,40 +1621,40 @@ if( fossil_strcmp(pManifest->aField[i].zName, zStatusColumn)==0 ){ zNewStatus = pManifest->aField[i].zValue; } } if( zNewStatus ){ - blob_appendf(&comment, "%h ticket [%s|%S]: %h", + blob_appendf(&comment, "%h ticket [%!S|%S]: %h", zNewStatus, pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle ); if( pManifest->nField>1 ){ blob_appendf(&comment, " plus %d other change%s", pManifest->nField-1, pManifest->nField==2 ? "" : "s"); } - blob_appendf(&brief, "%h ticket [%s|%S].", + blob_appendf(&brief, "%h ticket [%!S|%S].", zNewStatus, pManifest->zTicketUuid, pManifest->zTicketUuid); }else{ zNewStatus = db_text("unknown", "SELECT \"%w\" FROM ticket WHERE tkt_uuid=%Q", zStatusColumn, pManifest->zTicketUuid ); - blob_appendf(&comment, "Ticket [%s|%S] %h status still %h with " + blob_appendf(&comment, "Ticket [%!S|%S] %h status still %h with " "%d other change%s", pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle, zNewStatus, pManifest->nField, pManifest->nField==1 ? "" : "s" ); fossil_free(zNewStatus); - blob_appendf(&brief, "Ticket [%s|%S]: %d change%s", + blob_appendf(&brief, "Ticket [%!S|%S]: %d change%s", pManifest->zTicketUuid, pManifest->zTicketUuid, pManifest->nField, pManifest->nField==1 ? "" : "s" ); } }else{ - blob_appendf(&comment, "New ticket [%s|%S] %h.", + blob_appendf(&comment, "New ticket [%!S|%S] %h.", pManifest->zTicketUuid, pManifest->zTicketUuid, zTitle ); - blob_appendf(&brief, "New ticket [%s|%S].", pManifest->zTicketUuid, + blob_appendf(&brief, "New ticket [%!S|%S].", pManifest->zTicketUuid, pManifest->zTicketUuid); } fossil_free(zTitle); db_multi_exec( "REPLACE INTO event(type,tagid,mtime,objid,user,comment,brief)" @@ -1756,11 +1756,11 @@ if( (p = manifest_cache_find(rid))!=0 ){ blob_reset(pContent); }else if( (p = manifest_parse(pContent, rid, 0))==0 ){ assert( blob_is_reset(pContent) || pContent==0 ); if( (flags & MC_NO_ERRORS)==0 ){ - fossil_error(1, "syntax error in manifest [%s]", + fossil_error(1, "syntax error in manifest [%S]", db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid)); } return 0; } if( g.xlinkClusterOnly && p->type!=CFTYPE_CLUSTER ){ @@ -1771,11 +1771,11 @@ } if( p->type==CFTYPE_MANIFEST && fetch_baseline(p, 0) ){ manifest_destroy(p); assert( blob_is_reset(pContent) ); if( (flags & MC_NO_ERRORS)==0 ){ - fossil_error(1, "cannot fetch baseline for manifest [%s]", + fossil_error(1, "cannot fetch baseline for manifest [%S]", db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid)); } return 0; } db_begin_transaction(); @@ -2031,23 +2031,23 @@ p->zAttachTarget, p->zAttachName ); if( 'w' == attachToType ){ if( isAdd ){ zComment = mprintf( - "Add attachment [/artifact/%s|%h] to wiki page [%h]", + "Add attachment [/artifact/%!S|%h] to wiki page [%h]", p->zAttachSrc, p->zAttachName, p->zAttachTarget); }else{ zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]", p->zAttachName, p->zAttachTarget); } }else{ if( isAdd ){ zComment = mprintf( - "Add attachment [/artifact/%s|%h] to ticket [%s|%S]", + "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); }else{ - zComment = mprintf("Delete attachment \"%h\" from ticket [%s|%S]", + zComment = mprintf("Delete attachment \"%h\" from ticket [%!S|%S]", p->zAttachName, p->zAttachTarget, p->zAttachTarget); } } db_multi_exec( "REPLACE INTO event(type,mtime,objid,user,comment)" @@ -2072,11 +2072,11 @@ for(i=0; inTag; i++){ zTagUuid = p->aTag[i].zUuid; if( !zTagUuid ) continue; if( i==0 || fossil_strcmp(zTagUuid, p->aTag[i-1].zUuid)!=0 ){ blob_appendf(&comment, - " Edit [%s|%S]:", + " Edit [%!S|%S]:", zTagUuid, zTagUuid); branchMove = 0; if( permitHooks && db_exists("SELECT 1 FROM event, blob" " WHERE event.type='ci' AND event.objid=blob.rid" " AND blob.uuid=%Q", zTagUuid) ){ @@ -2086,11 +2086,11 @@ } zName = p->aTag[i].zName; zValue = p->aTag[i].zValue; if( strcmp(zName, "*branch")==0 ){ blob_appendf(&comment, - " Move to branch [/timeline?r=%h&nd&dp=%s&unhide | %h].", + " Move to branch [/timeline?r=%h&nd&dp=%!S&unhide | %h].", zValue, zTagUuid, zValue); branchMove = 1; continue; }else if( strcmp(zName, "*bgcolor")==0 ){ blob_appendf(&comment, Index: src/markdown.c ================================================================== --- src/markdown.c +++ src/markdown.c @@ -842,11 +842,13 @@ return end; } } -/* get_link_inline -- extract inline-style link and title from parenthesed data*/ +/* get_link_inline -- extract inline-style link and title from +** parenthesed data +*/ static int get_link_inline( struct Blob *link, struct Blob *title, char *data, size_t size @@ -1522,11 +1524,11 @@ if( !inter ){ if( rndr->make.listitem ){ rndr->make.listitem(ob, work, *flags, rndr->make.opaque); } if( work!=&fallback ) release_work_buffer(rndr, work); - blob_zero(&fallback); + blob_reset(&fallback); return beg; } /* render of li contents */ if( has_inside_empty ) *flags |= MKD_LI_BLOCK; @@ -1558,11 +1560,11 @@ if( rndr->make.listitem ){ rndr->make.listitem(ob, inter, *flags, rndr->make.opaque); } release_work_buffer(rndr, inter); if( work!=&fallback ) release_work_buffer(rndr, work); - blob_zero(&fallback); + blob_reset(&fallback); return beg; } /* parse_list -- parsing ordered or unordered list block */ @@ -1584,11 +1586,11 @@ if( !j || (flags & MKD_LI_END) ) break; } if( rndr->make.list ) rndr->make.list(ob, work, flags, rndr->make.opaque); if( work!=&fallback ) release_work_buffer(rndr, work); - blob_zero(&fallback); + blob_reset(&fallback); return i; } /* parse_atxheader -- parsing of atx-style headers */ @@ -1631,11 +1633,15 @@ } /* htmlblock_end -- checking end of HTML block : [ \t]*\n[ \t*]\n */ /* returns the length on match, 0 otherwise */ -static size_t htmlblock_end(const struct html_tag *tag, const char *data, size_t size){ +static size_t htmlblock_end( + const struct html_tag *tag, + const char *data, + size_t size +){ size_t i, w; /* assuming data[0]=='<' && data[1]=='/' already tested */ /* checking tag is a match */ @@ -2224,17 +2230,17 @@ if( rndr.make.prolog ) rndr.make.prolog(ob, rndr.make.opaque); parse_block(ob, &rndr, blob_buffer(&text), blob_size(&text)); if( rndr.make.epilog ) rndr.make.epilog(ob, rndr.make.opaque); /* clean-up */ - blob_zero(&text); + blob_reset(&text); lr = (struct link_ref *)blob_buffer(&rndr.refs); end = blob_size(&rndr.refs)/sizeof(struct link_ref); for(i=0; i\n" */ +#define PROLOG_SIZE 23 static void html_prolog(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); BLOB_APPEND_LITTERAL(ob, "
      \n"); + assert( blob_size(ob)==PROLOG_SIZE ); } static void html_epilog(struct Blob *ob, void *opaque){ INTER_BLOCK(ob); BLOB_APPEND_LITTERAL(ob, "
      \n"); @@ -126,13 +130,12 @@ void *opaque ){ struct Blob *title = opaque; /* The first header at the beginning of a text is considered as * a title and not output. */ - if( blob_size(ob)==0 && blob_size(title)==0 ){ + if( blob_size(ob)<=PROLOG_SIZE && blob_size(title)==0 ){ BLOB_APPEND_BLOB(title, text); - return; } INTER_BLOCK(ob); blob_appendf(ob, "", level); BLOB_APPEND_BLOB(ob, text); blob_appendf(ob, "", level); Index: src/moderate.c ================================================================== --- src/moderate.c +++ src/moderate.c @@ -144,11 +144,14 @@ void modreq_page(void){ Blob sql; Stmt q; login_check_credentials(); - if( !g.perm.RdWiki && !g.perm.RdTkt ){ login_needed(); return; } + if( !g.perm.RdWiki && !g.perm.RdTkt ){ + login_needed(g.anon.RdWiki && g.anon.RdTkt); + return; + } style_header("Pending Moderation Requests"); @

      All Pending Moderation Requests

      if( moderation_table_exists() ){ blob_init(&sql, timeline_query_for_www(), -1); blob_append_sql(&sql, Index: src/name.c ================================================================== --- src/name.c +++ src/name.c @@ -42,10 +42,20 @@ if( z[7]!='-') return 0; if( !fossil_isdigit(z[8]) ) return 0; if( !fossil_isdigit(z[9]) ) return 0; return 1; } + +/* +** Return the name of the branch containing RID -OR- zero if not found. +*/ +char *name_of_branch(int rid){ + return db_text(0,"SELECT value FROM tagxref" + " WHERE rid=%d AND tagid=%d" + " AND tagtype>0", + rid, TAG_BRANCH); +} /* ** Return the RID that is the "root" of the branch that contains ** check-in "rid" if inBranch==0 or the first check-in in the branch ** if inBranch==1. @@ -104,10 +114,15 @@ ** If zType is NULL or "" or "*" then any type of artifact will serve. ** If zType is "br" then find the first check-in of the named branch ** rather than the last. ** zType is "ci" in most use cases since we are usually searching for ** a check-in. +** +** Note that the input zTag for types "t" and "e" is the SHA1 hash of +** the ticket-change or event-change artifact, not the randomly generated +** hexadecimal identifier assigned to tickets and events. Those identifiers +** live in a separate namespace. */ int symbolic_name_to_rid(const char *zTag, const char *zType){ int vid; int rid = 0; int nTag; @@ -453,11 +468,11 @@ canonical16(z, strlen(z)); db_prepare(&q, "SELECT uuid, rid FROM blob WHERE uuid GLOB '%q*'", z); while( db_step(&q)==SQLITE_ROW ){ const char *zUuid = db_column_text(&q, 0); int rid = db_column_int(&q, 1); - @
    • + @

    • @ %s(zUuid) - object_description(rid, 0, 0); @

    • } db_finalize(&q); @@ -470,11 +485,11 @@ " ORDER BY tkt_ctime DESC", z); while( db_step(&q)==SQLITE_ROW ){ int rid = db_column_int(&q, 0); const char *zUuid = db_column_text(&q, 1); const char *zTitle = db_column_text(&q, 2); - @
    • + @

    • @ %s(zUuid) - @

        @ Ticket hyperlink_to_uuid(zUuid); @ - %s(zTitle). @@ -490,11 +505,11 @@ " FROM tagxref, tag WHERE tagxref.tagid = tag.tagid" " AND tagname GLOB 'event-%q*') GROUP BY uuid", z); while( db_step(&q)==SQLITE_ROW ){ int rid = db_column_int(&q, 0); const char* zUuid = db_column_text(&q, 1); - @
      • + @

      • @ %s(zUuid) - @

        • object_description(rid, 0, 0); @
        @

      • @@ -786,18 +801,18 @@ } /* ** Schema for the description table */ -static const char zDescTab[] = +static const char zDescTab[] = @ CREATE TEMP TABLE IF NOT EXISTS description( -@ rid INTEGER PRIMARY KEY, -- RID of the object -@ uuid TEXT, -- SHA1 hash of the object -@ ctime DATETIME, -- Time of creation +@ rid INTEGER PRIMARY KEY, -- RID of the object +@ uuid TEXT, -- SHA1 hash of the object +@ ctime DATETIME, -- Time of creation @ isPrivate BOOLEAN DEFAULT 0, -- True for unpublished artifacts @ type TEXT, -- file, checkin, wiki, ticket, etc. -@ summary TEXT, -- Summary comment for the object +@ summary TEXT, -- Summary comment for the object @ detail TEXT -- filename, checkin comment, etc @ ); ; /* @@ -991,11 +1006,11 @@ int n = atoi(PD("n","5000")); int mx = db_int(0, "SELECT max(rid) FROM blob"); char *zRange; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } style_header("List Of Artifacts"); if( mx>n && P("s")==0 ){ int i; @

        Select a range of artifacts to view:

        @
          @@ -1020,11 +1035,11 @@ int rid = db_column_int(&q,0); const char *zUuid = db_column_text(&q, 1); const char *zDesc = db_column_text(&q, 2); int isPriv = db_column_int(&q,2); @
      %d(rid) %z(href("%R/info/%s",zUuid))%s(zUuid)  %z(href("%R/info/%!S",zUuid))%s(zUuid) %h(zDesc)(unpublished)
      LengthInstancesFirst Instance
      %d(i)%d(aCollide[i].cnt)%h(aCollide[i].z)
      + @

      Total number of hashes: %d(nHash)

      + kk = 0; + for(i=UUID_SIZE; i>=4; i--){ + if( aCollide[i].cnt==0 ) continue; + if( aCollide[i].cnt>200 ) break; + kk += aCollide[i].cnt; + if( aCollide[i].cnt<25 ){ + @

      Collisions of length %d(i): + }else{ + @

      First 25 collisions of length %d(i): + } + for(j=0; j + } + } + style_footer(); +} Index: src/path.c ================================================================== --- src/path.c +++ src/path.c @@ -543,11 +543,11 @@ */ void test_rename_list_page(void){ Stmt q; login_check_credentials(); - if( !g.perm.Read ){ login_needed(); return; } + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } style_header("List Of File Name Changes"); @

      NB: Experimental Page

      @ @ @ @@ -561,11 +561,11 @@ const char *zUuid = db_column_text(&q, 3); @ @ @ @ - @ + @ } @
      Date & TimeOld Name
      %z(href("%R/timeline?c=%t",zDate))%s(zDate)%z(href("%R/finfo?name=%t",zOld))%h(zOld)%z(href("%R/finfo?name=%t",zNew))%h(zNew)%z(href("%R/info/%s",zUuid))%S(zUuid)
      %z(href("%R/info/%!S",zUuid))%S(zUuid)
      db_finalize(&q); style_footer(); } Index: src/printf.c ================================================================== --- src/printf.c +++ src/printf.c @@ -23,10 +23,48 @@ #if defined(_WIN32) # include # include #endif #include + +/* Two custom conversions are used to show a prefix of SHA1 hashes: +** +** %!S Prefix of a length appropriate for URLs +** %S Prefix of a length appropriate for human display +** +** The following macros help determine those lengths. FOSSIL_HASH_DIGITS +** is the default number of digits to display to humans. This value can +** be overridden using the hash-digits setting. FOSSIL_HASH_DIGITS_URL +** is the minimum number of digits to be used in URLs. The number used +** will always be at least 6 more than the number used for human output, +** or 40 if the number of digits in human output is 34 or more. +*/ +#ifndef FOSSIL_HASH_DIGITS +# define FOSSIL_HASH_DIGITS 10 /* For %S (human display) */ +#endif +#ifndef FOSSIL_HASH_DIGITS_URL +# define FOSSIL_HASH_DIGITS_URL 16 /* For %!S (embedded in URLs) */ +#endif + +/* +** Return the number of SHA1 hash digits to display. The number is for +** human output if the bForUrl is false and is destined for a URL if +** bForUrl is false. +*/ +static int hashDigits(int bForUrl){ + static int nDigitHuman = 0; + static int nDigitUrl = 0; + if( nDigitHuman==0 ){ + nDigitHuman = db_get_int("hash-digits", FOSSIL_HASH_DIGITS); + if( nDigitHuman < 6 ) nDigitHuman = 6; + if( nDigitHuman > 40 ) nDigitHuman = 40; + nDigitUrl = nDigitHuman + 6; + if( nDigitUrl < FOSSIL_HASH_DIGITS_URL ) nDigitUrl = FOSSIL_HASH_DIGITS_URL; + if( nDigitUrl > 40 ) nDigitUrl = 40; + } + return bForUrl ? nDigitUrl : nDigitHuman; +} /* ** Conversion types fall into various categories as defined by the ** following enumeration. */ @@ -620,16 +658,11 @@ if( bufpt==0 ){ bufpt = ""; }else if( xtype==etDYNSTRING ){ zExtra = bufpt; }else if( xtype==etSTRINGID ){ - precision = 0; - while( bufpt[precision]>='0' && bufpt[precision]<='9' ){ - precision++; - } - if( bufpt[precision]!=0 ) precision++; - if( precision<10 ) precision=10; + precision = hashDigits(flag_altform2); } length = StrNLen32(bufpt, limit); if( precision>=0 && precision\n", -1); zScript = ticket_reportlist_code(); if( g.thTrace ) Th_Trace("BEGIN_REPORTLIST_SCRIPT
      \n", -1); @@ -293,11 +296,11 @@ const char *zClrKey; Stmt q; login_check_credentials(); if( !g.perm.TktFmt ){ - login_needed(); + login_needed(g.anon.TktFmt); return; } rn = atoi(PD("rn","0")); db_prepare(&q, "SELECT title, sqlcode, owner, cols " "FROM reportfmt WHERE rn=%d",rn); @@ -343,11 +346,11 @@ char *zSQL; char *zErr = 0; login_check_credentials(); if( !g.perm.TktFmt ){ - login_needed(); + login_needed(g.anon.TktFmt); return; } /*view_add_functions(0);*/ rn = atoi(PD("rn","0")); zTitle = P("t"); @@ -1078,11 +1081,11 @@ Stmt q; char *zErr1 = 0; char *zErr2 = 0; login_check_credentials(); - if( !g.perm.RdTkt ){ login_needed(); return; } + if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } rn = atoi(PD("rn","0")); if( rn==0 ){ cgi_redirect("reportlist"); return; } Index: src/search.c ================================================================== --- src/search.c +++ src/search.c @@ -213,11 +213,11 @@ aiLastDoc[j] = iDoc; aiLastOfst[j] = i; for(k=1; j-k>=0 && anMatch[j-k] && aiWordIdx[j-k]==iWord-k; k++){} for(ii=0; ii>= 1; } + return n; +} /* ** Implemenation of the rank() function used with rank(matchinfo(*,'pcsx')). */ static void search_rank_sqlfunc( @@ -694,24 +743,45 @@ int argc, sqlite3_value **argv ){ const unsigned *aVal = (unsigned int*)sqlite3_value_blob(argv[0]); int nVal = sqlite3_value_bytes(argv[0])/4; + int nCol; /* Number of columns in the index */ int nTerm; /* Number of search terms in the query */ - int i; /* Loop counter */ - double r = 1.0; /* Score */ + int i, j; /* Loop counter */ + double r = 0.0; /* Score */ + const unsigned *aX, *aS; - if( nVal<6 ) return; - if( aVal[1]!=1 ) return; + if( nVal<2 ) return; nTerm = aVal[0]; - r *= 1<<((30*(aVal[2]-1))/nTerm); - for(i=1; i<=nTerm; i++){ - int hits_this_row = aVal[3*i]; - int hits_all_rows = aVal[3*i+1]; - int rows_with_hit = aVal[3*i+2]; - double avg_hits_per_row = (double)hits_all_rows/(double)rows_with_hit; - r *= hits_this_row/avg_hits_per_row; + nCol = aVal[1]; + if( nVal<2+3*nCol*nTerm+nCol ) return; + aS = aVal+2; + aX = aS+nCol; + for(j=0; j0 ){ + x = 0.0; + for(i=0; i','',' ... ',-1,35)" " FROM ftsidx CROSS JOIN ftsdocs" " WHERE ftsidx MATCH %Q" " AND ftsdocs.rowid=ftsidx.docid", @@ -838,29 +909,30 @@ ** ** Return the number of rows. */ int search_run_and_output( const char *zPattern, /* The query pattern */ - unsigned int srchFlags /* What to search over */ + unsigned int srchFlags, /* What to search over */ + int fDebug /* Extra debugging output */ ){ Stmt q; int nRow = 0; srchFlags = search_restrict(srchFlags); if( srchFlags==0 ) return 0; search_sql_setup(g.db); add_content_sql_commands(g.db); db_multi_exec( - "CREATE TEMP TABLE x(label,url,score,date,snip);" + "CREATE TEMP TABLE x(label,url,score,id,date,snip);" ); if( !search_index_exists() ){ search_fullscan(zPattern, srchFlags); }else{ search_update_index(srchFlags); search_indexed(zPattern, srchFlags); } - db_prepare(&q, "SELECT url, snip, label" + db_prepare(&q, "SELECT url, snip, label, score, id" " FROM x" " ORDER BY score DESC, date DESC;"); while( db_step(&q)==SQLITE_ROW ){ const char *zUrl = db_column_text(&q, 0); const char *zSnippet = db_column_text(&q, 1); @@ -867,12 +939,15 @@ const char *zLabel = db_column_text(&q, 2); if( nRow==0 ){ @
        } nRow++; - @
      1. %h(zLabel)
        - @ %z(cleanSnippet(zSnippet))

      2. + @
      3. %h(zLabel) + if( fDebug ){ + @ (%e(db_column_double(&q,3)), %s(db_column_text(&q,4))) + } + @
        %z(cleanSnippet(zSnippet))

      4. } db_finalize(&q); if( nRow ){ @
      } @@ -900,10 +975,11 @@ const char *zType = 0; const char *zClass = 0; const char *zDisable1; const char *zDisable2; const char *zPattern; + int fDebug = PB("debug"); srchFlags = search_restrict(srchFlags); switch( srchFlags ){ case SRCH_CKIN: zType = " Check-ins"; zClass = "Ckin"; break; case SRCH_DOC: zType = " Docs"; zClass = "Doc"; break; case SRCH_TKT: zType = " Tickets"; zClass = "Tkt"; break; @@ -947,10 +1023,13 @@ cgi_printf(">%s\n", aY[i].zNm); } @ srchFlags = newFlags; } + if( fDebug ){ + @ + } @ if( srchFlags==0 ){ @

      Search is disabled

      } @
      @@ -959,11 +1038,11 @@ if( zClass ){ @
      }else{ @
      } - if( search_run_and_output(zPattern, srchFlags)==0 ){ + if( search_run_and_output(zPattern, srchFlags, fDebug)==0 ){ @

      No matches for: %h(zPattern)

      } @
      } } @@ -983,10 +1062,14 @@ /* ** This is a helper function for search_stext(). Writing into pOut ** the search text obtained from pIn according to zMimetype. +** +** The title of the document is the first line of text. All subsequent +** lines are the body. If the document has no title, the first line +** is blank. */ static void get_stext_by_mimetype( Blob *pIn, const char *zMimetype, Blob *pOut @@ -994,41 +1077,74 @@ Blob html, title; blob_init(&html, 0, 0); blob_init(&title, 0, 0); if( zMimetype==0 ) zMimetype = "text/plain"; if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")==0 ){ - wiki_convert(pIn, &html, 0); + Blob tail; + blob_init(&tail, 0, 0); + if( wiki_find_title(pIn, &title, &tail) ){ + blob_appendf(pOut, "%s\n", blob_str(&title)); + wiki_convert(&tail, &html, 0); + blob_reset(&tail); + }else{ + blob_append(pOut, "\n", 1); + wiki_convert(pIn, &html, 0); + } html_to_plaintext(blob_str(&html), pOut); }else if( fossil_strcmp(zMimetype,"text/x-markdown")==0 ){ markdown_to_html(pIn, &title, &html); + if( blob_size(&title) ){ + blob_appendf(pOut, "%s\n", blob_str(&title)); + }else{ + blob_append(pOut, "\n", 1); + } html_to_plaintext(blob_str(&html), pOut); }else if( fossil_strcmp(zMimetype,"text/html")==0 ){ + if( doc_is_embedded_html(pIn, &title) ){ + blob_appendf(pOut, "%s\n", blob_str(&title)); + } html_to_plaintext(blob_str(pIn), pOut); }else{ - *pOut = *pIn; - blob_init(pIn, 0, 0); + blob_append(pOut, blob_buffer(pIn), blob_size(pIn)); } blob_reset(&html); blob_reset(&title); } /* ** Query pQuery is pointing at a single row of output. Append a text ** representation of every text-compatible column to pAccum. */ -static void append_all_ticket_fields(Blob *pAccum, Stmt *pQuery){ +static void append_all_ticket_fields(Blob *pAccum, Stmt *pQuery, int iTitle){ int n = db_column_count(pQuery); int i; + const char *zMime = 0; + if( iTitle>=0 && iTitle0)" " ||')'" " FROM event WHERE objid=:x AND type='ci'"); + if( isPlainText<0 ){ + isPlainText = db_get_boolean("timeline-plaintext",0); + } db_bind_int(&q, ":x", rid); if( db_step(&q)==SQLITE_ROW ){ - db_column_blob(&q, 0, pOut); blob_append(pOut, "\n", 1); + if( isPlainText ){ + db_column_blob(&q, 0, pOut); + }else{ + Blob x; + blob_init(&x,0,0); + db_column_blob(&q, 0, &x); + get_stext_by_mimetype(&x, "text/x-fossil-wiki", pOut); + blob_reset(&x); + } } db_reset(&q); break; } case 't': { /* Tickets */ static Stmt q1; - Blob raw; + static int iTitle = -1; db_static_prepare(&q1, "SELECT * FROM ticket WHERE tkt_id=:rid"); - blob_init(&raw,0,0); db_bind_int(&q1, ":rid", rid); if( db_step(&q1)==SQLITE_ROW ){ - append_all_ticket_fields(&raw, &q1); + if( iTitle<0 ){ + int n = db_column_count(&q1); + for(iTitle=0; iTitle0 ){ + blob_reset(&cache.stext); + }else{ + blob_init(&cache.stext,0,0); + } + cache.cType = cType; + cache.rid = rid; + if( cType==0 ) return 0; + search_stext(cType, rid, zName, &cache.stext); + z = blob_str(&cache.stext); + for(i=0; z[i] && z[i]!='\n'; i++){} + cache.nTitle = i; + } + if( pnTitle ) *pnTitle = cache.nTitle; + return blob_str(&cache.stext); +} /* ** COMMAND: test-search-stext ** ** Usage: fossil test-search-stext TYPE ARG1 ARG2 @@ -1131,10 +1303,30 @@ if( g.argc!=5 ) usage("TYPE RID NAME"); search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out); fossil_print("%s\n",blob_str(&out)); blob_reset(&out); } + +/* +** COMMAND: test-convert-stext +** +** Usage: fossil test-convert-stext FILE MIMETYPE +** +** Read the content of FILE and convert it to stext according to MIMETYPE. +** Send the result to standard output. +*/ +void test_convert_stext(void){ + Blob in, out; + db_find_and_open_repository(0,0); + if( g.argc!=4 ) usage("FILENAME MIMETYPE"); + blob_read_from_file(&in, g.argv[2]); + blob_init(&out, 0, 0); + get_stext_by_mimetype(&in, g.argv[3], &out); + fossil_print("%s\n",blob_str(&out)); + blob_reset(&in); + blob_reset(&out); +} /* The schema for the full-text index */ static const char zFtsSchema[] = @ -- One entry for each possible search result @@ -1145,20 +1337,21 @@ @ name TEXT, -- Additional document description @ idxed BOOLEAN, -- True if currently in the index @ label TEXT, -- Label to print on search results @ url TEXT, -- URL to access this document @ mtime DATE, -- Date when document created +@ bx TEXT, -- Temporary "body" content cache @ UNIQUE(type,rid) @ ); @ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0; @ CREATE INDEX "%w".ftsdocName ON ftsdocs(name) WHERE type='w'; @ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS @ SELECT rowid, type, rid, name, idxed, label, url, mtime, -@ stext(type,rid,name) AS 'stext' +@ title(type,rid,name) AS 'title', body(type,rid,name) AS 'body' @ FROM ftsdocs; @ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx -@ USING fts4(content="ftscontent", stext); +@ USING fts4(content="ftscontent", title, body%s); ; static const char zFtsDrop[] = @ DROP TABLE IF EXISTS "%w".ftsidx; @ DROP VIEW IF EXISTS "%w".ftscontent; @ DROP TABLE IF EXISTS "%w".ftsdocs; @@ -1168,13 +1361,15 @@ ** Create or drop the tables associated with a full-text index. */ static int searchIdxExists = -1; void search_create_index(void){ const char *zDb = db_name("repository"); + int useStemmer = db_get_boolean("search-stemmer",0); + const char *zExtra = useStemmer ? ",tokenize=porter" : ""; search_sql_setup(g.db); - db_multi_exec(zFtsSchema/*works-like:"%w%w%w%w%w"*/, - zDb, zDb, zDb, zDb, zDb); + db_multi_exec(zFtsSchema/*works-like:"%w%w%w%w%w%s"*/, + zDb, zDb, zDb, zDb, zDb, zExtra/*safe-for-%s*/); searchIdxExists = 1; } void search_drop_index(void){ const char *zDb = db_name("repository"); db_multi_exec(zFtsDrop/*works-like:"%w%w%w"*/, zDb, zDb, zDb); @@ -1292,34 +1487,39 @@ db_multi_exec( "DELETE FROM ftsdocs WHERE type='d'" " AND rid NOT IN (SELECT rid FROM current_docs)" ); db_multi_exec( - "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed,label,url,mtime)" + "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed,label,bx,url,mtime)" " SELECT 'd', rid, name, 0," - " printf('Document: %%s',name)," + " title('d',rid,name)," + " body('d',rid,name)," " printf('/doc/%q/%%s',urlencode(name))," " %.17g" " FROM current_docs", zBrUuid, rTime ); db_multi_exec( - "INSERT INTO ftsidx(docid,stext)" - " SELECT rowid, stext FROM ftscontent WHERE type='d' AND NOT idxed" + "INSERT INTO ftsidx(docid,title,body)" + " SELECT rowid, label, bx FROM ftsdocs WHERE type='d' AND NOT idxed" ); db_multi_exec( - "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed" + "UPDATE ftsdocs SET" + " idxed=1," + " bx=NULL," + " label='Document: '||label" + " WHERE type='d' AND NOT idxed" ); } /* ** Deal with all of the unindexed 'c' terms in FTSDOCS */ static void search_update_checkin_index(void){ db_multi_exec( - "INSERT INTO ftsidx(docid,stext)" - " SELECT rowid, stext('c',rid,NULL) FROM ftsdocs" + "INSERT INTO ftsidx(docid,title,body)" + " SELECT rowid, '', body('c',rid,NULL) FROM ftsdocs" " WHERE type='c' AND NOT idxed;" ); db_multi_exec( "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)" " SELECT ftsdocs.rowid, 1, 'c', ftsdocs.rid, NULL," @@ -1336,19 +1536,20 @@ /* ** Deal with all of the unindexed 't' terms in FTSDOCS */ static void search_update_ticket_index(void){ db_multi_exec( - "INSERT INTO ftsidx(docid,stext)" - " SELECT rowid, stext('t',rid,NULL) FROM ftsdocs" + "INSERT INTO ftsidx(docid,title,body)" + " SELECT rowid, title('t',rid,NULL), body('t',rid,NULL) FROM ftsdocs" " WHERE type='t' AND NOT idxed;" ); if( db_changes()==0 ) return; db_multi_exec( "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)" " SELECT ftsdocs.rowid, 1, 't', ftsdocs.rid, NULL," - " printf('Ticket [%%.16s] on %%s',tkt_uuid,datetime(tkt_mtime))," + " printf('Ticket: %%s (%%s)',title('t',tkt_id,null)," + " datetime(tkt_mtime))," " printf('/tktview/%%.20s',tkt_uuid)," " tkt_mtime" " FROM ftsdocs, ticket" " WHERE ftsdocs.type='t' AND NOT ftsdocs.idxed" " AND ticket.tkt_id=ftsdocs.rid" @@ -1358,12 +1559,12 @@ /* ** Deal with all of the unindexed 'w' terms in FTSDOCS */ static void search_update_wiki_index(void){ db_multi_exec( - "INSERT INTO ftsidx(docid,stext)" - " SELECT rowid, stext('w',rid,NULL) FROM ftsdocs" + "INSERT INTO ftsidx(docid,title,body)" + " SELECT rowid, title('w',rid,NULL),body('w',rid,NULL) FROM ftsdocs" " WHERE type='w' AND NOT idxed;" ); if( db_changes()==0 ) return; db_multi_exec( "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)" @@ -1416,19 +1617,22 @@ ** Usage: fossil fts-config ?SUBCOMMAND? ?ARGUMENT? ** ** The "fossil fts-config" command configures the full-text search capabilities ** of the repository. Subcommands: ** -** reindex Rebuild the search index. Create it if it does -** not already exist +** reindex Rebuild the search index. This is a no-op if +** index search is disabled ** ** index (on|off) Turn the search index on or off ** ** enable cdtw Enable various kinds of search. c=Check-ins, ** d=Documents, t=Tickets, w=Wiki. ** ** disable cdtw Disable versious kinds of search +** +** stemmer (on|off) Turn the Porter stemmer on or off for indexed +** search. (Unindexed search is never stemmed.) ** ** The current search settings are displayed after any changes are applied. ** Run this command with no arguments to simply see the settings. */ void test_fts_cmd(void){ @@ -1435,18 +1639,19 @@ static const struct { int iCmd; const char *z; } aCmd[] = { { 1, "reindex" }, { 2, "index" }, { 3, "disable" }, { 4, "enable" }, + { 5, "stemmer" }, }; static const struct { char *zSetting; char *zName; char *zSw; } aSetng[] = { - { "search-ckin", "check-in search:", "c" }, - { "search-doc", "document search:", "d" }, - { "search-tkt", "ticket search:", "t" }, - { "search-wiki", "wiki search:", "w" }, + { "search-ckin", "check-in search:", "c" }, + { "search-doc", "document search:", "d" }, + { "search-tkt", "ticket search:", "t" }, + { "search-wiki", "wiki search:", "w" }, }; - char *zSubCmd; + char *zSubCmd = 0; int i, j, n; int iCmd = 0; int iAction = 0; db_find_and_open_repository(0, 0); if( g.argc>2 ){ @@ -1464,11 +1669,11 @@ return; } iCmd = aCmd[i].iCmd; } if( iCmd==1 ){ - iAction = 2; + if( search_index_exists() ) iAction = 2; } if( iCmd==2 ){ if( g.argc<3 ) usage("index (on|off)"); iAction = 1 + is_truth(g.argv[3]); } @@ -1475,18 +1680,23 @@ db_begin_transaction(); /* Adjust search settings */ if( iCmd==3 || iCmd==4 ){ const char *zCtrl; - if( g.argc<4 ) usage("enable STRING"); + if( g.argc<4 ) usage(mprintf("%s STRING",zSubCmd)); zCtrl = g.argv[3]; for(j=0; j=1 ){ search_drop_index(); } @@ -1497,14 +1707,16 @@ /* Always show the status before ending */ for(i=0; i. Issue a warning @@ -152,11 +152,11 @@ Stmt s; int prevLevel = 0; login_check_credentials(); if( !g.perm.Admin ){ - login_needed(); + login_needed(0); return; } style_submenu_element("Add", "Add User", "setup_uedit"); style_header("User List"); @@ -336,11 +336,11 @@ const char *oa[128]; /* Must have ADMIN privileges to access this page */ login_check_credentials(); - if( !g.perm.Admin ){ login_needed(); return; } + if( !g.perm.Admin ){ login_needed(0); return; } /* Check to see if an ADMIN user is trying to edit a SETUP account. ** Don't allow that. */ zId = PD("id", "0"); @@ -998,11 +998,12 @@ ** WEBPAGE: setup_access */ void setup_access(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } style_header("Access Control Settings"); db_begin_transaction(); @
      @@ -1018,25 +1019,25 @@ @
      onoff_attribute("Require password for local access", "localauth", "localauth", 0, 0); @

      When enabled, the password sign-in is always required for @ web access. When disabled, unrestricted web access from 127.0.0.1 - @ is allowed for the fossil ui command or - @ from the fossil server, - @ fossil http commands when the + @ is allowed for the fossil ui command or + @ from the fossil server, + @ fossil http commands when the @ "--localauth" command line options is used, or from the - @ fossil cgi if a line containing + @ fossil cgi if a line containing @ the word "localauth" appears in the CGI script. @ @

      A password is always required if any one or more @ of the following are true: @

        @
      1. This button is checked @
      2. The inbound TCP/IP connection is not from 127.0.0.1 @
      3. The server is started using either of the - @ fossil server or - @ fossil http commands + @ fossil server or + @ fossil http commands @ without the "--localauth" option. @
      4. The server is started from CGI without the "localauth" keyword @ in the CGI script. @
      @ @@ -1203,11 +1204,12 @@ const char *zPw = PD("pw", ""); const char *zNewName = PD("newname", "New Login Group"); login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } file_canonical_name(g.zRepositoryName, &fullName, 0); zSelfRepo = fossil_strdup(blob_str(&fullName)); blob_reset(&fullName); if( P("join")!=0 ){ @@ -1315,11 +1317,12 @@ "3", "YYMMDD HH:MM", "4", "(off)" }; login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } style_header("Timeline Display Preferences"); db_begin_transaction(); @
      @@ -1393,11 +1396,12 @@ void setup_settings(void){ Setting const *pSet; login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } (void) aCmdHelp; /* NOTE: Silence compiler warning. */ style_header("Settings"); if(!g.repositoryOpen){ @@ -1473,11 +1477,12 @@ ** WEBPAGE: setup_config */ void setup_config(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } style_header("WWW Configuration"); db_begin_transaction(); @
      @@ -1551,11 +1556,12 @@ ** WEBPAGE: setup_editcss */ void setup_editcss(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); if( P("clear")!=0 ){ db_multi_exec("DELETE FROM config WHERE name='css'"); cgi_replace_parameter("css", builtin_text("skins/default/css.txt")); @@ -1596,11 +1602,12 @@ ** WEBPAGE: setup_header */ void setup_header(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); if( P("clear")!=0 ){ db_multi_exec("DELETE FROM config WHERE name='header'"); cgi_replace_parameter("header", builtin_text("skins/default/header.txt")); @@ -1660,11 +1667,12 @@ ** WEBPAGE: setup_footer */ void setup_footer(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); if( P("clear")!=0 ){ db_multi_exec("DELETE FROM config WHERE name='footer'"); cgi_replace_parameter("footer", builtin_text("skins/default/footer.txt")); @@ -1697,11 +1705,12 @@ ** WEBPAGE: setup_modreq */ void setup_modreq(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } style_header("Moderator For Wiki And Tickets"); db_begin_transaction(); @
      @@ -1708,11 +1717,11 @@ login_insert_csrf_secret(); @
      onoff_attribute("Moderate ticket changes", "modreq-tkt", "modreq-tkt", 0, 0); @

      When enabled, any change to tickets is subject to the approval - @ a ticket moderator - a user with the "q" or Mod-Tkt privilege. + @ by a ticket moderator - a user with the "q" or Mod-Tkt privilege. @ Ticket changes enter the system and are shown locally, but are not @ synced until they are approved. The moderator has the option to @ delete the change rather than approve it. Ticket changes made by @ a user who has the Mod-Tkt privilege are never subject to @ moderation. @@ -1719,11 +1728,11 @@ @ @


      onoff_attribute("Moderate wiki changes", "modreq-wiki", "modreq-wiki", 0, 0); @

      When enabled, any change to wiki is subject to the approval - @ a ticket moderator - a user with the "l" or Mod-Wiki privilege. + @ by a wiki moderator - a user with the "l" or Mod-Wiki privilege. @ Wiki changes enter the system and are shown locally, but are not @ synced until they are approved. The moderator has the option to @ delete the change rather than approve it. Wiki changes made by @ a user who has the Mod-Wiki privilege are never subject to @ moderation. @@ -1741,11 +1750,12 @@ ** WEBPAGE: setup_adunit */ void setup_adunit(void){ login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); if( P("clear")!=0 ){ db_multi_exec("DELETE FROM config WHERE name GLOB 'adunit*'"); cgi_replace_parameter("adunit",""); @@ -1822,11 +1832,12 @@ if( szBgImg>0 ){ zBgMime = PD("bgim:mimetype","image/gif"); } login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); if( P("setlogo")!=0 && zLogoMime && zLogoMime[0] && szLogoImg>0 ){ Blob img; Stmt ins; @@ -1961,11 +1972,12 @@ void sql_page(void){ const char *zQ = P("q"); int go = P("go")!=0; login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); style_header("Raw SQL Commands"); @

      Caution: There are no restrictions on the SQL that can be @ run by this page. You can do serious and irrepairable damage to the @@ -2082,11 +2094,12 @@ void th1_page(void){ const char *zQ = P("q"); int go = P("go")!=0; login_check_credentials(); if( !g.perm.Setup ){ - login_needed(); + login_needed(0); + return; } db_begin_transaction(); style_header("Raw TH1 Commands"); @

      Caution: There are no restrictions on the TH1 that can be @ run by this page. If Tcl integration was enabled at compile-time and @@ -2142,11 +2155,12 @@ int limit; int fLogEnabled; int counter = 0; login_check_credentials(); if( !g.perm.Setup && !g.perm.Admin ){ - login_needed(); + login_needed(0); + return; } style_header("Admin Log"); create_admin_log_table(); limit = atoi(PD("n","20")); fLogEnabled = db_get_boolean("admin-log", 0); @@ -2199,11 +2213,12 @@ ** Configure the search engine. */ void page_srchsetup(){ login_check_credentials(); if( !g.perm.Setup && !g.perm.Admin ){ - login_needed(); + login_needed(0); + return; } style_header("Search Configuration"); @

      login_insert_csrf_secret(); @
      @@ -2251,16 +2266,18 @@ search_update_index(search_restrict(SRCH_ALL)); } if( search_index_exists() ){ @

      Currently using an SQLite FTS4 search index. This makes search @ run faster, especially on large repositories, but takes up space.

      + onoff_attribute("Use Porter Stemmer","search-stemmer","ss",0,0); @

      @ }else{ @

      The SQLite FTS4 search index is disabled. All searching will be @ a full-text scan. This usually works fine, but can be slow for @ larger repositories.

      + onoff_attribute("Use Porter Stemmer","search-stemmer","ss",0,0); @

      } @

      style_footer(); } Index: src/shell.c ================================================================== --- src/shell.c +++ src/shell.c @@ -1742,10 +1742,11 @@ static char zHelp[] = ".backup ?DB? FILE Backup DB (default \"main\") to FILE\n" ".bail on|off Stop after hitting an error. Default OFF\n" ".clone NEWDB Clone data into NEWDB from the existing database\n" ".databases List names and files of attached databases\n" + ".dbinfo ?DB? Show status information about the database\n" ".dump ?TABLE? ... Dump the database in an SQL text format\n" " If TABLE specified, only dump tables matching\n" " LIKE pattern TABLE.\n" ".echo on|off Turn command echo on or off\n" ".eqp on|off Enable or disable automatic EXPLAIN QUERY PLAN\n" @@ -1754,12 +1755,12 @@ " With no args, it turns EXPLAIN on.\n" ".fullschema Show schema and the content of sqlite_stat tables\n" ".headers on|off Turn display of headers on or off\n" ".help Show this message\n" ".import FILE TABLE Import data from FILE into TABLE\n" - ".indices ?TABLE? Show names of all indices\n" - " If TABLE specified, only show indices for tables\n" + ".indexes ?TABLE? Show names of all indexes\n" + " If TABLE specified, only show indexes for tables\n" " matching LIKE pattern TABLE.\n" #ifdef SQLITE_ENABLE_IOTRACE ".iotrace FILE Enable I/O diagnostic logging to FILE\n" #endif #ifndef SQLITE_OMIT_LOAD_EXTENSION @@ -2434,10 +2435,119 @@ output_file_close(p->out); } p->outfile[0] = 0; p->out = stdout; } + +/* +** Run an SQL command and return the single integer result. +*/ +static int db_int(ShellState *p, const char *zSql){ + sqlite3_stmt *pStmt; + int res = 0; + sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0); + if( pStmt && sqlite3_step(pStmt)==SQLITE_ROW ){ + res = sqlite3_column_int(pStmt,0); + } + sqlite3_finalize(pStmt); + return res; +} + +/* +** Convert a 2-byte or 4-byte big-endian integer into a native integer +*/ +unsigned int get2byteInt(unsigned char *a){ + return (a[0]<<8) + a[1]; +} +unsigned int get4byteInt(unsigned char *a){ + return (a[0]<<24) + (a[1]<<16) + (a[2]<<8) + a[3]; +} + +/* +** Implementation of the ".info" command. +** +** Return 1 on error, 2 to exit, and 0 otherwise. +*/ +static int shell_dbinfo_command(ShellState *p, int nArg, char **azArg){ + static const struct { const char *zName; int ofst; } aField[] = { + { "file change counter:", 24 }, + { "database page count:", 28 }, + { "freelist page count:", 36 }, + { "schema cookie:", 40 }, + { "schema format:", 44 }, + { "default cache size:", 48 }, + { "autovacuum top root:", 52 }, + { "incremental vacuum:", 64 }, + { "text encoding:", 56 }, + { "user version:", 60 }, + { "application id:", 68 }, + { "software version:", 96 }, + }; + static const struct { const char *zName; const char *zSql; } aQuery[] = { + { "number of tables:", + "SELECT count(*) FROM %s WHERE type='table'" }, + { "number of indexes:", + "SELECT count(*) FROM %s WHERE type='index'" }, + { "number of triggers:", + "SELECT count(*) FROM %s WHERE type='trigger'" }, + { "number of views:", + "SELECT count(*) FROM %s WHERE type='view'" }, + { "schema size:", + "SELECT total(length(sql)) FROM %s" }, + }; + sqlite3_file *pFile; + int i; + char *zSchemaTab; + char *zDb = nArg>=2 ? azArg[1] : "main"; + unsigned char aHdr[100]; + open_db(p, 0); + if( p->db==0 ) return 1; + sqlite3_file_control(p->db, zDb, SQLITE_FCNTL_FILE_POINTER, &pFile); + if( pFile==0 || pFile->pMethods==0 || pFile->pMethods->xRead==0 ){ + return 1; + } + i = pFile->pMethods->xRead(pFile, aHdr, 100, 0); + if( i!=SQLITE_OK ){ + fprintf(stderr, "unable to read database header\n"); + return 1; + } + i = get2byteInt(aHdr+16); + if( i==1 ) i = 65536; + fprintf(p->out, "%-20s %d\n", "database page size:", i); + fprintf(p->out, "%-20s %d\n", "write format:", aHdr[18]); + fprintf(p->out, "%-20s %d\n", "read format:", aHdr[19]); + fprintf(p->out, "%-20s %d\n", "reserved bytes:", aHdr[20]); + for(i=0; iout, "%-20s %u", aField[i].zName, val); + switch( ofst ){ + case 56: { + if( val==1 ) fprintf(p->out, " (utf8)"); + if( val==2 ) fprintf(p->out, " (utf16le)"); + if( val==3 ) fprintf(p->out, " (utf16be)"); + } + } + fprintf(p->out, "\n"); + } + if( zDb==0 ){ + zSchemaTab = sqlite3_mprintf("main.sqlite_master"); + }else if( strcmp(zDb,"temp")==0 ){ + zSchemaTab = sqlite3_mprintf("%s", "sqlite_temp_master"); + }else{ + zSchemaTab = sqlite3_mprintf("\"%w\".sqlite_master", zDb); + } + for(i=0; iout, "%-20s %d\n", aQuery[i].zName, val); + } + sqlite3_free(zSchemaTab); + return 0; +} + /* ** If an input line begins with "." then invoke this routine to ** process that line. ** @@ -2576,10 +2686,14 @@ fprintf(stderr,"Error: %s\n", zErrMsg); sqlite3_free(zErrMsg); rc = 1; } }else + + if( c=='d' && strncmp(azArg[0], "dbinfo", n)==0 ){ + rc = shell_dbinfo_command(p, nArg, azArg); + }else if( c=='d' && strncmp(azArg[0], "dump", n)==0 ){ open_db(p, 0); /* When playing back a "dump", the content might appear in an order ** which causes immediate foreign key constraints to be violated. @@ -2916,11 +3030,11 @@ sqlite3_bind_text(pStmt, i+1, z, -1, SQLITE_TRANSIENT); if( iArtifact(s)
      for( p = zUuid ; *p ; p += UUID_SIZE+1 ){ - @ %s(p)
      + @ %s(p)
      } @ are no longer being shunned.

      }else{ @

      Artifact(s)
      for( p = zUuid ; *p ; p += UUID_SIZE+1 ){ @@ -146,11 +147,11 @@ admin_log("Shunned %Q", p); p += UUID_SIZE+1; } @

      Artifact(s)
      for( p = zUuid ; *p ; p += UUID_SIZE+1 ){ - @ %s(p)
      + @ %s(p)
      } @ have been shunned. They will no longer be pushed. @ They will be removed from the repository the next time the repository @ is rebuilt using the fossil rebuild command-line

      } @@ -248,11 +249,11 @@ while( db_step(&q)==SQLITE_ROW ){ const char *zUuid = db_column_text(&q, 0); int stillExists = db_column_int(&q, 1); cnt++; if( stillExists ){ - @ %s(zUuid)
      + @ %s(zUuid)
      }else{ @ %s(zUuid)
      } } if( cnt==0 ){ @@ -301,11 +302,12 @@ int cnt; Stmt q; login_check_credentials(); if( !g.perm.Admin ){ - login_needed(); + login_needed(0); + return; } style_header("Artifact Receipts"); if( showAll ){ ofst = 0; }else{ @@ -381,11 +383,12 @@ int rcvid = atoi(PD("rcvid","0")); Stmt q; login_check_credentials(); if( !g.perm.Admin ){ - login_needed(); + login_needed(0); + return; } style_header("Artifact Receipt %d", rcvid); if( db_exists( "SELECT 1 FROM blob WHERE rcvid=%d AND" " NOT EXISTS (SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)", rcvid) @@ -436,13 +439,13 @@ while( db_step(&q)==SQLITE_ROW ){ const char *zUuid = db_column_text(&q, 1); int size = db_column_int(&q, 2); const char *zDesc = db_column_text(&q, 3); if( zDesc==0 ) zDesc = ""; - @ %s(zUuid) + @ %s(zUuid) @ %h(zDesc) (size: %d(size))
      } @
      db_finalize(&q); style_footer(); } Index: src/sitemap.c ================================================================== --- src/sitemap.c +++ src/sitemap.c @@ -34,62 +34,74 @@ @ The following links are just a few of the many web-pages available for @ this Fossil repository: @

      @ @
      + +

      Fossil is a simple, high-reliability, distributed software configuration +management with these advanced features: + + 1. Integrated Bug Tracking, Wiki, and Technotes - In addition to doing [./concepts.wiki | distributed version control] like Git and Mercurial, - Fossil also supports [./bugtheory.wiki | distributed bug tracking], - [./wikitheory.wiki | distributed wiki], and a - [./event.wiki | distributed blog] mechanism all in a single - integrated package. - - 2. Web Interface - - Fossil has a built-in and easy-to-use [./webui.wiki | web interface] - that simplifies project tracking and promotes situational awareness. - Simply type "fossil ui" from within any check-out and Fossil - automatically opens your web browser in a page that gives detailed - [/timeline?n=100&y=ci | graphical history] and status information - on that project. - - This entire website (except the - [http://www.fossil-scm.org/download.html | download] page) + Fossil also supports [./bugtheory.wiki | bug tracking], + [./wikitheory.wiki | wiki], and [./event.wiki | technotes]. + + 2. Built-in Web Interface - + Fossil has a built-in and intuitive [./webui.wiki | web interface] + with a rich assortment of information pages + ([./webpage-ex.md|examples]) designed to promote situational awareness. + + This entire website¹ is just a running instance of Fossil. The pages you see here are all [./wikitheory.wiki | wiki] or [./embeddeddoc.wiki | embedded documentation]. When you clone Fossil from one of its [./selfhost.wiki | self-hosting repositories], you get more than just source code - you get this entire website. + (¹except the + [http://www.fossil-scm.org/download.html | download] page) + + 3. Self-Contained - + Fossil is a single self-contained stand-alone executable. + To install, simply download a + precompiled binary + for Linux, Mac, OpenBSD, or Windows and put it on your $PATH. + [./build.wiki | Easy-to-compile source code] is also available. + + 4. Simple Networking - + No custom protocols or TCP ports. + Fossil uses ordinary HTTP (or HTTPS or SSH) + for network communications, so it works fine from behind + restrictive firewalls, including [./quickstart.wiki#proxy|proxies]. + The protocol is + [./stats.wiki | bandwidth efficient] to the point that Fossil can be + used comfortably over dial-up. + + 5. CGI/SCGI Enabled - No server is required, but if you want to + set one up, Fossil supports four easy + [./server.wiki | server configurations]. - 3. Autosync - + 6. Autosync - Fossil supports [./concepts.wiki#workflow | "autosync" mode] which helps to keep projects moving forward by reducing the amount of needless [./branching.wiki | forking and merging] often associated with distributed projects. - 4. Self-Contained - - Fossil is a single stand-alone executable that contains everything - needed to do configuration management. - Installation is trivial: simply download a - precompiled binary - for Linux, Mac, or Windows and put it on your $PATH. - [./build.wiki | Easy-to-compile source code] is available for - users on other platforms. Fossil sources are also mostly self-contained, - requiring only the standard C library to build. - - 5. Simple Networking - - Fossil uses plain old HTTP (with - [./quickstart.wiki#proxy | proxy support]) - for all network communications, meaning that it works fine from behind - restrictive firewalls. The protocol is - [./stats.wiki | bandwidth efficient] to the point that Fossil can be - used comfortably over a dial-up internet connection. - - 6. CGI/SCGI Enabled - - No server is required to use fossil. But a - server does make collaboration easier. Fossil supports four different - yet simple [./server.wiki | server configurations]. - The most popular is a 2-line CGI script. This is the approach - used by the [./selfhost.wiki | self-hosting fossil repositories]. - 7. Robust & Reliable - Fossil stores content using an [./fileformat.wiki | enduring file format] in an SQLite database so that transactions are - atomic even if interrupted by a power loss or system crash. Furthermore, - automatic [./selfcheck.wiki | self-checks] verify that all aspects of - the repository are consistent prior to each commit. In over six years + atomic even if interrupted by a power loss or system crash. + Automatic [./selfcheck.wiki | self-checks] verify that all aspects of + the repository are consistent prior to each commit. In over seven years of operation, no work has ever been lost after having been committed to a Fossil repository. + + 8. Free and Open-Source - Uses the [../COPYRIGHT-BSD2.txt|2-clause BSD license].


      Links For Fossil Users:

      * "Fuel" is cross-platform GUI front-end for Fossil @@ -159,11 +135,11 @@ * [./fossil-v-git.wiki | Fossil versus Git]. * [./fiveminutes.wiki | Up and running in 5 minutes as a single user] (contributed by Gilles Ganault on 2013-01-08). * [./antibot.wiki | How Fossil defends against abuse by spiders and bots]. -

      Links For Fossil Developer:

      +

      Links For Fossil Developers:

      * [./contribute.wiki | Contributing] code or documentation to the Fossil project. * [./theory1.wiki | Thoughts On The Design Of Fossil]. * [./pop.wiki | Principles Of Operation] Index: www/inout.wiki ================================================================== --- www/inout.wiki +++ www/inout.wiki @@ -48,5 +48,16 @@ since the git-fast-export file format is currently the only VCS interchange format that Fossil will generate. However, future versions of Fossil might add the ability to generate other VCS interchange formats, and so for compatibility, the use of the --git option recommented. + +An anonymous user sends this comment: + +
      +The main Fossil branch is called "trunk", while the main git branch is +called "master". After you've exported your FOSSIL repo to git, you won't +see any files and gitk will complain about a missing "HEAD". You can +resolve this problem by merging "trunk" with "master" +(first verify using git status that you are on the "master" branch): +git merge trunk +
      Index: www/mkdownload.tcl ================================================================== --- www/mkdownload.tcl +++ www/mkdownload.tcl @@ -5,42 +5,41 @@ # # set out [open download.html w] fconfigure $out -encoding utf-8 -translation lf puts $out \ -{ - - -Fossil: Downloads - - - -
      - - -
      Fossil Downloads
      -
      - Index: www/quotes.wiki ================================================================== --- www/quotes.wiki +++ www/quotes.wiki @@ -71,16 +71,23 @@
      Stephen Beal on the [http://www.mail-archive.com/fossil-users@lists.fossil-scm.org/msg17181.html|Fossil mailing list] 2014-09-01.
      +
    • If programmers _really_ wanted to help scientists, they'd build a version control +system that was more usable than Git. + +
      +Tweet by Greg Wilson @gvwilson on 2015-02-22 17:47 +
      +

      On The Usability Of Fossil:

        -
      1. +
      2. Fossil mesmerizes me with simplicity especially after I struggled to get a bug-tracking system to work with mercurial.
        rawjeev at [http://stackoverflow.com/questions/156322/what-do-people-think-of-the-fossil-dvcs] @@ -108,11 +115,11 @@

        On Git Versus Fossil

          -
        1. +
        2. Just want to say thanks for fossil making my life easier.... Also [for] not having a misanthropic command line interface.
          Joshua Paine at [http://www.mail-archive.com/fossil-users@lists.fossil-scm.org/msg02736.html] Index: www/selfcheck.wiki ================================================================== --- www/selfcheck.wiki +++ www/selfcheck.wiki @@ -44,11 +44,11 @@ new delta-encoding mechanism designed expressly for fossil. We want to make sure that bugs in these encoding mechanisms do not lead to loss of data. To increase our confidence that everything in the repository is -recoverable, fossil makes sure it can extract an exact replicate +recoverable, fossil makes sure it can extract an exact replica of every content file that it changes just prior to transaction commit. So during the course of check-in (or other repository operation) many different files in the repository might be modified. Some files are simply compressed. Other files are delta encoded and then compressed. ADDED www/th1.md Index: www/th1.md ================================================================== --- /dev/null +++ www/th1.md @@ -0,0 +1,166 @@ +TH1 Scripts +=========== + +TH1 is a very small scripting language used to help generate web-page +content in Fossil. + +Origins +------- + +TH1 began as a minimalist reimplementation of the TCL scripting language. +There was a need to test the SQLite library on Symbian phones, but at that +time all of the test cases for SQLite were written in Tcl and Tcl could not +be easily compiled on the SymbianOS. So TH1 was developed as a cut-down +version of TCL that would facilitate running the SQLite test scripts on +SymbianOS. + +The testing of SQLite on SymbianOS was eventually accomplished by other +means. But Fossil was first being designed at about the same time. +Early prototypes of Fossil were written in pure TCL. But as the development +shifted toward the use of C-code, the need arose to have a TCL-like +scripting language to help with code generation. TH1 was small and +light-weight and used minimal resources and seemed ideally suited for the +task. + +The name "TH1" stands "Test Harness 1", since that was its original purpose. + +Overview +-------- + +TH1 is a string-processing language. All values are strings. Any numerical +operations are accomplished by converting from string to numeric, performing +the computation, then converting the result back into a string. (This might +seem inefficient, but it is faster than people imagine, and numeric +computations do not come up very often for the kinds of work that TH1 does, +so it has never been a factor.) + +A TH1 script consist of a sequence of commands. +Each command is terminated by the first (unescaped) newline or ";" character. +The text of the command (excluding the newline or semicolon terminator) +is broken into space-separated tokens. The first token is the command +name and subsequent tokens are the arguments. In this since, TH1 syntax +is similar to the familiar command-line shell syntax. + +A token is any sequence of characters other than whitespace and semicolons. +Or, all text without double-quotes is a single token even if it includes +whitespace and semicolons. Or, all text without nested {...} pairs is a +single token. + +The nested {...} form of tokens is important because it allows TH1 commands +to have an appearance similar to C/C++. It is important to remember, though, +that a TH1 script is really just a list of text commands, not a context-free +language with a grammar like C/C++. This can be confusing to long-time +C/C++ programmers because TH1 does look a lot like C/C++. But the semantics +of TH1 are closer to FORTH or Lisp than they are to C. + +Consider the "if" command in TH1. + + if {$current eq "dev"} { + puts "hello" + } else { + puts "world" + } + +The example above is a single command. The first token, and the name +of the command, is "if". +The second token is '$current eq "dev"' - an expression. (The outer {...} +are removed from each token by the command parser.) The third token +is the 'puts "hello"', with its whitespace and newlines. The fourth token +is "else". And the fifth and last token is 'puts "world"'. + +The "if" command word by evaluating its first argument (the second token) +as an expression, and if that expression is true, evaluating its +second argument (the third token) as a TH1 script. +If the expression is false and the third argument is "else" then +the fourth argument is evaluated as a TH1 expression. + +So, you see, even though the example above spans five lines, it is really +just a single command. + +Summary of Core TH1 Commands +---------------------------- + +The original TCL language after when TH1 is modeled has a very rich +repetoire of commands. TH1, as it is designed to be minimalist and +embedded has a greatly reduced command set. The following bullets +summarize the commands available in TH1: + + * break + * catch SCRIPT ?VARIABLE? + * continue + * error ?STRING? + * expr EXPR + * for INIT-SCRIPT TEST-EXPR NEXT-SCRIPT BODY-SCRIPT + * if EXPR SCRIPT (elseif EXPR SCRIPT)* ?else SCRIPT? + * info exists VARNAME + * lindex LIST INDEX + * list ARG ... + * llength LIST + * proc NAME ARG-LIST BODY-SCRIPT + * rename OLD NEW + * return ?-code CODE? ?VALUE? + * set VARNAME VALUE + * string compare STR1 STR2 + * string first NEEDLE HAYSTACK ?START-INDEX? + * string is CLASS STRING + * string last NEEDLE HAYSTACK ?START-INDEX? + * string length STRING + * string range STRING FIRST LAST + * string repeat STRING COUNT + * unset VARNAME + * uplevel ?LEVEL? SCRIPT + * upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR? + +All of the above commands works as in the original TCL. Refer to the +TCL documentation for details. + +TH1 Extended Commands +--------------------- + +There are many new commands added to TH1 and used to access the special +features of Fossil. The following is a summary of the extended commands: + + * anoncap + * anycap + * artifact + * checkout + * combobox + * date + * decorate + * enable_output + * getParameter + * globalState + * httpize + * hascap + * hasfeature + * html + * htmlize + * http + * linecount + * puts + * query + * randhex + * regexp + * reinitialize + * render + * repository + * searchable + * setParameter + * setting + * styleHeader + * styleFooter + * tclReady + * trace + * stime + * utime + * wiki + +Each of the commands above is documented by a block comment above their +implementation in the th_main.c source file. + +**To Do:** We would like to have a community volunteer go through and +copy the documentation for each of these command (with appropriate +format changes and spelling and grammar corrections) into subsequent +sections of this document. It is suggested that the list of extension +commands be left intact - as a quick reference. But it would be really +nice to also have the details of each each command does. ADDED www/webpage-ex.md Index: www/webpage-ex.md ================================================================== --- /dev/null +++ www/webpage-ex.md @@ -0,0 +1,99 @@ +Web-Page Examples +================= + +Here are a few examples of the many web pages supported +by Fossil. This is not an exhaustive list. +Explore hyperlinks to see more. + + + * Example + 100 most recent check-ins. + + * Example + All changes to the src/file.c source file. + + * Example + All check-ins using a particular version of the src/file.c + source file. + + * Example + Check-ins proximate to an historical point in time (2014-01-01). + + * Example + The previous augmented with file changes. + + * Example + First 25 check-ins after 1970-01-01. (The first 25 check-ins of + the project.) + + * Example + All check-ins of the "svn-import" branch together with check-ins + that merge with that branch. + + * Example + All check-ins of the "svn-import" branch only. + + * Example + 100 most recent check-ins color coded by committer. + + * Example + All check-ins on the most direct path from + version-1.27 to version-1.28 + + (Hint: In any graph above, click the square node boxes + for two check-ins or files to see a diff.) + + * Example + All files for a particular check-in (daff9d20621480) + + * Example + All files for the latest check-in on a branch (trunk) sorted by + last modification time. + + * Example + Age of all files in the latest checking for branch "svn-import". + last modification time. + + * Example + Table of branches. (Click on column headers to sort.) + + * Example + Overall repository status. + + * Example + Number of check-ins for each source file. + (Click on column headers to sort.) + + * + Example + Most recent change to each line of a particular source file in a + particular check-in. + + * Example + List of tags on check-ins. Index: www/webui.wiki ================================================================== --- www/webui.wiki +++ www/webui.wiki @@ -8,19 +8,19 @@ * [./wikitheory.wiki | Wiki] * [./embeddeddoc.wiki | On-line documentation] * Status information * Timelines * Graphs of revision and branching history - * [./event.wiki | Blogs, News, and Announcements] + * [./event.wiki | Technical notes] * File and version lists and differences * Download historical versions as ZIP archives * Historical change data * Add and remove tags on checkins * Move checkins between branches * Revise checkin comments * Manage user credentials and access permissions - * And so forth... + * And so forth... (some [./webpage-ex.md|examples]) You get all of this, and more, for free when you use Fossil. There are no extra programs to install or setup. Everything you need is already pre-configured and built into the self-contained, stand-alone Fossil executable. Index: www/wikitheory.wiki ================================================================== --- www/wikitheory.wiki +++ www/wikitheory.wiki @@ -5,12 +5,12 @@ * Stand-alone wiki pages. * Description and comments in [./bugtheory.wiki | bug reports]. * Check-in comments. * [./embeddeddoc.wiki | Embedded documentation] files whose - name ends in "wiki". - * [./event.wiki | Event descriptions]. + name ends in ".wiki". + * [./event.wiki | Technical notes]. The [/wiki_rules | formatting rules] for fossil wiki are designed to be simple and intuitive. The idea is that wiki provides paragraph breaks, numbered and bulleted lists, and hyperlinking for simple documents together with a safe subset of HTML for more complex @@ -31,10 +31,14 @@ 3. Where the fossil wiki markup language is insufficient, HTML is used. HTML is a standard language familiar to most programmers so there is nothing new to learn. And, though cumbersome, the HTML does not need to be used very often so is not a burden. +UPDATE: Since 2012, Fossil also contains a [/md_rules | Markdown] +rendering engine. Markdown can optionally be used to format +[./embeddeddoc.wiki | embedded documents], wiki pages, +[./event.wiki | technical notes], and bug report text.

          Stand-alone Wiki Pages

          Each wiki page has its own revision history which is independent of the sequence of check-ins (check-ins). Wiki pages can branch and merge