Changes On Branch timeline-enhance-2025
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch timeline-enhance-2025 Excluding Merge-Ins

This is equivalent to a diff from 1ff0d0b0cf to ff94437a7c

2025-10-20
17:52
Land the timeline-enhance-2025 branch. The "Simple" timeline view. Improved access to tarballs and ZIPs. See items 3 and 4 in the merged www/changes.wiki for more detail. See also the discussions on [forum:/forumpost/6a01bc270d3d2ae6|forum thread 6a01bc270d]. check-in: 6ce705b8dc user: drh tags: trunk
10:31
Use ascii "self" instead of chinese "自", since some users do not have han fonts installed. Closed-Leaf check-in: ff94437a7c user: drh tags: timeline-enhance-2025
2025-10-19
21:32
Fix typos in the documentation of the 'suggested-downloads' setting. check-in: 302c30b5dc user: danield tags: timeline-enhance-2025
2025-10-17
14:31
When handling HTTP over SSH only strip the filename from the PATH for GET method as POST has a different mechanism for handling it. check-in: 7531b9452e user: andybradford tags: trunk
13:35
Merge trunk fixes into the timeline-enhance-2025 branch. check-in: 36f17b0c91 user: drh tags: timeline-enhance-2025
13:34
Fix the /finfo page display that was broken by recent infrastructure enhancements. check-in: 1ff0d0b0cf user: drh tags: trunk
11:18
Add the /tarlist page with infrastructure changes to facilitate. Fix the "ng" (no-graph) query parameter so that it still colors timeline entries appropriately and still shows node circles, it just omits all the connecting lines. check-in: 9cc36d2354 user: drh tags: trunk

Changes to src/branch.c.
841
842
843
844
845
846
847

848
849
850
851
852
853
854
  int show_colors = PB("colors");
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  style_set_current_feature("branch");
  style_header("Branches");
  style_adunit_config(ADUNIT_RIGHT_OK);
  style_submenu_checkbox("colors", "Use Branch Colors", 0, 0);

  login_anonymous_available();

  brlist_create_temp_table();
  db_prepare(&q, "SELECT * FROM tmp_brlist ORDER BY mtime DESC");
  rNow = db_double(0.0, "SELECT julianday('now')");
  @ <script id="brlist-data" type="application/json">\
  @ {"timelineUrl":"%R/timeline"}</script>







>







841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
  int show_colors = PB("colors");
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  style_set_current_feature("branch");
  style_header("Branches");
  style_adunit_config(ADUNIT_RIGHT_OK);
  style_submenu_checkbox("colors", "Use Branch Colors", 0, 0);

  login_anonymous_available();

  brlist_create_temp_table();
  db_prepare(&q, "SELECT * FROM tmp_brlist ORDER BY mtime DESC");
  rNow = db_double(0.0, "SELECT julianday('now')");
  @ <script id="brlist-data" type="application/json">\
  @ {"timelineUrl":"%R/timeline"}</script>
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034

1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046






1047
1048

1049
1050
1051

1052
1053
1054
1055
1056
1057
1058
1059
1060
*/
static void brtimeline_extra(
  Stmt *pQuery,               /* Current row of the timeline query */
  int tmFlags,                /* Flags to www_print_timeline() */
  const char *zThisUser,      /* Suppress links to this user */
  const char *zThisTag        /* Suppress links to this tag */
){
  int rid = db_column_int(pQuery, 0);
  Stmt q;
  if( !g.perm.Hyperlink ) return;
  db_prepare(&q,
    "SELECT substr(tagname,5) FROM tagxref, tag"

    " WHERE tagxref.rid=%d"
    "   AND tagxref.tagid=tag.tagid"
    "   AND tagxref.tagtype>0"
    "   AND tag.tagname GLOB 'sym-*'",
    rid
  );
  while( db_step(&q)==SQLITE_ROW ){
    const char *zTagName = db_column_text(&q, 0);
#define OLD_STYLE 1
#if OLD_STYLE
    @  %z(href("%R/timeline?r=%T",zTagName))[timeline]</a>
#else






    char *zBrName = branch_of_rid(rid);
    @  <strong>%h(zBrName)</strong><br>\

    @  %z(href("%R/timeline?r=%T",zTagName))<button>timeline</button></a>
    fossil_free(zBrName);
#endif

  }
  db_finalize(&q);
}

/*
** WEBPAGE: brtimeline
**
** List the first check of every branch, starting with the most recent
** and going backwards in time.







|
|
<
|
|
>
|
<
<
<
|
<
<
<
<
<
<
|
>
>
>
>
>
>
|
<
>
|
<
|
>

<







1024
1025
1026
1027
1028
1029
1030
1031
1032

1033
1034
1035
1036



1037






1038
1039
1040
1041
1042
1043
1044
1045

1046
1047

1048
1049
1050

1051
1052
1053
1054
1055
1056
1057
*/
static void brtimeline_extra(
  Stmt *pQuery,               /* Current row of the timeline query */
  int tmFlags,                /* Flags to www_print_timeline() */
  const char *zThisUser,      /* Suppress links to this user */
  const char *zThisTag        /* Suppress links to this tag */
){
  int rid;
  int tmFlagsNew;

  char *zBrName;

  if( (tmFlags & TIMELINE_INLINE)!=0 ){
    tmFlagsNew = (tmFlags & ~TIMELINE_VIEWS) | TIMELINE_MODERN;



    cgi_printf("(");






  }else{
    tmFlagsNew = tmFlags;
  }
  timeline_extra(pQuery,tmFlagsNew,zThisUser,zThisTag);

  if( !g.perm.Hyperlink ) return;
  rid = db_column_int(pQuery,0);
  zBrName = branch_of_rid(rid);

  @  branch:&nbsp;<span class='timelineHash'>\
  @ %z(href("%R/timeline?r=%T",zBrName))%h(zBrName)</a></span>

  if( (tmFlags & TIMELINE_INLINE)!=0 ){
    cgi_printf(")");
  }

}

/*
** WEBPAGE: brtimeline
**
** List the first check of every branch, starting with the most recent
** and going backwards in time.
1068
1069
1070
1071
1072
1073
1074

1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
  Stmt q;
  int tmFlags;                            /* Timeline display flags */
  int fNoHidden = PB("nohidden")!=0;      /* The "nohidden" query parameter */
  int fOnlyHidden = PB("onlyhidden")!=0;  /* The "onlyhidden" query parameter */

  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }


  style_set_current_feature("branch");
  style_header("Branches");
  style_submenu_element("Branch List", "brlist");
  login_anonymous_available();
#if OLD_STYLE
  timeline_ss_submenu();
#endif
  cgi_check_for_malice();
  @ <h2>First check-in for every branch, starting with the most recent
  @ and going backwards in time.</h2>
  blob_append(&sql, timeline_query_for_www(), -1);
  blob_append_sql(&sql,
    "AND blob.rid IN (SELECT rid FROM tagxref"
    "                  WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
  if( fNoHidden || fOnlyHidden ){
    const char* zUnaryOp = fNoHidden ? "NOT" : "";
    blob_append_sql(&sql,
      " AND %s EXISTS(SELECT 1 FROM tagxref"
      " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
      zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
  }
  db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
  blob_reset(&sql);
  /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
  ** many descenders to (off-screen) parents. */
  tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
#if !OLD_STYLE
  tmFlags |= TIMELINE_COLUMNAR;
#endif
  if( PB("ubg")!=0 ){
    tmFlags |= TIMELINE_UCOLOR;
  }else{
    tmFlags |= TIMELINE_BRCOLOR;
  }
  www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, brtimeline_extra);
  db_finalize(&q);







>





<

<

|
<
















<
<
<







1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077

1078

1079
1080

1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096



1097
1098
1099
1100
1101
1102
1103
  Stmt q;
  int tmFlags;                            /* Timeline display flags */
  int fNoHidden = PB("nohidden")!=0;      /* The "nohidden" query parameter */
  int fOnlyHidden = PB("onlyhidden")!=0;  /* The "onlyhidden" query parameter */

  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  if( robot_restrict("timelineX") ) return;

  style_set_current_feature("branch");
  style_header("Branches");
  style_submenu_element("Branch List", "brlist");
  login_anonymous_available();

  timeline_ss_submenu();

  cgi_check_for_malice();
  @ <h2>The initial check-in for each branch:</h2>

  blob_append(&sql, timeline_query_for_www(), -1);
  blob_append_sql(&sql,
    "AND blob.rid IN (SELECT rid FROM tagxref"
    "                  WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
  if( fNoHidden || fOnlyHidden ){
    const char* zUnaryOp = fNoHidden ? "NOT" : "";
    blob_append_sql(&sql,
      " AND %s EXISTS(SELECT 1 FROM tagxref"
      " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
      zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
  }
  db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
  blob_reset(&sql);
  /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
  ** many descenders to (off-screen) parents. */
  tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;



  if( PB("ubg")!=0 ){
    tmFlags |= TIMELINE_UCOLOR;
  }else{
    tmFlags |= TIMELINE_BRCOLOR;
  }
  www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, brtimeline_extra);
  db_finalize(&q);
Changes to src/browse.c.
227
228
229
230
231
232
233



234
235
236
237
238
239
240
    zHeader = mprintf("%z matching \"%s\"", zHeader, zRegexp);
    zMatch = mprintf(" matching \"%h\"", zRegexp);
  }else{
    zMatch = "";
  }
  style_header("%s", zHeader);
  fossil_free(zHeader);



  style_adunit_config(ADUNIT_RIGHT_OK);
  sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
                          pathelementFunc, 0, 0);
  url_initialize(&sURI, "dir");
  cgi_check_for_malice();
  cgi_query_parameters_to_url(&sURI);








>
>
>







227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
    zHeader = mprintf("%z matching \"%s\"", zHeader, zRegexp);
    zMatch = mprintf(" matching \"%h\"", zRegexp);
  }else{
    zMatch = "";
  }
  style_header("%s", zHeader);
  fossil_free(zHeader);
  if( rid && zD==0 && zMatch[0]==0 && g.perm.Zip ){
    style_submenu_element("Download","%R/rchvdwnld/%!S",zUuid);
  }
  style_adunit_config(ADUNIT_RIGHT_OK);
  sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
                          pathelementFunc, 0, 0);
  url_initialize(&sURI, "dir");
  cgi_check_for_malice();
  cgi_query_parameters_to_url(&sURI);

812
813
814
815
816
817
818



819
820
821
822
823
824
825
  if( zCI ){
    if( nD==0 && !showDirOnly ){
      style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
    }
  }
  style_submenu_element("Flat-View", "%s",
                        url_render(&sURI, "type", "flat", 0, 0));




  /* Compute the file hierarchy.
  */
  if( zCI ){
    Stmt q;
    compute_fileage(rid, 0);
    db_prepare(&q,







>
>
>







815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
  if( zCI ){
    if( nD==0 && !showDirOnly ){
      style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
    }
  }
  style_submenu_element("Flat-View", "%s",
                        url_render(&sURI, "type", "flat", 0, 0));
  if( rid && zD==0 && zRE==0 && !showDirOnly && g.perm.Zip ){
    style_submenu_element("Download","%R/rchvdwnld/%!S", zUuid);
  }

  /* Compute the file hierarchy.
  */
  if( zCI ){
    Stmt q;
    compute_fileage(rid, 0);
    db_prepare(&q,
Changes to src/clone.c.
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454


455
456
457
    db_unprotect(PROTECT_ALL);
    db_set("ssh-command", g.zSshCmd, 0);
    db_protect_pop();
  }
}

/*
** WEBPAGE: download
**
** Provide a simple page that enables newbies to download the latest tarball or
** ZIP archive, and provides instructions on how to clone.
*/
void download_page(void){
  login_check_credentials();
  cgi_check_for_malice();
  style_header("Download Page");
  if( !g.perm.Zip ){
    @ <p>Bummer.  You do not have permission to download.
    if( g.zLogin==0 || g.zLogin[0]==0 ){
      @ Maybe it would work better if you
      @ %z(href("%R/login"))logged in</a>.
    }else{
      @ Contact the site administrator and ask them to give
      @ you "Download Zip" privileges.
    }
  }else{
    const char *zDLTag = db_get("download-tag","trunk");
    const char *zNm = db_get("short-project-name","download");
    char *zUrl = href("%R/zip/%t/%t.zip", zDLTag, zNm);
    @ <p>ZIP Archive: %z(zUrl)%h(zNm).zip</a>
    zUrl = href("%R/tarball/%t/%t.tar.gz", zDLTag, zNm);
    @ <p>Tarball: %z(zUrl)%h(zNm).tar.gz</a>
    if( g.zLogin!=0 ){
      zUrl = href("%R/sqlar/%t/%t.sqlar", zDLTag, zNm);
      @ <p>SQLite Archive: %z(zUrl)%h(zNm).sqlar</a>
    }
  }
  if( !g.perm.Clone ){
    @ <p>You are not authorized to clone this repository.
    if( g.zLogin==0 || g.zLogin[0]==0 ){
      @ Maybe you would be able to clone if you
      @ %z(href("%R/login"))logged in</a>.
    }else{
      @ Contact the site administrator and ask them to give
      @ you "Clone" privileges in order to clone.
    }
  }else{
    const char *zNm = db_get("short-project-name","clone");
    @ <p>Clone the repository using this command:
    @ <blockquote><pre>
    @ fossil  clone  %s(g.zBaseURL)  %h(zNm).fossil
    @ </pre></blockquote>


  }
  style_finish_page();
}







|

<
|

|


|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<











|



>
>



403
404
405
406
407
408
409
410
411

412
413
414
415
416
417





















418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
    db_unprotect(PROTECT_ALL);
    db_set("ssh-command", g.zSshCmd, 0);
    db_protect_pop();
  }
}

/*
** WEBPAGE: howtoclone
**

** Provide instructions on how to clone this repository.
*/
void howtoclone_page(void){
  login_check_credentials();
  cgi_check_for_malice();
  style_header("How To Clone This Repository");





















  if( !g.perm.Clone ){
    @ <p>You are not authorized to clone this repository.
    if( g.zLogin==0 || g.zLogin[0]==0 ){
      @ Maybe you would be able to clone if you
      @ %z(href("%R/login"))logged in</a>.
    }else{
      @ Contact the site administrator and ask them to give
      @ you "Clone" privileges in order to clone.
    }
  }else{
    const char *zNm = db_get("short-project-name","clone");
    @ <p>Clone this repository by running a command like the following:
    @ <blockquote><pre>
    @ fossil  clone  %s(g.zBaseURL)  %h(zNm).fossil
    @ </pre></blockquote>
    @ <p>Do a web search for "fossil clone" or similar to find additional
    @ information about using a cloned Fossil repository.
  }
  style_finish_page();
}
Changes to src/db.c.
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
  }
  db_finalize(&s);
}

/*
** Execute a query.  Return the first column of the first row
** of the result set as a string.  Space to hold the string is
** obtained from malloc().  If the result set is empty, return
** zDefault instead.
*/
char *db_text(const char *zDefault, const char *zSql, ...){
  va_list ap;
  Stmt s;
  char *z;
  va_start(ap, zSql);
  db_vprepare(&s, 0, zSql, ap);







|
|







1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
  }
  db_finalize(&s);
}

/*
** Execute a query.  Return the first column of the first row
** of the result set as a string.  Space to hold the string is
** obtained from fossil_strdup() and should be freed using fossil_free().
** If the result set is empty, return a copy of zDefault instead.
*/
char *db_text(const char *zDefault, const char *zSql, ...){
  va_list ap;
  Stmt s;
  char *z;
  va_start(ap, zSql);
  db_vprepare(&s, 0, zSql, ap);
Changes to src/default.css.
55
56
57
58
59
60
61



62
63
64
65
66
67
68
tr.timelineCurrent td {
  border-radius: 0;
  border-width: 0;
}
span.timelineLeaf {
  font-weight: bold;
}



span.timelineHistDsp {
  font-weight: bold;
}
td.timelineTime {
  vertical-align: top;
  text-align: right;
  white-space: nowrap;







>
>
>







55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
tr.timelineCurrent td {
  border-radius: 0;
  border-width: 0;
}
span.timelineLeaf {
  font-weight: bold;
}
span.timelineHash {
  font-weight: bold;
}
span.timelineHistDsp {
  font-weight: bold;
}
td.timelineTime {
  vertical-align: top;
  text-align: right;
  white-space: nowrap;
Changes to src/finfo.c.
392
393
394
395
396
397
398


399
400
401
402
403
404
405
  }
  login_anonymous_available();
  tmFlags = timeline_ss_submenu();
  if( tmFlags & TIMELINE_COLUMNAR ){
    zStyle = "Columnar";
  }else if( tmFlags & TIMELINE_COMPACT ){
    zStyle = "Compact";


  }else if( tmFlags & TIMELINE_VERBOSE ){
    zStyle = "Verbose";
  }else if( tmFlags & TIMELINE_CLASSIC ){
    zStyle = "Classic";
  }else{
    zStyle = "Modern";
  }







>
>







392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
  }
  login_anonymous_available();
  tmFlags = timeline_ss_submenu();
  if( tmFlags & TIMELINE_COLUMNAR ){
    zStyle = "Columnar";
  }else if( tmFlags & TIMELINE_COMPACT ){
    zStyle = "Compact";
  }else if( tmFlags & TIMELINE_SIMPLE ){
    zStyle = "Simple";
  }else if( tmFlags & TIMELINE_VERBOSE ){
    zStyle = "Verbose";
  }else if( tmFlags & TIMELINE_CLASSIC ){
    zStyle = "Classic";
  }else{
    zStyle = "Modern";
  }
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749





750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
        @ <td class="timelineDetailCell">
      }
    }
    if( tmFlags & TIMELINE_COMPACT ){
      cgi_printf("<span class='clutter' id='detail-%d'>",frid);
    }
    cgi_printf("<span class='timeline%sDetail'>", zStyle);
    if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ) cgi_printf("(");
    if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
      @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
      @ [%S(zUuid)]</a>
      if( fShowId ){
        int srcId = delta_source_rid(frid);
        if( srcId>0 ){
          @ id:&nbsp;%z(href("%R/deltachain/%d",frid))\
          @ %d(frid)&larr;%d(srcId)</a>
        }else{
          @ id:&nbsp;%z(href("%R/deltachain/%d",frid))%d(frid)</a>
        }
      }
    }





    @ check-in:&nbsp;\
    hyperlink_to_version(zCkin);
    if( fShowId ){
      @ (%d(fmid))
    }
    @ user:&nbsp;\
    hyperlink_to_user(zUser, zDate, ",");
    @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
    if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
      @ size:&nbsp;%d(szFile))
    }else{
      @ size:&nbsp;%d(szFile)
    }
    if( g.perm.Hyperlink && zUuid ){
      const char *z = zFName;
      @ <span id='links-%d(frid)'><span class='timelineExtraLinks'>
      @ %z(href("%R/annotate?filename=%h&checkin=%s",z,zCkin))







|


|










>
>
>
>
>








|
|







731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
        @ <td class="timelineDetailCell">
      }
    }
    if( tmFlags & TIMELINE_COMPACT ){
      cgi_printf("<span class='clutter' id='detail-%d'>",frid);
    }
    cgi_printf("<span class='timeline%sDetail'>", zStyle);
    if( tmFlags & TIMELINE_INLINE ) cgi_printf("(");
    if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
      @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
      @ %S(zUuid)</a>
      if( fShowId ){
        int srcId = delta_source_rid(frid);
        if( srcId>0 ){
          @ id:&nbsp;%z(href("%R/deltachain/%d",frid))\
          @ %d(frid)&larr;%d(srcId)</a>
        }else{
          @ id:&nbsp;%z(href("%R/deltachain/%d",frid))%d(frid)</a>
        }
      }
    }
    if( tmFlags & TIMELINE_SIMPLE ){
      @ <span class='timelineEllipsis' data-id='%d(frid)' \
      @ id='ellipsis-%d(frid)'>...</span>
      @ <span class='clutter' id='detail-%d(frid)'>
    }
    @ check-in:&nbsp;\
    hyperlink_to_version(zCkin);
    if( fShowId ){
      @ (%d(fmid))
    }
    @ user:&nbsp;\
    hyperlink_to_user(zUser, zDate, ",");
    @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
    if( tmFlags & TIMELINE_INLINE ){
      @ size:&nbsp;%d(szFile)
    }else{
      @ size:&nbsp;%d(szFile)
    }
    if( g.perm.Hyperlink && zUuid ){
      const char *z = zFName;
      @ <span id='links-%d(frid)'><span class='timelineExtraLinks'>
      @ %z(href("%R/annotate?filename=%h&checkin=%s",z,zCkin))
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
        }
      }
      zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
      @ %z(zAncLink)[ancestry]</a>
    }
    tag_private_status(frid);
    /* End timelineDetail */
    if( tmFlags & TIMELINE_COMPACT ){
      @ </span></span>
    }else{
      @ </span>
    }
    @ </td></tr>
  }
  db_finalize(&q);
  db_finalize(&qparent);
  if( pGraph ){
    graph_finish(pGraph, 0, TIMELINE_DISJOINT);







|
|

|







798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
        }
      }
      zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
      @ %z(zAncLink)[ancestry]</a>
    }
    tag_private_status(frid);
    /* End timelineDetail */
    if( tmFlags & (TIMELINE_COMPACT|TIMELINE_SIMPLE) ){
      @ </span>)</span>
    }else{
      @ )</span>
    }
    @ </td></tr>
  }
  db_finalize(&q);
  db_finalize(&qparent);
  if( pGraph ){
    graph_finish(pGraph, 0, TIMELINE_DISJOINT);
Changes to src/graph.js.
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
    if(x) x.style.display=value;
  }
  function toggleDetail(){
    var id = parseInt(this.getAttribute('data-id'))
    var x = document.getElementById("detail-"+id);
    if( x.style.display=="inline" ){
      x.style.display="none";
      changeDisplayById("ellipsis-"+id,"inline");
      changeDisplayById("links-"+id,"none");
    }else{
      x.style.display="inline";
      changeDisplayById("ellipsis-"+id,"none");
      changeDisplayById("links-"+id,"inline");
    }
    checkHeight();
  }
  function scrollToSelected(){
    var x = document.getElementsByClassName('timelineSelected');
    if(x[0]){







|



|







724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
    if(x) x.style.display=value;
  }
  function toggleDetail(){
    var id = parseInt(this.getAttribute('data-id'))
    var x = document.getElementById("detail-"+id);
    if( x.style.display=="inline" ){
      x.style.display="none";
      document.getElementById("ellipsis-"+id).textContent = "...";
      changeDisplayById("links-"+id,"none");
    }else{
      x.style.display="inline";
      document.getElementById("ellipsis-"+id).textContent = "←";
      changeDisplayById("links-"+id,"inline");
    }
    checkHeight();
  }
  function scrollToSelected(){
    var x = document.getElementsByClassName('timelineSelected');
    if(x[0]){
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
  }else{
    function checkHeight(){}
  }
  if( tx.scrollToSelect ){
    scrollToSelected();
  }

  /* Set the onclick= attributes for elements of the "Compact" display
  ** mode so that clicking turns the details on and off.
  */
  var lx = topObj.getElementsByClassName('timelineEllipsis');
  var i;
  for(i=0; i<lx.length; i++){
    if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
  }
  lx = topObj.getElementsByClassName('timelineCompactComment');







|
|







762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
  }else{
    function checkHeight(){}
  }
  if( tx.scrollToSelect ){
    scrollToSelected();
  }

  /* Set the onclick= attributes for elements of the "Compact" and
  ** "Simple" views so that clicking turns the details on and off.
  */
  var lx = topObj.getElementsByClassName('timelineEllipsis');
  var i;
  for(i=0; i<lx.length; i++){
    if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
  }
  lx = topObj.getElementsByClassName('timelineCompactComment');
Changes to src/info.c.
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013




1014
1015
1016
1017
1018
1019
1020
1021

1022
1023
1024
1025
1026
1027
1028
    @ <div class="accordion_panel">
    @ <table class="label-value">
    @ <tr><th>Comment:</th><td class="infoComment">\
    @ %!W(zEComment?zEComment:zComment)</td></tr>

    /* The Download: line */
    if( g.perm.Zip  ){
      char *zPJ = db_get("short-project-name", 0);
      char *zUrl;
      Blob projName;
      int jj;
      if( zPJ==0 ) zPJ = db_get("project-name", "unnamed");
      blob_zero(&projName);
      blob_append(&projName, zPJ, -1);
      blob_trim(&projName);
      zPJ = blob_str(&projName);
      for(jj=0; zPJ[jj]; jj++){
        if( (zPJ[jj]>0 && zPJ[jj]<' ') || strchr("\"*/:<>?\\|", zPJ[jj]) ){
          zPJ[jj] = '_';
        }
      }
      zUrl = mprintf("%R/tarball/%S/%t-%S.tar.gz", zUuid, zPJ, zUuid);
      @ <tr><th>Downloads:</th><td>




      @ %z(href("%s",zUrl))Tarball</a>
      @ | %z(href("%R/zip/%S/%t-%S.zip",zUuid, zPJ,zUuid))ZIP archive</a>
      if( g.zLogin!=0 ){
        @ | %z(href("%R/sqlar/%S/%t-%S.sqlar",zUuid,zPJ,zUuid))\
        @ SQL archive</a></td></tr>
      }
      fossil_free(zUrl);
      blob_reset(&projName);

    }

    @ <tr><th>Timelines:</th><td>
    @   %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a>
    if( zParent ){
      @ | %z(href("%R/timeline?p=%!S&unhide",zUuid))ancestors</a>
    }







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

>
>
>
>
|
|
|
|
|
|
|
<
>







991
992
993
994
995
996
997















998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009

1010
1011
1012
1013
1014
1015
1016
1017
    @ <div class="accordion_panel">
    @ <table class="label-value">
    @ <tr><th>Comment:</th><td class="infoComment">\
    @ %!W(zEComment?zEComment:zComment)</td></tr>

    /* The Download: line */
    if( g.perm.Zip  ){















      @ <tr><th>Downloads:</th><td>
      if( robot_would_be_restricted("download") ){
        @ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
      }else{
        char *zBase = archive_base_name(rid);
        @ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a>
        @ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a>
        if( g.zLogin!=0 ){
          @ | %z(href("%R/sqlar/%s.sqlar",zBase))\
          @ SQL archive</a></td></tr>
        }
        fossil_free(zBase);

      }
    }

    @ <tr><th>Timelines:</th><td>
    @   %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a>
    if( zParent ){
      @ | %z(href("%R/timeline?p=%!S&unhide",zUuid))ancestors</a>
    }
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
*/
int preferred_diff_type(void){
  int dflt;
  int res;
  int isBot;
  static char zDflt[2]
    /*static b/c cookie_link_parameter() does not copy it!*/;
  if( client_might_be_a_robot() && robot_restrict_has_tag("diff") ){
    dflt = 0;
    isBot = 1;
  }else{
    dflt = db_get_int("preferred-diff-type",-99);
    if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2;
    isBot = 0;
  }







|







1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
*/
int preferred_diff_type(void){
  int dflt;
  int res;
  int isBot;
  static char zDflt[2]
    /*static b/c cookie_link_parameter() does not copy it!*/;
  if( robot_would_be_restricted("diff") ){
    dflt = 0;
    isBot = 1;
  }else{
    dflt = db_get_int("preferred-diff-type",-99);
    if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2;
    isBot = 0;
  }
Changes to src/robot.c.
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/*
** SETTING: robot-restrict                width=40 block-text
** The VALUE of this setting is a list of GLOB patterns that match
** pages for which complex HTTP requests from unauthenticated clients
** should be disallowed.  "Unauthenticated" means the user is "nobody".
** The recommended value for this setting is:
**
**     timelineX,diff,annotate,fileage,file,finfo,reports
**
** The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
** /vpatch.  The "annotate" tag also covers /blame and /praise.  "zip"
** also covers /tarball and /sqlar.  If a tag has an "X" character appended,
** then it only applies if query parameters are such that the page is
** particularly difficult to compute. In all other case, the tag should
** exactly match the page name.  Useful "X" tags include "timelineX"
** and "zipX".  See the robot-zip-leaf and robot-zip-tag settings
** for additional controls associated with the "zipX" restriction.
**
** Change this setting "off" to disable all robot restrictions.
*/
/*
** SETTING: robot-exception              width=40 block-text
**







|



|


|
|







261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/*
** SETTING: robot-restrict                width=40 block-text
** The VALUE of this setting is a list of GLOB patterns that match
** pages for which complex HTTP requests from unauthenticated clients
** should be disallowed.  "Unauthenticated" means the user is "nobody".
** The recommended value for this setting is:
**
**   timelineX,diff,annotate,fileage,file,finfo,reports,tree,download,hexdump
**
** The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
** /vpatch.  The "annotate" tag also covers /blame and /praise.  "zip"
** also covers /tarball and /sqlar.  If a tag has an "X" character appended
** then it only applies if query parameters are such that the page is
** particularly difficult to compute. In all other case, the tag should
** exactly match the page name.  Useful "X" tags include "timelineX" and
** "zipX".  See the [[robot-zip-leaf]] and [[robot-zip-tag]] settings
** for additional controls associated with the "zipX" restriction.
**
** Change this setting "off" to disable all robot restrictions.
*/
/*
** SETTING: robot-exception              width=40 block-text
**
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322

323
324
325
326
327
328
329
330
331
332
333
334
335
336
** matches.
*/
/*
** SETTING: robot-zip-leaf               boolean
**
** If this setting is true, the robots are allowed to download tarballs,
** ZIP-archives, and SQL-archives even though "zipX" is found in
** the robot-restrict setting as long as the specific check-in being
** downloaded is a leaf check-in.
*/
/*
** SETTING: robot-zip-tag                width=40 block-text
**
** If this setting is a list of GLOB patterns matching tags,
** then robots are allowed to download tarballs, ZIP-archives, and
** SQL-archives even though "zipX" appears in robot-restrict, as long as
** the specific check-in being downloaded has a tags that matches
** the GLOB list of this setting.  Recommended value:  
** "release,robot-access".
*/

/*
** Return the default restriction GLOB
*/
const char *robot_restrict_default(void){
  return "timelineX,diff,annotate,fileage,file,finfo,reports";

}

/*
** Return true if zTag matches one of the tags in the robot-restrict
** setting.
*/
int robot_restrict_has_tag(const char *zTag){
  static const char *zGlob = 0;
  if( zGlob==0 ){
    zGlob = db_get("robot-restrict",robot_restrict_default());
    if( zGlob==0 ) zGlob = "";
  }
  if( zGlob[0]==0 || fossil_strcmp(zGlob, "off")==0 ){
    return 0;







|







|









|
>






|







297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
** matches.
*/
/*
** SETTING: robot-zip-leaf               boolean
**
** If this setting is true, the robots are allowed to download tarballs,
** ZIP-archives, and SQL-archives even though "zipX" is found in
** the [[robot-restrict]] setting as long as the specific check-in being
** downloaded is a leaf check-in.
*/
/*
** SETTING: robot-zip-tag                width=40 block-text
**
** If this setting is a list of GLOB patterns matching tags,
** then robots are allowed to download tarballs, ZIP-archives, and
** SQL-archives even though "zipX" appears in [[robot-restrict]], as long as
** the specific check-in being downloaded has a tags that matches
** the GLOB list of this setting.  Recommended value:  
** "release,robot-access".
*/

/*
** Return the default restriction GLOB
*/
const char *robot_restrict_default(void){
  return "timelineX,diff,annotate,fileage,file,finfo,reports,"
         "tree,hexdump,download";
}

/*
** Return true if zTag matches one of the tags in the robot-restrict
** setting.
*/
static int robot_restrict_has_tag(const char *zTag){
  static const char *zGlob = 0;
  if( zGlob==0 ){
    zGlob = db_get("robot-restrict",robot_restrict_default());
    if( zGlob==0 ) zGlob = "";
  }
  if( zGlob[0]==0 || fossil_strcmp(zGlob, "off")==0 ){
    return 0;
406
407
408
409
410
411
412
413


414
415


416
417








418

419
420
421
422
423
424
425
426
427
428
429

430













431
432
433


434

435
436
437
438
439
440
441
    re_free(pRe);
  }
  fossil_free(zRequest);
  return bMatch;
}

/*
** Check to see if the page named in the argument is on the


** robot-restrict list.  If it is on the list and if the user
** is "nobody" then bring up a captcha to test to make sure that


** client is not a robot.
**








** This routine returns true if a captcha was rendered and if subsequent

** page generation should be aborted.  It returns false if the page
** should not be restricted and should be rendered normally.
*/
int robot_restrict(const char *zTag){
  if( robot.resultCache==KNOWN_NOT_ROBOT ) return 0;
  if( !robot_restrict_has_tag(zTag) ) return 0;
  if( !client_might_be_a_robot() ) return 0;
  if( robot_exception() ){
    robot.resultCache = KNOWN_NOT_ROBOT;
    return 0;
  }















  /* Generate the proof-of-work captcha */
  ask_for_proof_that_client_is_not_robot();
  return 1;


}


/*
** Check to see if a robot is allowed to download a tarball, ZIP archive,
** or SQL Archive for a particular check-in identified by the "rid" 
** argument.  Return true to block the download.  Return false to
** continue.  Prior to returning true, a captcha is presented to the user.
** No output is generated when returning false.







|
>
>
|
<
>
>
|

>
>
>
>
>
>
>
>
|
>
|
<

|







>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
>
>
|
>







407
408
409
410
411
412
413
414
415
416
417

418
419
420
421
422
423
424
425
426
427
428
429
430
431
432

433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
    re_free(pRe);
  }
  fossil_free(zRequest);
  return bMatch;
}

/*
** Return true if one or more of the conditions below are true.
** Return false if all of the following are false:
**
**   *  The zTag is on the robot-restrict list

**
**   *  The client that submitted the HTTP request might be
**      a robot
**
**   *  The Request URI does not match any of the exceptions
**      in the robot-exception setting.
**
** In other words, return true if a call to robot_restrict() would
** return true and false if a call to robot_restrict() would return
** false.
**
** The difference between this routine an robot_restrict() is that
** this routine does not generate a proof-of-work captcha.  This
** routine does not change the HTTP reply in any way.  It simply
** returns true or false.

*/
int robot_would_be_restricted(const char *zTag){
  if( robot.resultCache==KNOWN_NOT_ROBOT ) return 0;
  if( !robot_restrict_has_tag(zTag) ) return 0;
  if( !client_might_be_a_robot() ) return 0;
  if( robot_exception() ){
    robot.resultCache = KNOWN_NOT_ROBOT;
    return 0;
  }
  return 1;
}

/*
** Check to see if the page named in the argument is on the
** robot-restrict list.  If it is on the list and if the user
** is might be a robot, then bring up a captcha to test to make
** sure that client is not a robot.
**
** This routine returns true if a captcha was rendered and if subsequent
** page generation should be aborted.  It returns false if the page
** should not be restricted and should be rendered normally.
*/
int robot_restrict(const char *zTag){
  if( robot_would_be_restricted(zTag) ){
    /* Generate the proof-of-work captcha */
    ask_for_proof_that_client_is_not_robot();
    return 1;
  }else{
    return 0;
  }
}

/*
** Check to see if a robot is allowed to download a tarball, ZIP archive,
** or SQL Archive for a particular check-in identified by the "rid" 
** argument.  Return true to block the download.  Return false to
** continue.  Prior to returning true, a captcha is presented to the user.
** No output is generated when returning false.
Changes to src/setup.c.
116
117
118
119
120
121
122


123
124
125
126
127
128
129
    setup_menu_entry("Robot-Defense", "setup_robot",
      "Settings for configure defense against robots");
    setup_menu_entry("Settings", "setup_settings",
      "Web interface to the \"fossil settings\" command");
  }
  setup_menu_entry("Timeline", "setup_timeline",
    "Timeline display preferences");


  if( setup_user ){
    setup_menu_entry("Login-Group", "setup_login_group",
      "Manage single sign-on between this repository and others"
      " on the same server");
    setup_menu_entry("Tickets", "tktsetup",
      "Configure the trouble-ticketing system for this repository");
    setup_menu_entry("Wiki", "setup_wiki",







>
>







116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
    setup_menu_entry("Robot-Defense", "setup_robot",
      "Settings for configure defense against robots");
    setup_menu_entry("Settings", "setup_settings",
      "Web interface to the \"fossil settings\" command");
  }
  setup_menu_entry("Timeline", "setup_timeline",
    "Timeline display preferences");
  setup_menu_entry("Tarballs and ZIPs", "setup_download",
    "Preferences for auto-generated tarballs and ZIP files");
  if( setup_user ){
    setup_menu_entry("Login-Group", "setup_login_group",
      "Manage single sign-on between this repository and others"
      " on the same server");
    setup_menu_entry("Tickets", "tktsetup",
      "Configure the trouble-ticketing system for this repository");
    setup_menu_entry("Wiki", "setup_wiki",
138
139
140
141
142
143
144

145
146

147
148
149
150
151
152
153
  setup_menu_entry("Search","srchsetup",
    "Configure the built-in search engine");
  setup_menu_entry("URL Aliases", "waliassetup",
    "Configure URL aliases");
  if( setup_user ){
    setup_menu_entry("Notification", "setup_notification",
      "Automatic notifications of changes via outbound email");

    setup_menu_entry("Transfers", "xfersetup",
      "Configure the transfer system for this repository");

  }
  setup_menu_entry("Skins", "setup_skin_admin",
    "Select and/or modify the web interface \"skins\"");
  setup_menu_entry("Moderation", "setup_modreq",
    "Enable/Disable requiring moderator approval of Wiki and/or Ticket"
    " changes and attachments.");
  setup_menu_entry("Ad-Unit", "setup_adunit",







>


>







140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
  setup_menu_entry("Search","srchsetup",
    "Configure the built-in search engine");
  setup_menu_entry("URL Aliases", "waliassetup",
    "Configure URL aliases");
  if( setup_user ){
    setup_menu_entry("Notification", "setup_notification",
      "Automatic notifications of changes via outbound email");
#if 0  /* Disabled for now.  Does this even work? */
    setup_menu_entry("Transfers", "xfersetup",
      "Configure the transfer system for this repository");
#endif
  }
  setup_menu_entry("Skins", "setup_skin_admin",
    "Select and/or modify the web interface \"skins\"");
  setup_menu_entry("Moderation", "setup_modreq",
    "Enable/Disable requiring moderator approval of Wiki and/or Ticket"
    " changes and attachments.");
  setup_menu_entry("Ad-Unit", "setup_adunit",
486
487
488
489
490
491
492
493
494
495
496

497
498
499
500
501
502
503
  @ A captcha test is is rendered instead.
  @ The default value for this setting is:
  @ <p>
  @ &emsp;&emsp;&emsp;<tt>%h(robot_restrict_default())</tt>
  @ <p>
  @ The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and 
  @ /vpatch.  The "annotate" tag covers /annotate and also /blame and
  @ /praise.  The "zip" covers itself and also /tarball and /sqlar. If a
  @ tag has an "X" character appended, then it only applies if query
  @ parameters are such that the page is expensive and/or unusual.
  @ In all other case, the tag should exactly match the page name.

  @
  @ To disable robot restrictions, change this setting to "off".
  @ (Property: robot-restrict)
  @ <br>
  textarea_attribute("", 2, 80,
      "robot-restrict", "rbrestrict", robot_restrict_default(), 0);








|
|
|
|
>







490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
  @ A captcha test is is rendered instead.
  @ The default value for this setting is:
  @ <p>
  @ &emsp;&emsp;&emsp;<tt>%h(robot_restrict_default())</tt>
  @ <p>
  @ The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and 
  @ /vpatch.  The "annotate" tag covers /annotate and also /blame and
  @ /praise.  The "zip" covers itself and also /tarball and /sqlar.
  @ If a tag has an "X" character appended (ex: "timelineX") then it only
  @ applies if query parameters are such that the page is expensive
  @ and/or unusual. In all other case, the tag should exactly match
  @ the page name.
  @
  @ To disable robot restrictions, change this setting to "off".
  @ (Property: robot-restrict)
  @ <br>
  textarea_attribute("", 2, 80,
      "robot-restrict", "rbrestrict", robot_restrict_default(), 0);

983
984
985
986
987
988
989





990
991
992
993
994
995
996
  static const char *const azTimeFormats[] = {
      "0", "HH:MM",
      "1", "HH:MM:SS",
      "2", "YYYY-MM-DD HH:MM",
      "3", "YYMMDD HH:MM",
      "4", "(off)"
  };





  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }

  style_set_current_feature("setup");







>
>
>
>
>







988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
  static const char *const azTimeFormats[] = {
      "0", "HH:MM",
      "1", "HH:MM:SS",
      "2", "YYYY-MM-DD HH:MM",
      "3", "YYMMDD HH:MM",
      "4", "(off)"
  };
  static const char *const azLeafMark[] = {
      "0", "No",
      "1", "Yes",
      "2", "Yes - with emphasis",
  };
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }

  style_set_current_feature("setup");
1071
1072
1073
1074
1075
1076
1077






1078
1079
1080
1081
1082
1083
1084
            "tdf", "0", count(azTimeFormats)/2, azTimeFormats);
  @ <p>If the "HH:MM" or "HH:MM:SS" format is selected, then the date is shown
  @ in a separate box (using CSS class "timelineDate") whenever the date
  @ changes.  With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
  @ the complete date and time is shown on every timeline entry using the
  @ CSS class "timelineTime". (Property: "timeline-date-format")</p>







  @ <hr>
  entry_attribute("Max timeline comment length", 6,
                  "timeline-max-comment", "tmc", "0", 0);
  @ <p>The maximum length of a comment to be displayed in a timeline.
  @ "0" there is no length limit.
  @ (Property: "timeline-max-comment")</p>








>
>
>
>
>
>







1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
            "tdf", "0", count(azTimeFormats)/2, azTimeFormats);
  @ <p>If the "HH:MM" or "HH:MM:SS" format is selected, then the date is shown
  @ in a separate box (using CSS class "timelineDate") whenever the date
  @ changes.  With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
  @ the complete date and time is shown on every timeline entry using the
  @ CSS class "timelineTime". (Property: "timeline-date-format")</p>

  @ <hr>
  multiple_choice_attribute("Leaf Markings", "timeline-mark-leaves",
            "tml", "1", count(azLeafMark)/2, azLeafMark);
  @ <p>Should timeline entries for leaf check-ins be identified in the
  @ detail section.  (Property: "timeline-mark-leaves")</p>

  @ <hr>
  entry_attribute("Max timeline comment length", 6,
                  "timeline-max-comment", "tmc", "0", 0);
  @ <p>The maximum length of a comment to be displayed in a timeline.
  @ "0" there is no length limit.
  @ (Property: "timeline-max-comment")</p>

1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
  @ Other repositories use this URL to clone or sync against this repository.
  @ This is also the basename for hyperlinks included in email alert text.
  @ Omit the trailing "/".
  @ If this repo will not be set up as a persistent server and will not
  @ be sending email alerts, then leave this entry blank.
  @ Suggested value: "%h(g.zBaseURL)"
  @ (Property: "email-url")</p>
  @ <hr>
  entry_attribute("Tarball and ZIP-archive Prefix", 20, "short-project-name",
                  "spn", "", 0);
  @ <p>This is used as a prefix on the names of generated tarballs and
  @ ZIP archive. For best results, keep this prefix brief and avoid special
  @ characters such as "/" and "\".
  @ If no tarball prefix is specified, then the full Project Name above is used.
  @ (Property: "short-project-name")
  @ </p>
  @ <hr>
  entry_attribute("Download Tag", 20, "download-tag", "dlt", "trunk", 0);
  @ <p>The <a href='%R/download'>/download</a> page is designed to provide
  @ a convenient place for newbies
  @ to download a ZIP archive or a tarball of the project.  By default,
  @ the latest trunk check-in is downloaded.  Change this tag to something
  @ else (ex: release) to alter the behavior of the /download page.
  @ (Property: "download-tag")
  @ </p>
  @ <hr>
  entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
  @ <p>Enter the pathname of the page to display when the "Home" menu
  @ option is selected and when no pathname is
  @ specified in the URL.  For example, if you visit the url:</p>
  @
  @ <blockquote><p>%h(g.zBaseURL)</p></blockquote>







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







1340
1341
1342
1343
1344
1345
1346


















1347
1348
1349
1350
1351
1352
1353
  @ Other repositories use this URL to clone or sync against this repository.
  @ This is also the basename for hyperlinks included in email alert text.
  @ Omit the trailing "/".
  @ If this repo will not be set up as a persistent server and will not
  @ be sending email alerts, then leave this entry blank.
  @ Suggested value: "%h(g.zBaseURL)"
  @ (Property: "email-url")</p>


















  @ <hr>
  entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
  @ <p>Enter the pathname of the page to display when the "Home" menu
  @ option is selected and when no pathname is
  @ specified in the URL.  For example, if you visit the url:</p>
  @
  @ <blockquote><p>%h(g.zBaseURL)</p></blockquote>
1424
1425
1426
1427
1428
1429
1430



































1431
1432
1433
1434
1435


1436
1437
1438
1439
1440
1441
1442
1443


1444
1445
1446

1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
  @
  @ <p>The default value is blank, meaning no added entries.
  @ (Property: sitemap-extra)
  @ <p>
  textarea_attribute("Custom Sitemap Entries", 8, 80,
      "sitemap-extra", "smextra", "", 0);
  @ <hr>



































  @ <p>Configuration for the <a href="%R/tarlist">/tarlist</a> page.
  @ The value is a TCL list divided into pairs.
  @ <ol>
  @ <li> The first term of each pair is an integer (N).
  @ <li> The second term of each pair is a glob pattern (PATTERN).


  @ </ol>
  @ For each pair, the most recent N check-ins that have a tag that
  @ matches PATTERN are included in on the /tarlist page.  The special
  @ pattern of "OPEN-LEAF" matches all open leaf check-ins.  Example:
  @ <blockquote><tt>1 trunk 3 release 5 OPEN-LEAF</tt></blockquote>
  @ The example pattern above shows the union of the most recent trunk
  @ check-in, the 5 most recent open leaf check-ins, and the 3 most
  @ recent check-ins tagged with "release".  


  @ <p>
  @ The /tarlist page is omitted from the <a href="%R/sitemap">/sitemap</a>
  @ if the first token is "0".  The default value is "1 trunk".

  @ (Property: suggested-tarlist)
  @ <p>
  textarea_attribute("Check-ins To Show On /tarlist", 2, 80,
      "suggested-tarlist", "sgtrlst", "", 0);
  @ <hr>
  @ <p><input type="submit"  name="submit" value="Apply Changes"></p>
  @ </div></form>
  db_end_transaction(0);
  style_finish_page();
}








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|

|
|
>
>

<
<
|
<
|
<
<
>
>

|
|
>
|

|
|







1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471


1472

1473


1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
  @
  @ <p>The default value is blank, meaning no added entries.
  @ (Property: sitemap-extra)
  @ <p>
  textarea_attribute("Custom Sitemap Entries", 8, 80,
      "sitemap-extra", "smextra", "", 0);
  @ <hr>
  @ <p><input type="submit"  name="submit" value="Apply Changes"></p>
  @ </div></form>
  db_end_transaction(0);
  style_finish_page();
}

/*
** WEBPAGE: setup_download
**
** The "Admin/Download" page.  Requires Setup privilege.
*/
void setup_download(void){
  login_check_credentials();
  if( !g.perm.Setup ){
    login_needed(0);
    return;
  }

  style_set_current_feature("setup");
  style_header("Tarball and ZIP Downloads");
  db_begin_transaction();
  @ <form action="%R/setup_download" method="post"><div>
  login_insert_csrf_secret();
  @ <input type="submit"  name="submit" value="Apply Changes"></p>
  @ <hr>
  entry_attribute("Tarball and ZIP Name Prefix", 20, "short-project-name",
                  "spn", "", 0);
  @ <p>This is used as a prefix for the names of generated tarballs and
  @ ZIP archive. Keep this prefix brief and use only lower-case ASCII
  @ characters, digits, "_", "-" in the name. If this setting is blank,
  @ then the full <a href='%R/help/project-name'>project-name</a> setting
  @ is used instead.
  @ (Property: "short-project-name")
  @ </p>
  @ <hr>
  @ <p><b>Configuration for the <a href="%R/download">/download</a> page.</b>
  @ <p>The value is a TCL list divided into groups of four tokens:
  @ <ol>
  @ <li> Maximum number of matches (COUNT).
  @ <li> Tag to match using glob (TAG).
  @ <li> Maximum age of check-ins to match (MAX_AGE).
  @ <li> Comment to apply to matches (COMMENT).
  @ </ol>


  @ Each 4-tuple will match zero or more check-ins.  The /download page

  @ displays the union of matches from all 4-tuples.


  @ See the <a href="%R/help/suggested-downloads">suggested-downloads</a>
  @ setting documentation for further detail.
  @ <p>
  @ The /download page is omitted from the <a href="%R/sitemap">/sitemap</a>
  @ if the first token is "0" or "off" or "no".  The default value 
  @ for this setting is "off".
  @ (Property: <a href="%R/help/suggested-downloads">suggested-downloads</a>)
  @ <p>
  textarea_attribute("", 4, 80,
      "suggested-downloads", "sgtrlst", "off", 0);
  @ <hr>
  @ <p><input type="submit"  name="submit" value="Apply Changes"></p>
  @ </div></form>
  db_end_transaction(0);
  style_finish_page();
}

Changes to src/sitemap.c.
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
    @   <li>%z(href("%R/fileage?name=trunk"))File ages for Trunk</a></li>
    @   <li>%z(href("%R/uvlist"))Unversioned Files</a>
    if( g.perm.Write && zEditGlob[0]!=0 ){
      @   <li>%z(href("%R/fileedit"))On-line File Editor</li>
    }
    @ </ul>
  }
  if( g.perm.Zip && db_get_boolean("suggested-tarlist",1)!=0 ){
    @ <li>%z(href("%R/tarlist"))Tarballs and ZIPs</a>
  }
  if( g.perm.Read ){
    @ <li>%z(href("%R/timeline"))Project Timeline</a>
    @ <ul>
    @   <li>%z(href("%R/reports"))Activity Reports</a></li>
    @   <li>%z(href("%R/sitemap-timeline"))Other timelines</a></li>
    @ </ul>







|
|







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
    @   <li>%z(href("%R/fileage?name=trunk"))File ages for Trunk</a></li>
    @   <li>%z(href("%R/uvlist"))Unversioned Files</a>
    if( g.perm.Write && zEditGlob[0]!=0 ){
      @   <li>%z(href("%R/fileedit"))On-line File Editor</li>
    }
    @ </ul>
  }
  if( g.perm.Zip && db_get_boolean("suggested-downloads",0)!=0 ){
    @ <li>%z(href("%R/download"))Tarballs and ZIPs</a>
  }
  if( g.perm.Read ){
    @ <li>%z(href("%R/timeline"))Project Timeline</a>
    @ <ul>
    @   <li>%z(href("%R/reports"))Activity Reports</a></li>
    @   <li>%z(href("%R/sitemap-timeline"))Other timelines</a></li>
    @ </ul>
Changes to src/tar.c.
29
30
31
32
33
34
35























































36
37
38
39
40
41
42
  unsigned char *aHdr;      /* Space for building headers */
  char *zSpaces;            /* Spaces for padding */
  char *zPrevDir;           /* Name of directory for previous entry */
  int nPrevDirAlloc;        /* size of zPrevDir */
  Blob pax;                 /* PAX data */
} tball;

























































/*
** field lengths of 'ustar' name and prefix fields.
*/
#define USTAR_NAME_LEN    100
#define USTAR_PREFIX_LEN  155








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
  unsigned char *aHdr;      /* Space for building headers */
  char *zSpaces;            /* Spaces for padding */
  char *zPrevDir;           /* Name of directory for previous entry */
  int nPrevDirAlloc;        /* size of zPrevDir */
  Blob pax;                 /* PAX data */
} tball;

/*
** Convert a string so that it contains only lower-case ASCII, digits,
** "_" and "-".  Changes are made in-place.
*/
static void sanitize_name(char *zName){
  int i;
  char c;
  for(i=0; (c = zName[i])!=0; i++){
    if( fossil_isupper(c) ){
      zName[i] = fossil_tolower(c);
    }else if( !fossil_isalnum(c) && c!='_' && c!='-' ){
      if( c<=0x7f ){
        zName[i] = '_';
      }else{
                /*  123456789 123456789 123456  */
        zName[i] = "abcdefghijklmnopqrstuvwxyz"[(unsigned)c%26];
      }
    }
  }
}

/*
** Compute a sensible base-name for an archive file (tarball, ZIP, or SQLAR)
** based on the rid of the check-in contained in that file.
**
**      PROJECTNAME-DATETIME-HASHPREFIX
**
** So that the name will be safe to use as a URL or a filename on any system,
** the name is only allowed to contain lower-case ASCII alphabetics,
** digits, '_' and '-'.  Upper-case ASCII is converted to lower-case.  All
** other bytes are mapped into a lower-case alphabetic.
**
** The value returned is obtained from mprintf() or fossil_strdup() and should
** be released by the caller using fossil_free().
*/
char *archive_base_name(int rid){
  char *zPrefix;
  char *zName;
  zPrefix = db_get("short-project-name",0);
  if( zPrefix==0 || zPrefix[0]==0 ){
    zPrefix = db_get("project-name","unnamed");
  }
  zName = db_text(0,
    "SELECT %Q||"
          " strftime('-%%Y%%m%%d%%H%%M%%S-',event.mtime)||"
          " substr(blob.uuid,1,10)"
     " FROM blob, event LEFT JOIN config"
    " WHERE blob.rid=%d"
      " AND event.objid=%d"
      " AND config.name='project-name'",
    zPrefix, rid, rid);
  fossil_free(zPrefix);
  sanitize_name(zName);
  return zName;
}

/*
** field lengths of 'ustar' name and prefix fields.
*/
#define USTAR_NAME_LEN    100
#define USTAR_PREFIX_LEN  155

651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
















































680
681
682
683
684

685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709









710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728

729
730
731
732
733
734
735
736
  }
  zOut = g.argv[3];
  if( fossil_strcmp("/dev/null",zOut)==0 || fossil_strcmp("",zOut)==0 ){
    zOut = 0;
  }

  if( zName==0 ){
    zName = db_text("default-name",
       "SELECT replace(%Q,' ','_') "
          " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
          " || substr(blob.uuid, 1, 10)"
       "  FROM event, blob"
       " WHERE event.objid=%d"
       "   AND blob.rid=%d",
       db_get("project-name", "unnamed"), rid, rid
    );
  }
  tarball_of_checkin(rid, zOut ? &tarball : 0,
                     zName, pInclude, pExclude, listFlag);
  glob_free(pInclude);
  glob_free(pExclude);
  if( listFlag ) fflush(stdout);
  if( zOut ){
    blob_write_to_file(&tarball, zOut);
    blob_reset(&tarball);
  }
}

/*
















































** Check to see if the input string is of one of the following
** two the forms:
**
**        check-in-name/filename.ext                       (1)
**        tag-name/check-in-name/filename.txt              (2)

**
** In other words, check to see if the input string contains either
** a check-in name or a tag-name and a check-in name separated by
** a slash.  There must be either 1 or 2 "/" characters.  In the
** second form, tag-name must be an individual tag (not a branch-tag)
** that is found on the check-in identified by the check-in-name.
**
** If the condition is true, then:
**
**   *  Make *pzName point to the fielname suffix only
**   *  return a copy of the check-in name in memory from mprintf().
**
** If the condition is false, leave *pzName unchanged and return either
** NULL or an empty string.  Normally NULL is returned, however an
** empty string is returned for format (2) if check-in-name does not
** match tag-name.
**
** Format (2) is specifically designed to allow URLs like this:
**
**      /tarball/release/UUID/PROJECT.tar.gz
**
** Such URLs will pass through most anti-robot filters because of the
** "/tarball/release" prefix will match the suggested "robot-exception"
** pattern and can still refer to an historic release rather than just
** the most recent release.









*/
char *tar_uuid_from_name(char **pzName){
  char *zName = *pzName;      /* Original input */
  int n1 = 0;                 /* Bytes in first prefix (tag-name) */
  int n2 = 0;                 /* Bytes in second prefix (check-in-name) */
  int n = 0;                  /* max(n1,n2) */
  int i;                      /* Loop counter */
  for(i=n1=n2=0; zName[i]; i++){
    if( zName[i]=='/' ){
      if( n1==0 ){
        n = n1 = i;
      }else if( n2==0 ){
        n = n2 = i;
      }else{
        return 0;   /* More than two "/" characters seen */
      }
    }
  }
  if( n1==0 ){

    return 0;    /* No prefix of any kind */
  }
  if( zName[n+1]==0 ){
    return 0;    /* No filename suffix */
  }
  if( n2==0 ){
    /* Format (1): check-in name only.  The check-in-name is not verified */
    zName[n1] = 0;







|
<
<
<
<
<
<
<
<













>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|
>



|





|















>
>
>
>
>
>
>
>
>



















>
|







706
707
708
709
710
711
712
713








714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
  }
  zOut = g.argv[3];
  if( fossil_strcmp("/dev/null",zOut)==0 || fossil_strcmp("",zOut)==0 ){
    zOut = 0;
  }

  if( zName==0 ){
    zName = archive_base_name(rid);








  }
  tarball_of_checkin(rid, zOut ? &tarball : 0,
                     zName, pInclude, pExclude, listFlag);
  glob_free(pInclude);
  glob_free(pExclude);
  if( listFlag ) fflush(stdout);
  if( zOut ){
    blob_write_to_file(&tarball, zOut);
    blob_reset(&tarball);
  }
}

/*
** This is a helper routine for tar_uuid_from_name().  It handles
** the case where *pzName contains no "/" character.  Check for
** format (3).  Return the hash if the name matches format (3),
** or return NULL if it does not.
*/
static char *format_three_parser(const char *zName){
  int iDot = 0;    /* Index in zName[] of the first '.' */
  int iDash1 = 0;  /* Index in zName[] of the '-' before the timestamp */
  int iDash2 = 0;  /* Index in zName[] of the '-' between timestamp and hash */
  int nHash;       /* Size of the hash */
  char *zHash;     /* A copy of the hash value */
  char *zDate;     /* Copy of the timestamp */
  char *zUuid;     /* Final result */
  int i;           /* Loop query */
  Stmt q;          /* Query to verify that hash and timestamp agree */

  for(i=0; zName[i]; i++){
    char c = zName[i];
    if( c=='.' ){ iDot = i;  break; }
    if( c=='-' ){ iDash1 = iDash2; iDash2 = i; }
    if( !fossil_isalnum(c) && c!='_' && c!='-' ){ break; }
  }
  if( iDot==0 ) return 0;
  if( iDash1==0 ) return 0;
  nHash = iDot - iDash2 - 1;
  if( nHash<8 ) return 0;                /* HASH value too short */  
  if( (iDash2 - iDash1)!=15 ) return 0;  /* Wrong timestamp size */
  zHash = fossil_strndup(&zName[iDash2+1], nHash);
  zDate = fossil_strndup(&zName[iDash1+1], 14);
  db_prepare(&q, 
    "SELECT blob.uuid"
    "  FROM blob JOIN event ON event.objid=blob.rid"
    " WHERE blob.uuid GLOB '%q*'"
    "   AND strftime('%%Y%%m%%d%%H%%M%%S',event.mtime)='%q'", 
    zHash, zDate
  );
  fossil_free(zHash);
  fossil_free(zDate);
  if( db_step(&q)==SQLITE_ROW ){
    zUuid = fossil_strdup(db_column_text(&q,0));
  }else{
    zUuid = 0;
  }
  db_finalize(&q);
  return zUuid;
}

/*
** Check to see if the input string is of one of the following
** two the forms:
**
**        check-in-name/filename.ext                       (1)
**        tag-name/check-in-name/filename.ext              (2)
**        project-datetime-hash.ext                        (3)
**
** In other words, check to see if the input string contains either
** a check-in name or a tag-name and a check-in name separated by
** a slash.  There must be between 0 or 2 "/" characters.  In the
** second form, tag-name must be an individual tag (not a branch-tag)
** that is found on the check-in identified by the check-in-name.
**
** If the condition is true, then:
**
**   *  Make *pzName point to the filename suffix only
**   *  return a copy of the check-in name in memory from mprintf().
**
** If the condition is false, leave *pzName unchanged and return either
** NULL or an empty string.  Normally NULL is returned, however an
** empty string is returned for format (2) if check-in-name does not
** match tag-name.
**
** Format (2) is specifically designed to allow URLs like this:
**
**      /tarball/release/UUID/PROJECT.tar.gz
**
** Such URLs will pass through most anti-robot filters because of the
** "/tarball/release" prefix will match the suggested "robot-exception"
** pattern and can still refer to an historic release rather than just
** the most recent release.
**
** Format (3) is designed to allow URLs like this:
**
**     /tarball/fossil-20251018193920-d6c9aee97df.tar.gz
**
** In other words, filename itself contains sufficient information to
** uniquely identify the check-in, including a timestamp of the form
** YYYYMMDDHHMMSS and a prefix of the check-in hash.  The timestamp
** and hash must immediately preceed the first "." in the name.
*/
char *tar_uuid_from_name(char **pzName){
  char *zName = *pzName;      /* Original input */
  int n1 = 0;                 /* Bytes in first prefix (tag-name) */
  int n2 = 0;                 /* Bytes in second prefix (check-in-name) */
  int n = 0;                  /* max(n1,n2) */
  int i;                      /* Loop counter */
  for(i=n1=n2=0; zName[i]; i++){
    if( zName[i]=='/' ){
      if( n1==0 ){
        n = n1 = i;
      }else if( n2==0 ){
        n = n2 = i;
      }else{
        return 0;   /* More than two "/" characters seen */
      }
    }
  }
  if( n1==0 ){
    /* Check for format (3) */
    return format_three_parser(*pzName);
  }
  if( zName[n+1]==0 ){
    return 0;    /* No filename suffix */
  }
  if( n2==0 ){
    /* Format (1): check-in name only.  The check-in-name is not verified */
    zName[n1] = 0;
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946





947
948
949
950
951
952
953
954









955

956
957
958

959

960



961
962
963
964
965
966
967
968

969
970
971
972
973
974
975
976


977





978
979
980









981
982

983

984
985
986
987



988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004


1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015

1016
1017
1018

1019
1020
1021


1022
1023
1024
1025
1026
1027
1028
1029
1030


































1031
1032
1033
1034
1035

1036
1037
1038
1039

1040
1041
1042
1043
1044
1045
1046
1047

1048
1049
1050
1051

1052
1053
1054

1055
1056
1057
1058
1059
1060
1061
1062
1063
1064





1065
1066
1067
1068
1069
1070
1071
1072



















1073
1074































































  g.zOpenRevision = 0;
  blob_reset(&cacheKey);
  cgi_set_content(&tarball);
  cgi_set_content_type("application/x-compressed");
}

/*
** This routine is called for each check-in on the /tarlist page to
** construct the "extra" information after the description.
*/
static void tarlist_extra(
  Stmt *pQuery,               /* Current row of the timeline query */
  int tmFlags,                /* Flags to www_print_timeline() */
  const char *zThisUser,      /* Suppress links to this user */
  const char *zThisTag        /* Suppress links to this tag */
){





  int rid = db_column_int(pQuery, 0);
  const char *zUuid = db_column_text(pQuery, 1);
  const char *zDate = db_column_text(pQuery, 2);
  char *zBrName = branch_of_rid(rid);
  static const char *zProject = 0;
  int nProject;
  char *zNm;










  if( zProject==0 ) zProject = db_get("project-name","unnamed");

  zNm = mprintf("%s-%sZ-%.8s", zProject, zDate, zUuid);
  nProject = (int)strlen(zProject);
  zNm[nProject+11] = 'T';

  @ <strong>%h(zBrName)</strong><br>\

  @ %z(href("%R/timeline?c=%!S&y=ci&n=11",zUuid))<button>Context</button></a>



  @ %z(href("%R/tarball/%!S/%t.tar.gz",zUuid,zNm))\
  @    <button>Tarball</button></a>
  @  %z(href("%R/zip/%!S/%t.zip",zUuid,zNm))\
  @    <button>ZIP&nbsp;Archive</button></a>
  fossil_free(zBrName);
  fossil_free(zNm);
}


/*
** SETTING: suggested-tarlist               width=70  block-text
**
** This setting controls the suggested tarball/ZIP downloads on the
** [[/tarlist]] page.  The value is a TCL list.  Each pair of items
** defines a set of check-ins to be added to the suggestion list.
** The first item of each pair is an integer count (N) and second
** item is a tag GLOB pattern (PATTERN). For each pair, the most


** recent N check-ins that have a tag matching PATTERN are added





** to the list.  The special pattern "OPEN-LEAF" matches any open
** leaf check-in.
**









** Example:
**

**        3 OPEN-LEAF 3 release 1 trunk

**
** The value causes the /tarlist page to show the union of the 3
** most recent open leaves, the three most recent check-ins marked
** "release", and the single most recent trunk check-in.



*/

/*
** WEBPAGE: /tarlist
**
** Show a special no-graph timeline of recent important check-ins with
** an opportunity to pull tarballs and ZIPs.
*/
void tarlist_page(void){
  Stmt q;                       /* The actual timeline query */
  const char *zTarlistCfg;      /* Configuration string */
  char **azItem;                /* Decomposed elements of zTarlistCfg */
  int *anItem;                  /* Bytes in each term of azItem[] */
  int nItem;                    /* Number of terms in azItem[] */
  int i;                        /* Loop counter */
  int tmFlags;                  /* Timeline display flags */
  int n;                        /* Number of suggested downloads */



  login_check_credentials();
  if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }

  style_set_current_feature("timeline");
  style_header("Suggested Tarballs And ZIP Archives");

  zTarlistCfg = db_get("suggested-tarlist","1 trunk");
  db_multi_exec(
    "CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY);"
  );

  if( !g.interp ) Th_FossilInit(0);
  Th_SplitList(g.interp, zTarlistCfg, (int)strlen(zTarlistCfg),
                   &azItem, &anItem, &nItem);

  for(i=0; i<nItem-1; i+=2){
    int cnt;
    char *zLabel;


    if( anItem[i]==1 && azItem[i][0]=='*' ){
      cnt = -1;
    }else if( anItem[i]<1 ){
      cnt = 0;
    }else{
      cnt = atoi(azItem[i]);
    }
    if( cnt==0 ) continue;
    zLabel = fossil_strndup(azItem[i+1],anItem[i+1]);


































    if( fossil_strcmp("OPEN-LEAF",zLabel)==0 ){
      db_multi_exec(
        "INSERT OR IGNORE INTO tarlist(rid)"
         " SELECT leaf.rid FROM leaf, event"
          " WHERE event.objid=leaf.rid"

            " AND NOT EXISTS(SELECT 1 FROM tagxref"
                            " WHERE tagxref.rid=leaf.rid"
                              " AND tagid=%d AND tagtype>0)"
          " ORDER BY event.mtime DESC LIMIT %d", TAG_CLOSED, cnt

      );
    }else{
      db_multi_exec(
        "WITH taglist(tid) AS"
            " (SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q')"
        "INSERT OR IGNORE INTO tarlist(rid)"
        " SELECT event.objid FROM event CROSS JOIN tagxref"
        "  WHERE event.type='ci'"

        "    AND tagxref.tagid IN taglist"
        "    AND tagtype>0"
        "    AND tagxref.rid=event.objid"
        "  ORDER BY event.mtime DESC LIMIT %d", zLabel, cnt

      );
    }
    fossil_free(zLabel);

  }
  Th_Free(g.interp, azItem);

  n = db_int(0, "SELECT count(*) FROM tarlist");
  if( n==0 ){
    @ <h2>No tarball/ZIP suggestions are available at this time</h2>
  }else{
    @ <h2>%d(n) Tarball/ZIP Download Suggestions:</h2>
    db_prepare(&q,
      "%s AND blob.rid IN tarlist ORDER BY event.mtime DESC",





      timeline_query_for_www()
    );

    tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL | TIMELINE_COLUMNAR
            | TIMELINE_BRCOLOR;
    www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, tarlist_extra);
    db_finalize(&q);
  }



















  style_finish_page();
}






































































|


|





>
>
>
>
>
|
|
<
|
<
<
|

>
>
>
>
>
>
>
>
>
|
>
|
<
<
>
|
>
|
>
>
>
|
|
|
|
|
|
|
|
>

|


|

|
|
>
>
|
>
>
>
>
>
|
|

>
>
>
>
>
>
>
>
>


>
|
>

|
<
|
>
>
>



|




|








>
>





|

|

|

>



>
|
|
|
>
>









>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|
|

>



|
>





|
|

>



|
>



>







|

|
>
>
>
>
>





|


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059

1060


1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074


1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125

1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
  g.zOpenRevision = 0;
  blob_reset(&cacheKey);
  cgi_set_content(&tarball);
  cgi_set_content_type("application/x-compressed");
}

/*
** This routine is called for each check-in on the /download page to
** construct the "extra" information after the description.
*/
void download_extra(
  Stmt *pQuery,               /* Current row of the timeline query */
  int tmFlags,                /* Flags to www_print_timeline() */
  const char *zThisUser,      /* Suppress links to this user */
  const char *zThisTag        /* Suppress links to this tag */
){
  const char *zType = db_column_text(pQuery, 7);
  assert( zType!=0 );
  if( zType[0]!='c' ){
    timeline_extra(pQuery, tmFlags, zThisUser, zThisTag);
  }else{    
    int rid = db_column_int(pQuery, 0);
    const char *zUuid = db_column_text(pQuery, 1);

    char *zBrName = branch_of_rid(rid);


    char *zNm;

    if( tmFlags & TIMELINE_COLUMNAR ){
      @ <nobr>check-in:&nbsp;\
      @   %z(href("%R/info/%!S",zUuid))<span class='timelineHash'>\
      @   %S(zUuid)</span></a></nobr><br>
      if( fossil_strcmp(zBrName,"trunk")!=0 ){
        @ <nobr>branch:&nbsp;\
        @   %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a></nobr><br>\
      }
    }else{
      if( (tmFlags & TIMELINE_CLASSIC)==0 ){
        @ check-in:&nbsp;%z(href("%R/info/%!S",zUuid))\
        @ <span class='timelineHash'>%S(zUuid)</span></a>


      }
      if( (tmFlags & TIMELINE_GRAPH)==0 && fossil_strcmp(zBrName,"trunk")!=0 ){
        @ branch:&nbsp;\
        @   %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
      }
    }
    zNm = archive_base_name(rid);
    @ %z(href("%R/tarball/%s.tar.gz",zNm))\
    @    <button>Tarball</button></a>
    @  %z(href("%R/zip/%s.zip",zNm))\
    @    <button>ZIP&nbsp;Archive</button></a>
    fossil_free(zBrName);
    fossil_free(zNm);
  }
}

/*
** SETTING: suggested-downloads               width=70  block-text
**
** This setting controls the suggested tarball/ZIP downloads on the
** [[/download]] page.  The value is a TCL list.  Each set of four items
** defines a set of check-ins to be added to the suggestion list.
** The items in each group are:
**
** |    COUNT   TAG   MAX_AGE    COMMENT
**
** COUNT is the number of check-ins to match, starting with the most
** recent and working bacwards in time.  Check-ins match if they contain
** the tag TAG.  If MAX_AGE is not an empty string, then it specifies
** the maximum age of any matching check-in.  COMMENT is an optional
** comment for each match.
**
** The special value of "OPEN-LEAF" for TAG matches any check-in that
** is an open leaf.
**
** MAX_AGE is of the form "{AMT UNITS}"  where AMT is a floating point
** value and UNITS is one of "seconds", "hours", "days", "weeks", "months",
** or "years".  If MAX_AGE is an empty string then there is no age limit.
**
** If COMMENT is not an empty string, then it is an additional comment
** added to the output description of the suggested download.  The idea of
** COMMENT is to explain to the reader why a check-in is a suggested
** download.  
**
** Example:
**
** |       1   trunk     {}         {Latest Trunk Check-in}
** |       5   OPEN-LEAF {1 month}  {Active Branch}
** |       999 release   {1 year}   {Official Release}
**
** The value causes the /download page to show the union of the most

** recent trunk check-in of any age, the five most recent
** open leaves within the past month, and essentially
** all releases within the past year.  If the same check-in matches more
** than one rule, the COMMENT of the first match is used.
*/

/*
** WEBPAGE: /download
**
** Show a special no-graph timeline of recent important check-ins with
** an opportunity to pull tarballs and ZIPs.
*/
void download_page(void){
  Stmt q;                       /* The actual timeline query */
  const char *zTarlistCfg;      /* Configuration string */
  char **azItem;                /* Decomposed elements of zTarlistCfg */
  int *anItem;                  /* Bytes in each term of azItem[] */
  int nItem;                    /* Number of terms in azItem[] */
  int i;                        /* Loop counter */
  int tmFlags;                  /* Timeline display flags */
  int n;                        /* Number of suggested downloads */
  double rNow;                  /* Current time.  Julian day number */
  int bPlainTextCom;            /* Use plain-text comments */

  login_check_credentials();
  if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }

  style_set_current_feature("timeline");
  style_header("Suggested Downloads");

  zTarlistCfg = db_get("suggested-downloads","off");
  db_multi_exec(
    "CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY, com TEXT);"
  );
  rNow = db_double(0.0,"SELECT julianday()");
  if( !g.interp ) Th_FossilInit(0);
  Th_SplitList(g.interp, zTarlistCfg, (int)strlen(zTarlistCfg),
                   &azItem, &anItem, &nItem);
  bPlainTextCom = db_get_boolean("timeline-plaintext",0);
  for(i=0; i<nItem-3; i+=4){
    int cnt;             /* The number of instances of zLabel to use */
    char *zLabel;        /* The label to match */
    double rStart;       /* Starting time, julian day number */
    char *zComment = 0;  /* Comment to apply */
    if( anItem[i]==1 && azItem[i][0]=='*' ){
      cnt = -1;
    }else if( anItem[i]<1 ){
      cnt = 0;
    }else{
      cnt = atoi(azItem[i]);
    }
    if( cnt==0 ) continue;
    zLabel = fossil_strndup(azItem[i+1],anItem[i+1]);
    if( anItem[i+2]==0 ){
      rStart = 0.0;
    }else{
      char *zMax = fossil_strndup(azItem[i+2], anItem[i+2]);
      double r = atof(zMax);
      if( strstr(zMax,"sec") ){
        rStart = rNow - r/86400.0;
      }else
      if( strstr(zMax,"hou") ){
        rStart = rNow - r/24.0;
      }else
      if( strstr(zMax,"da") ){
        rStart = rNow - r;
      }else
      if( strstr(zMax,"wee") ){
        rStart = rNow - r*7.0;
      }else
      if( strstr(zMax,"mon") ){
        rStart = rNow - r*30.44;
      }else
      if( strstr(zMax,"yea") ){
        rStart = rNow - r*365.24;
      }else
      { /* Default to seconds */
        rStart = rNow - r/86400.0;
      }
    }
    if( anItem[i+3]==0 ){
      zComment = fossil_strdup("");
    }else if( bPlainTextCom ){
      zComment = mprintf("** %.*s ** ", anItem[i+3], azItem[i+3]);
    }else{
      zComment = mprintf("<b>%.*s</b>\n<p>", anItem[i+3], azItem[i+3]);
    }
    if( fossil_strcmp("OPEN-LEAF",zLabel)==0 ){
      db_multi_exec(
        "INSERT OR IGNORE INTO tarlist(rid,com)"
         " SELECT leaf.rid, %Q FROM leaf, event"
          " WHERE event.objid=leaf.rid"
            " AND event.mtime>=%.6f"
            " AND NOT EXISTS(SELECT 1 FROM tagxref"
                            " WHERE tagxref.rid=leaf.rid"
                              " AND tagid=%d AND tagtype>0)"
          " ORDER BY event.mtime DESC LIMIT %d",
          zComment, rStart, TAG_CLOSED, cnt
      );
    }else{
      db_multi_exec(
        "WITH taglist(tid) AS"
            " (SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q')"
        "INSERT OR IGNORE INTO tarlist(rid,com)"
        " SELECT event.objid, %Q FROM event CROSS JOIN tagxref"
        "  WHERE event.type='ci'"
        "    AND event.mtime>=%.6f"
        "    AND tagxref.tagid IN taglist"
        "    AND tagtype>0"
        "    AND tagxref.rid=event.objid"
        "  ORDER BY event.mtime DESC LIMIT %d",
        zLabel, zComment, rStart, cnt
      );
    }
    fossil_free(zLabel);
    fossil_free(zComment);
  }
  Th_Free(g.interp, azItem);

  n = db_int(0, "SELECT count(*) FROM tarlist");
  if( n==0 ){
    @ <h2>No tarball/ZIP suggestions are available at this time</h2>
  }else{
    @ <h2>%d(n) Tarball/ZIP Download Suggestion%s(n>1?"s":""):</h2>
    db_prepare(&q,
      "WITH matches AS (%s AND blob.rid IN (SELECT rid FROM tarlist))\n"
      "SELECT blobRid, uuid, timestamp,"
            " com||comment,"
            " user, leaf, bgColor, eventType, tags, tagid, brief, mtime"
      "  FROM matches JOIN tarlist ON tarlist.rid=blobRid"
      " ORDER BY matches.mtime DESC",
      timeline_query_for_www()
    );

    tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL | TIMELINE_COLUMNAR
            | TIMELINE_BRCOLOR;
    www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, download_extra);
    db_finalize(&q);
  }
  if( g.perm.Clone ){
    char *zNm = fossil_strdup(db_get("project-name","clone"));
    sanitize_name(zNm);    
    @ <hr>
    @ <h2>You Can Clone This Repository</h2>
    @
    @ <p>Clone this repository by running a command similar to the following:
    @ <blockquote><pre>
    @ fossil  clone  %s(g.zBaseURL)  %h(zNm).fossil
    @ </pre></blockquote>
    @ <p>A clone gives you local access to all historical content.
    @ Cloning is a bandwidth- and CPU-efficient alternative to extracting
    @ multiple tarballs and ZIPs.
    @ Do a web search for "fossil clone" or similar to find additional
    @ information about using a cloned Fossil repository.  Or ask your
    @ favorite AI how to extract content from a Fossil clone.
    fossil_free(zNm);
  }

  style_finish_page();
}

/*
** WEBPAGE: rchvdwnld
**
** Short for "archive download".  This page should have a single name=
** query parameter that is a check-in hash.  It present a menu of possible
** download options for that check-in, including tarball, ZIP, or SQLAR.
**
** This is a utility page.  The /dir and /tree pages sometimes have a
** "Download" option in their submenu which redirects here.  Those pages
** used to have separate "Tarball" and "ZIP" submenu entries, but as
** submenu entries appear in alphabetical order, that caused the two
** submenu entries to be separated from one another, which is distracting.
*/
void rchvdwnld_page(void){
  const char *zUuid;
  char *zBase;
  int nUuid;
  int rid;
  login_check_credentials();
  if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
  if( robot_restrict("zip") || robot_restrict("download") ) return;

  zUuid = P("name");
  if( zUuid==0
   || (nUuid = (int)strlen(zUuid))<6
   || !validate16(zUuid,-1)
   || (rid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUuid))==0
   || !db_exists("SELECT 1 from event WHERE type='ci' AND objid=%d",rid)
  ){
    fossil_redirect_home();
  }
  zUuid = db_text(zUuid, "SELECT uuid FROM blob WHERE rid=%d", rid);
  style_header("Downloads For Check-in %!S", zUuid);
  zBase = archive_base_name(rid);
  @ <div class="section accordion">Downloads for check-in \
  @ %z(href("%R/info/%!S",zUuid))%S(zUuid)</a></div>
  @ <div class="accordion_panel">
  @ <table class="label-value">
  @ <tr>
  @ <th>Tarball:</th>
  @ <td>%z(href("%R/tarball/%s.tar.gz",zBase))\
  @ %s(g.zBaseURL)/tarball/%s(zBase).tar.gz</a></td>
  @ </tr>
  @
  @ <tr>
  @ <th>ZIP:</th>
  @ <td>%z(href("%R/zip/%s.zip",zBase))\
  @ %s(g.zBaseURL)/zip/%s(zBase).zip</a></td>
  @ </tr>
  @
  @ <tr>
  @ <th>SQLAR:</th>
  @ <td>%z(href("%R/sqlar/%s.sqlar",zBase))\
  @ %s(g.zBaseURL)/sqlar/%s(zBase).sqlar</a></td>
  @ </tr>
  @ </table></div>
  fossil_free(zBase);
  @ <div class="section accordion">Context</div><div class="accordion_panel">
  render_checkin_context(rid, 0, 0, 0);
  @ </div>
  style_finish_page();
}
Changes to src/timeline.c.
113
114
115
116
117
118
119


120
121
122
123
124
125
126
127
#define TIMELINE_SHOWRID  0x0000400 /* Show RID values in addition to hashes */
#define TIMELINE_BISECT   0x0000800 /* Show supplemental bisect information */
#define TIMELINE_COMPACT  0x0001000 /* Use the "compact" view style */
#define TIMELINE_VERBOSE  0x0002000 /* Use the "detailed" view style */
#define TIMELINE_MODERN   0x0004000 /* Use the "modern" view style */
#define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */
#define TIMELINE_CLASSIC  0x0010000 /* Use the "classic" view style */


#define TIMELINE_VIEWS    0x001f000 /* Mask for all of the view styles */
#define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */
#define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */
#define TIMELINE_CHPICK   0x0400000 /* Show cherrypick merges */
#define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */
#define TIMELINE_XMERGE   0x1000000 /* Omit merges from off-graph nodes */
#define TIMELINE_NOTKT    0x2000000 /* Omit extra ticket classes */
#define TIMELINE_FORUMTXT 0x4000000 /* Render all forum messages */







>
>
|







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#define TIMELINE_SHOWRID  0x0000400 /* Show RID values in addition to hashes */
#define TIMELINE_BISECT   0x0000800 /* Show supplemental bisect information */
#define TIMELINE_COMPACT  0x0001000 /* Use the "compact" view style */
#define TIMELINE_VERBOSE  0x0002000 /* Use the "detailed" view style */
#define TIMELINE_MODERN   0x0004000 /* Use the "modern" view style */
#define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */
#define TIMELINE_CLASSIC  0x0010000 /* Use the "classic" view style */
#define TIMELINE_SIMPLE   0x0020000 /* Use the "simple" view style */
#define TIMELINE_INLINE   0x0033000 /* Mask for views with in-line display */
#define TIMELINE_VIEWS    0x003f000 /* Mask for all of the view styles */
#define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */
#define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */
#define TIMELINE_CHPICK   0x0400000 /* Show cherrypick merges */
#define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */
#define TIMELINE_XMERGE   0x1000000 /* Omit merges from off-graph nodes */
#define TIMELINE_NOTKT    0x2000000 /* Omit extra ticket classes */
#define TIMELINE_FORUMTXT 0x4000000 /* Render all forum messages */
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207












208
209


210
211
212
213
214
215
216

217
218
219
220
221
222
223
224
225
226
227
228
229





230
231
232
233
234
235
236
** in a timeline.
**
** Example:  "(check-in: [abcdefg], user: drh, tags: trunk)"
**
** This routine is used if no xExtra argument is supplied to
** www_print_timeline().
*/
static void defaultExtra(
  Stmt *pQuery,               /* Current row of the timeline query */
  int tmFlags,                /* Flags to www_print_timeline() */
  const char *zThisUser,      /* Suppress links to this user */
  const char *zThisTag        /* Suppress links to this tag */
){
  int rid = db_column_int(pQuery, 0);
  const char *zUuid = db_column_text(pQuery, 1);
  const char *zDate = db_column_text(pQuery, 2);
  const char *zType = db_column_text(pQuery, 7);
  const char *zUser = db_column_text(pQuery, 4);
  const char *zTagList = db_column_text(pQuery, 8);
  int tagid = db_column_int(pQuery, 9);
  const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";

  if( (tmFlags & (TIMELINE_CLASSIC|TIMELINE_VERBOSE|TIMELINE_COMPACT))!=0 ){
    cgi_printf("(");
  }

/* Set to 1 for historical appearance.  Set to 0 for new experimental look */
#define OLD_STYLE 1
#if OLD_STYLE
  if( (tmFlags & TIMELINE_CLASSIC)==0 ){
    if( zType[0]=='c' ){












      int isLeaf = db_column_int(pQuery, 5);
      if( isLeaf ){


        if( has_closed_tag(rid) ){
          @ <span class='timelineLeaf'>Closed-Leaf</span>
        }else{
          @ <span class='timelineLeaf'>Leaf</span>
        }
      }
      cgi_printf("check-in:&nbsp;%z%S</a> ",

                  href("%R/info/%!S",zUuid),zUuid);
    }else if( zType[0]=='e' && tagid ){
      cgi_printf("technote:&nbsp;");
      hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
    }else{
      cgi_printf("artifact:&nbsp;%z%S</a> ",
                 href("%R/info/%!S",zUuid),zUuid);
    }
  }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t'
            || zType[0]=='n' || zType[0]=='f'){
    cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
  }
#endif /* OLD_STYLE */






  if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
    char *zLink;
    if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){
      zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate);
    }else{
      zLink = mprintf("%R/timeline?u=%h&c=%t&y=a&vfx", zDispUser, zDate);







|














|



<
<
<


>
>
>
>
>
>
>
>
>
>
>
>
|
|
>
>
|
|
<
|


|
>
|











|
>
>
>
>
>







179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
** in a timeline.
**
** Example:  "(check-in: [abcdefg], user: drh, tags: trunk)"
**
** This routine is used if no xExtra argument is supplied to
** www_print_timeline().
*/
void timeline_extra(
  Stmt *pQuery,               /* Current row of the timeline query */
  int tmFlags,                /* Flags to www_print_timeline() */
  const char *zThisUser,      /* Suppress links to this user */
  const char *zThisTag        /* Suppress links to this tag */
){
  int rid = db_column_int(pQuery, 0);
  const char *zUuid = db_column_text(pQuery, 1);
  const char *zDate = db_column_text(pQuery, 2);
  const char *zType = db_column_text(pQuery, 7);
  const char *zUser = db_column_text(pQuery, 4);
  const char *zTagList = db_column_text(pQuery, 8);
  int tagid = db_column_int(pQuery, 9);
  const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";

  if( (tmFlags & TIMELINE_INLINE)!=0 ){
    cgi_printf("(");
  }




  if( (tmFlags & TIMELINE_CLASSIC)==0 ){
    if( zType[0]=='c' ){
      const char *zPrefix = 0;
      static int markLeaves = -1;
      if( markLeaves<0 ){
        markLeaves = db_get_int("timeline-mark-leaves",1);
        if( markLeaves<0 ) markLeaves = 1;
      }
      if( strcmp(zUuid, MANIFEST_UUID)==0 ){
        /* This will only ever happen when Fossil is drawing a timeline for
        ** its own self-host repository.  If the timeline shows the specific
        ** check-in corresponding to the current executable, then tag that
        ** check-in with "self" */
        zPrefix = "self&nbsp;";
      }else if( markLeaves && db_column_int(pQuery,5) ){
        if( markLeaves==1 ){
          zPrefix = has_closed_tag(rid) ? "closed&nbsp;" : "leaf&nbsp;";
        }else{
          zPrefix = has_closed_tag(rid) ?
               "<span class='timelineLeaf'>Closed-Leaf</span>\n" :

               "<span class='timelineLeaf'>Leaf</span>\n";
        }
      }
      cgi_printf("%scheck-in:&nbsp;%z<span class='timelineHash'>"
                 "%S</span></a> ",
                  zPrefix, href("%R/info/%!S",zUuid),zUuid);
    }else if( zType[0]=='e' && tagid ){
      cgi_printf("technote:&nbsp;");
      hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
    }else{
      cgi_printf("artifact:&nbsp;%z%S</a> ",
                 href("%R/info/%!S",zUuid),zUuid);
    }
  }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t'
            || zType[0]=='n' || zType[0]=='f'){
    cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
  }

  if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
    @ <span class='timelineEllipsis' id='ellipsis-%d(rid)' \
    @ data-id='%d(rid)'>...</span>
    @ <span class='clutter' id='detail-%d(rid)'>
  }

  if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
    char *zLink;
    if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){
      zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate);
    }else{
      zLink = mprintf("%R/timeline?u=%h&c=%t&y=a&vfx", zDispUser, zDate);
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
















329
330
331
332
333
334
335
    }else{
      cgi_printf(" id:&nbsp;%z%d</a>",
                 href("%R/deltachain/%d",rid), rid);
    }
  }
  tag_private_status(rid);

#if !OLD_STYLE
  if( (tmFlags & TIMELINE_CLASSIC)==0 ){
    if( zType[0]=='e' && tagid ){
      char *zId = db_text(0,
          "SELECT substr(tagname,7) FROM tag WHERE tagid=abs(%d)", tagid);
      cgi_printf(" technote:&nbsp;%z%S</a>",
                 href("%R/technote/%t",zId), zId);
    }else{
      cgi_printf(" hash:&nbsp;%z%S</a>", href("%R/info/%!S", zUuid), zUuid);
    }
  }
#endif /* !OLD_STYLE */

  /* End timelineDetail */
  if( (tmFlags & (TIMELINE_CLASSIC|TIMELINE_VERBOSE|TIMELINE_COMPACT))!=0 ){
    cgi_printf(")");
  }

  if( tmFlags & TIMELINE_COMPACT ){
    @ </span></span>
  }else{
    @ </span>
  }
}


/*
** SETTING: timeline-truncate-at-blank  boolean default=off
**
** If enabled, check-in comments displayed on the timeline are truncated
** at the first blank line of the comment text.  The comment text after
** the first blank line is only seen in the /info or similar pages that
** show details about the check-in.
*/
/*
** SETTING: timeline-tslink-info       boolean default=off
**
** The hyperlink on the timestamp associated with each timeline entry,
** on the far left-hand side of the screen, normally targets another
** /timeline page that shows the entry in context.  However, if this
** option is turned on, that hyperlink targets the /info page showing
** the details of the entry.
*/


















/*
** Output a timeline in the web format given a query.  The query
** should return these columns:
**
**    0.  rid
**    1.  artifact hash







<
|
<
<
<
|
<
<
<
|
|
<
<

|


<
<
<
<
<
<




















|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







296
297
298
299
300
301
302

303



304



305
306


307
308
309
310






311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
    }else{
      cgi_printf(" id:&nbsp;%z%d</a>",
                 href("%R/deltachain/%d",rid), rid);
    }
  }
  tag_private_status(rid);


  if( (tmFlags & TIMELINE_SIMPLE)!=0 ){



    cgi_printf("</span>");  /* End of the declutter span */



  }



  /* End timelineDetail */
  if( (tmFlags & TIMELINE_INLINE)!=0 ){
    cgi_printf(")");
  }






}


/*
** SETTING: timeline-truncate-at-blank  boolean default=off
**
** If enabled, check-in comments displayed on the timeline are truncated
** at the first blank line of the comment text.  The comment text after
** the first blank line is only seen in the /info or similar pages that
** show details about the check-in.
*/
/*
** SETTING: timeline-tslink-info       boolean default=off
**
** The hyperlink on the timestamp associated with each timeline entry,
** on the far left-hand side of the screen, normally targets another
** /timeline page that shows the entry in context.  However, if this
** option is turned on, that hyperlink targets the /info page showing
** the details of the entry.
*/
/*
** SETTING: timeline-mark-leaves       width=5 default=1
**
** Determine whether or not leaf check-ins are marked as such in the
** details section of the timeline.  The value is an integer between 0
** and 2:
**
**    0   Do not show any special marking for leaf check-ins.
** 
**    1   Show just "leaf" or "closed"
**
**    2   Show "Leaf" or "Closed-Leaf" with emphasis
**
** The default is currently 1.  Prior to 2025-10-19, the default was 2.
** This setting has no effect on the "Classic" view, which always behaves
** as if the setting were 2.
*/

/*
** Output a timeline in the web format given a query.  The query
** should return these columns:
**
**    0.  rid
**    1.  artifact hash
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392


393
394
395
396
397
398
399
                              ** page rather than the /timeline page */
  char *zMainBranch = db_get("main-branch","trunk");


  if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
    vid = db_lget_int("checkout", 0);
  }
  if( xExtra==0 ) xExtra = defaultExtra;
  zPrevDate[0] = 0;
  mxWikiLen = db_get_int("timeline-max-comment", 0);
  dateFormat = db_get_int("timeline-date-format", 0);
  bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
  bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
  if( (tmFlags & TIMELINE_VIEWS)==0 ){
    tmFlags |= timeline_ss_cookie();
  }
  if( tmFlags & TIMELINE_COLUMNAR ){
    zStyle = "Columnar";
  }else if( tmFlags & TIMELINE_COMPACT ){
    zStyle = "Compact";


  }else if( tmFlags & TIMELINE_VERBOSE ){
    zStyle = "Verbose";
  }else if( tmFlags & TIMELINE_CLASSIC ){
    zStyle = "Classic";
  }else{
    zStyle = "Modern";
  }







|












>
>







392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
                              ** page rather than the /timeline page */
  char *zMainBranch = db_get("main-branch","trunk");


  if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
    vid = db_lget_int("checkout", 0);
  }
  if( xExtra==0 ) xExtra = timeline_extra;
  zPrevDate[0] = 0;
  mxWikiLen = db_get_int("timeline-max-comment", 0);
  dateFormat = db_get_int("timeline-date-format", 0);
  bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
  bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
  if( (tmFlags & TIMELINE_VIEWS)==0 ){
    tmFlags |= timeline_ss_cookie();
  }
  if( tmFlags & TIMELINE_COLUMNAR ){
    zStyle = "Columnar";
  }else if( tmFlags & TIMELINE_COMPACT ){
    zStyle = "Compact";
  }else if( tmFlags & TIMELINE_SIMPLE ){
    zStyle = "Simple";
  }else if( tmFlags & TIMELINE_VERBOSE ){
    zStyle = "Verbose";
  }else if( tmFlags & TIMELINE_CLASSIC ){
    zStyle = "Classic";
  }else{
    zStyle = "Modern";
  }
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
        blob_reset(&truncated);
        drawDetailEllipsis = 0;
      }else{
        cgi_printf("%W",blob_str(&comment));
      }
    }

    if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){
      /* This will only ever happen when Fossil is drawing a timeline for
      ** its own self-host repository.  If the timeline shows the specific
      ** check-in corresponding to the current executable, then tag that
      ** check-in with "This is me!". */
      @ <b>&larr; This is me!</b>
    }

    @ </span>
    blob_reset(&comment);

    /* Generate extra information and hyperlinks that follow the comment.
    ** Example:  "(check-in: [abcdefg], user: drh, tags: trunk)"
    */
    if( drawDetailEllipsis ){







<
<
<
<
<
<
<
<







757
758
759
760
761
762
763








764
765
766
767
768
769
770
        blob_reset(&truncated);
        drawDetailEllipsis = 0;
      }else{
        cgi_printf("%W",blob_str(&comment));
      }
    }









    @ </span>
    blob_reset(&comment);

    /* Generate extra information and hyperlinks that follow the comment.
    ** Example:  "(check-in: [abcdefg], user: drh, tags: trunk)"
    */
    if( drawDetailEllipsis ){
1267
1268
1269
1270
1271
1272
1273
















1274
1275
1276
1277
1278
1279
1280
    assert( i<=count(az) );
  }
  if( i>2 ){
    style_submenu_multichoice("y", i/2, az, isDisabled);
  }
}

















/*
** Return the default value for the "ss" cookie or query parameter.
** The "ss" cookie determines the graph style.  See the
** timeline_view_styles[] global constant for a list of choices.
*/
const char *timeline_default_ss(void){
  static const char *zSs = 0;







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
    assert( i<=count(az) );
  }
  if( i>2 ){
    style_submenu_multichoice("y", i/2, az, isDisabled);
  }
}

/*
** SETTING: timeline-default-style            width=5 default=m
**
** This setting determines the default "view style" for timelines.
** The setting should be a single character, one of the following:
**
**    c     Compact
**    j     Columnar
**    m     Modern
**    s     Simple
**    v     Verbose
**    x     Classic
**
** The default value is m (Modern).
*/

/*
** Return the default value for the "ss" cookie or query parameter.
** The "ss" cookie determines the graph style.  See the
** timeline_view_styles[] global constant for a list of choices.
*/
const char *timeline_default_ss(void){
  static const char *zSs = 0;
1291
1292
1293
1294
1295
1296
1297

1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309

1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
  const char *v = cookie_value("ss",0);
  if( v==0 ) v = timeline_default_ss();
  switch( v[0] ){
    case 'c':  tmFlags = TIMELINE_COMPACT;  break;
    case 'v':  tmFlags = TIMELINE_VERBOSE;  break;
    case 'j':  tmFlags = TIMELINE_COLUMNAR; break;
    case 'x':  tmFlags = TIMELINE_CLASSIC;  break;

    default:   tmFlags = TIMELINE_MODERN;   break;
  }
  return tmFlags;
}

/* Available timeline display styles, together with their y= query
** parameter names.
*/
const char *const timeline_view_styles[] = {
  "m", "Modern View",
  "j", "Columnar View",
  "c", "Compact View",

  "v", "Verbose View",
  "x", "Classic View",
};
#if INTERFACE
# define N_TIMELINE_VIEW_STYLE 5
#endif

/*
** Add the select/option box to the timeline submenu that is used to
** set the ss= parameter that determines the viewing mode.
**
** Return the TIMELINE_* value appropriate for the view-style.







>












>




|







1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
  const char *v = cookie_value("ss",0);
  if( v==0 ) v = timeline_default_ss();
  switch( v[0] ){
    case 'c':  tmFlags = TIMELINE_COMPACT;  break;
    case 'v':  tmFlags = TIMELINE_VERBOSE;  break;
    case 'j':  tmFlags = TIMELINE_COLUMNAR; break;
    case 'x':  tmFlags = TIMELINE_CLASSIC;  break;
    case 's':  tmFlags = TIMELINE_SIMPLE;   break;
    default:   tmFlags = TIMELINE_MODERN;   break;
  }
  return tmFlags;
}

/* Available timeline display styles, together with their y= query
** parameter names.
*/
const char *const timeline_view_styles[] = {
  "m", "Modern View",
  "j", "Columnar View",
  "c", "Compact View",
  "s", "Simple View",
  "v", "Verbose View",
  "x", "Classic View",
};
#if INTERFACE
# define N_TIMELINE_VIEW_STYLE 6
#endif

/*
** Add the select/option box to the timeline submenu that is used to
** set the ss= parameter that determines the viewing mode.
**
** Return the TIMELINE_* value appropriate for the view-style.
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
**    mionly          Show related parents but not related children.
**    nowiki          Do not show wiki associated with branch or tag
**    ms=MATCHSTYLE   Set tag name match algorithm.  One of "exact", "glob",
**                    "like", or "regexp".
**    u=USER          Only show items associated with USER
**    y=TYPE          'ci', 'w', 't', 'n', 'e', 'f', or 'all'.
**    ss=VIEWSTYLE    c: "Compact", v: "Verbose", m: "Modern", j: "Columnar",
*                     x: "Classic".
**    advm            Use the "Advanced" or "Busy" menu design.
**    ng              No Graph.
**    ncp             Omit cherrypick merges
**    nd              Do not highlight the focus check-in
**    nsm             Omit the submenu
**    nc              Omit all graph colors other than highlights
**    v               Show details of files changed







|







1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
**    mionly          Show related parents but not related children.
**    nowiki          Do not show wiki associated with branch or tag
**    ms=MATCHSTYLE   Set tag name match algorithm.  One of "exact", "glob",
**                    "like", or "regexp".
**    u=USER          Only show items associated with USER
**    y=TYPE          'ci', 'w', 't', 'n', 'e', 'f', or 'all'.
**    ss=VIEWSTYLE    c: "Compact", v: "Verbose", m: "Modern", j: "Columnar",
**                    x: "Classic".
**    advm            Use the "Advanced" or "Busy" menu design.
**    ng              No Graph.
**    ncp             Omit cherrypick merges
**    nd              Do not highlight the focus check-in
**    nsm             Omit the submenu
**    nc              Omit all graph colors other than highlights
**    v               Show details of files changed
1891
1892
1893
1894
1895
1896
1897

1898

1899
1900
1901
1902
1903
1904
1905
  }
  if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum)
   || (bisectLocal && !g.perm.Setup)
  ){
    login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
    return;
  }

  if( (zBefore || zCirca) && robot_restrict("timelineX") ) return;

  if( !bisectLocal ){
    etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0);
  }
  cookie_read_parameter("y","y");
  zType = P("y");
  if( zType==0 ){
    zType = g.perm.Read ? "ci" : "all";







>
|
>







1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
  }
  if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum)
   || (bisectLocal && !g.perm.Setup)
  ){
    login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
    return;
  }
  if( zBefore || zCirca ){
    if( robot_restrict("timelineX") ) return;
  }
  if( !bisectLocal ){
    etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0);
  }
  cookie_read_parameter("y","y");
  zType = P("y");
  if( zType==0 ){
    zType = g.perm.Read ? "ci" : "all";
Changes to src/zip.c.
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
  }
  zOut = g.argv[3];
  if( fossil_strcmp(zOut,"")==0 || fossil_strcmp(zOut,"/dev/null")==0 ){
    zOut = 0;
  }

  if( zName==0 ){
    zName = db_text("default-name",
       "SELECT replace(%Q,' ','_') "
          " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
          " || substr(blob.uuid, 1, 10)"
       "  FROM event, blob"
       " WHERE event.objid=%d"
       "   AND blob.rid=%d",
       db_get("project-name", "unnamed"), rid, rid
    );
  }
  zip_of_checkin(eType, rid, zOut ? &zip : 0,
                 zName, pInclude, pExclude, listFlag);
  glob_free(pInclude);
  glob_free(pExclude);
  if( zOut ){
    blob_write_to_file(&zip, zOut);







|
<
<
<
<
<
<
<
<







862
863
864
865
866
867
868
869








870
871
872
873
874
875
876
  }
  zOut = g.argv[3];
  if( fossil_strcmp(zOut,"")==0 || fossil_strcmp(zOut,"/dev/null")==0 ){
    zOut = 0;
  }

  if( zName==0 ){
    zName = archive_base_name(rid);








  }
  zip_of_checkin(eType, rid, zOut ? &zip : 0,
                 zName, pInclude, pExclude, listFlag);
  glob_free(pInclude);
  glob_free(pExclude);
  if( zOut ){
    blob_write_to_file(&zip, zOut);
Changes to www/changes.wiki.
1
2
3
4
5
6
7
8
9


10
11
12






























13
14
15
16
17
18
19
<title>Change Log</title>

<h2 id='v2_28'>Changes for version 2.28 (pending)</h2><ol>
  <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
      <li> The default configuration now allows robots to download any tarball
           or similar, to better support of automated build systems.
      <li> No special tag "zipX" for the [/help/robot-restrict|robot-restrict]
           setting blocks robot access to tarballs, but with exceptions to support
           automated build systems.


      </ol>
  <li> A drop-down menu of recent branches is now possible for the submenu, and
       is used in the code browser.






























  <li> The [/help/timeline|timeline command] is enhanced with the new
       "<tt>-u|--for-user</tt>" option.
</ol>

<h2 id='v2_27'>Changes for version 2.27 (2025-09-30)</h2><ol>
  <li> Close a potential Denial-of-Service attack against any public-facing Fossil
       server involving exponential behavior in Fossil's regexp implementation.






|


>
>


|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<title>Change Log</title>

<h2 id='v2_28'>Changes for version 2.28 (pending)</h2><ol>
  <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
      <li> The default configuration now allows robots to download any tarball
           or similar, to better support of automated build systems.
      <li> New special tag "zipX" for the [/help/robot-restrict|robot-restrict]
           setting blocks robot access to tarballs, but with exceptions to support
           automated build systems.
      <li> Enhancements to the default value for the 
           [/help/robot-restrict|robot-restrict setting].
      </ol>
  <li> A drop-down menu of recent branches is now possible for the submenu, and
       is used in the [/dir?ci=trunk|code browser].
  <li> Easier access to generated tarballs and ZIPs:<ol type="a">
       <li> When in the [/dir?ci=trunk|code browser] at the top-level,
            a new "Download" submenu option is available to take the
            user to a page where he can download a tarball or ZIP archive.
       <li> New [/help/www/download|/download page] is available. When
            configured using the new
            [/help/suggested-downloads|suggested-downloads setting], a
            link to [/download] named "Tarballs and ZIPs" appears in the
            [/sitemap] and thus on the hamburger menu.
       <li> The filenames for tarballs and ZIPs are now standardized to
            include a timestamp and a hash prefix.
       </ol>
  <li> Timeline enhancements:<ol type="a">
       <li> A new "Simple" view is available.  This is compromise between "Verbose"
            and "Compact" that shows only the check-in hash rather than the full
            detail section.  There is an ellipsis that one can click on to see the
            full detail text.
       <li> The artifact hash in the detail section of each timeline entry is now
            emphasized (controlled by CSS) to make it easier to locate amid all
            the other text and links.
       <li> When clicking on the ellipsis in "Compact" or "Simple" views, the ellipsis
            is replaced by a left arrow ("←") which can be clicked to hide the extra
            detail again.
       <li> New setting [/help/timeline-mark-leaves|timeline-mark-leaves] controls
            whether or not leaf check-ins are marked in the timeline.
       <li> "No-graph" timelines (using the "ng" query parameter) now show
            branch colors and bare check-in circles on the left.  The check-in
            circles appear, but no lines connecting them.
            ([/timeline?ng|example]).
       </ol>
  <li> The [/help/timeline|timeline command] is enhanced with the new
       "<tt>-u|--for-user</tt>" option.
</ol>

<h2 id='v2_27'>Changes for version 2.27 (2025-09-30)</h2><ol>
  <li> Close a potential Denial-of-Service attack against any public-facing Fossil
       server involving exponential behavior in Fossil's regexp implementation.