Fossil SCM

Land the timeline-enhance-2025 branch. The "Simple" timeline view. Improved access to tarballs and ZIPs. See items 3 and 4 in the merged [/doc/7b7cbd325350/www/changes.wiki|www/changes.wiki] for more detail.

drh 2025-10-20 17:52 trunk merge
Commit 6ce705b8dcfeca4f83c95a0a0a566e8912449d32a64abd2cec0ee83ba1c51a2f
+22 -30
--- src/branch.c
+++ src/branch.c
@@ -843,10 +843,11 @@
843843
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
844844
style_set_current_feature("branch");
845845
style_header("Branches");
846846
style_adunit_config(ADUNIT_RIGHT_OK);
847847
style_submenu_checkbox("colors", "Use Branch Colors", 0, 0);
848
+
848849
login_anonymous_available();
849850
850851
brlist_create_temp_table();
851852
db_prepare(&q, "SELECT * FROM tmp_brlist ORDER BY mtime DESC");
852853
rNow = db_double(0.0, "SELECT julianday('now')");
@@ -1025,34 +1026,30 @@
10251026
Stmt *pQuery, /* Current row of the timeline query */
10261027
int tmFlags, /* Flags to www_print_timeline() */
10271028
const char *zThisUser, /* Suppress links to this user */
10281029
const char *zThisTag /* Suppress links to this tag */
10291030
){
1030
- int rid = db_column_int(pQuery, 0);
1031
- Stmt q;
1031
+ int rid;
1032
+ int tmFlagsNew;
1033
+ char *zBrName;
1034
+
1035
+ if( (tmFlags & TIMELINE_INLINE)!=0 ){
1036
+ tmFlagsNew = (tmFlags & ~TIMELINE_VIEWS) | TIMELINE_MODERN;
1037
+ cgi_printf("(");
1038
+ }else{
1039
+ tmFlagsNew = tmFlags;
1040
+ }
1041
+ timeline_extra(pQuery,tmFlagsNew,zThisUser,zThisTag);
1042
+
10321043
if( !g.perm.Hyperlink ) return;
1033
- db_prepare(&q,
1034
- "SELECT substr(tagname,5) FROM tagxref, tag"
1035
- " WHERE tagxref.rid=%d"
1036
- " AND tagxref.tagid=tag.tagid"
1037
- " AND tagxref.tagtype>0"
1038
- " AND tag.tagname GLOB 'sym-*'",
1039
- rid
1040
- );
1041
- while( db_step(&q)==SQLITE_ROW ){
1042
- const char *zTagName = db_column_text(&q, 0);
1043
-#define OLD_STYLE 1
1044
-#if OLD_STYLE
1045
- @ %z(href("%R/timeline?r=%T",zTagName))[timeline]</a>
1046
-#else
1047
- char *zBrName = branch_of_rid(rid);
1048
- @ <strong>%h(zBrName)</strong><br>\
1049
- @ %z(href("%R/timeline?r=%T",zTagName))<button>timeline</button></a>
1050
- fossil_free(zBrName);
1051
-#endif
1052
- }
1053
- db_finalize(&q);
1044
+ rid = db_column_int(pQuery,0);
1045
+ zBrName = branch_of_rid(rid);
1046
+ @ branch:&nbsp;<span class='timelineHash'>\
1047
+ @ %z(href("%R/timeline?r=%T",zBrName))%h(zBrName)</a></span>
1048
+ if( (tmFlags & TIMELINE_INLINE)!=0 ){
1049
+ cgi_printf(")");
1050
+ }
10541051
}
10551052
10561053
/*
10571054
** WEBPAGE: brtimeline
10581055
**
@@ -1070,21 +1067,19 @@
10701067
int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
10711068
int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
10721069
10731070
login_check_credentials();
10741071
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
1072
+ if( robot_restrict("timelineX") ) return;
10751073
10761074
style_set_current_feature("branch");
10771075
style_header("Branches");
10781076
style_submenu_element("Branch List", "brlist");
10791077
login_anonymous_available();
1080
-#if OLD_STYLE
10811078
timeline_ss_submenu();
1082
-#endif
10831079
cgi_check_for_malice();
1084
- @ <h2>First check-in for every branch, starting with the most recent
1085
- @ and going backwards in time.</h2>
1080
+ @ <h2>The initial check-in for each branch:</h2>
10861081
blob_append(&sql, timeline_query_for_www(), -1);
10871082
blob_append_sql(&sql,
10881083
"AND blob.rid IN (SELECT rid FROM tagxref"
10891084
" WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
10901085
if( fNoHidden || fOnlyHidden ){
@@ -1097,13 +1092,10 @@
10971092
db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
10981093
blob_reset(&sql);
10991094
/* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
11001095
** many descenders to (off-screen) parents. */
11011096
tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
1102
-#if !OLD_STYLE
1103
- tmFlags |= TIMELINE_COLUMNAR;
1104
-#endif
11051097
if( PB("ubg")!=0 ){
11061098
tmFlags |= TIMELINE_UCOLOR;
11071099
}else{
11081100
tmFlags |= TIMELINE_BRCOLOR;
11091101
}
11101102
--- src/branch.c
+++ src/branch.c
@@ -843,10 +843,11 @@
843 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
844 style_set_current_feature("branch");
845 style_header("Branches");
846 style_adunit_config(ADUNIT_RIGHT_OK);
847 style_submenu_checkbox("colors", "Use Branch Colors", 0, 0);
 
848 login_anonymous_available();
849
850 brlist_create_temp_table();
851 db_prepare(&q, "SELECT * FROM tmp_brlist ORDER BY mtime DESC");
852 rNow = db_double(0.0, "SELECT julianday('now')");
@@ -1025,34 +1026,30 @@
1025 Stmt *pQuery, /* Current row of the timeline query */
1026 int tmFlags, /* Flags to www_print_timeline() */
1027 const char *zThisUser, /* Suppress links to this user */
1028 const char *zThisTag /* Suppress links to this tag */
1029 ){
1030 int rid = db_column_int(pQuery, 0);
1031 Stmt q;
 
 
 
 
 
 
 
 
 
 
1032 if( !g.perm.Hyperlink ) return;
1033 db_prepare(&q,
1034 "SELECT substr(tagname,5) FROM tagxref, tag"
1035 " WHERE tagxref.rid=%d"
1036 " AND tagxref.tagid=tag.tagid"
1037 " AND tagxref.tagtype>0"
1038 " AND tag.tagname GLOB 'sym-*'",
1039 rid
1040 );
1041 while( db_step(&q)==SQLITE_ROW ){
1042 const char *zTagName = db_column_text(&q, 0);
1043 #define OLD_STYLE 1
1044 #if OLD_STYLE
1045 @ %z(href("%R/timeline?r=%T",zTagName))[timeline]</a>
1046 #else
1047 char *zBrName = branch_of_rid(rid);
1048 @ <strong>%h(zBrName)</strong><br>\
1049 @ %z(href("%R/timeline?r=%T",zTagName))<button>timeline</button></a>
1050 fossil_free(zBrName);
1051 #endif
1052 }
1053 db_finalize(&q);
1054 }
1055
1056 /*
1057 ** WEBPAGE: brtimeline
1058 **
@@ -1070,21 +1067,19 @@
1070 int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
1071 int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
1072
1073 login_check_credentials();
1074 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
 
1075
1076 style_set_current_feature("branch");
1077 style_header("Branches");
1078 style_submenu_element("Branch List", "brlist");
1079 login_anonymous_available();
1080 #if OLD_STYLE
1081 timeline_ss_submenu();
1082 #endif
1083 cgi_check_for_malice();
1084 @ <h2>First check-in for every branch, starting with the most recent
1085 @ and going backwards in time.</h2>
1086 blob_append(&sql, timeline_query_for_www(), -1);
1087 blob_append_sql(&sql,
1088 "AND blob.rid IN (SELECT rid FROM tagxref"
1089 " WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
1090 if( fNoHidden || fOnlyHidden ){
@@ -1097,13 +1092,10 @@
1097 db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
1098 blob_reset(&sql);
1099 /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
1100 ** many descenders to (off-screen) parents. */
1101 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
1102 #if !OLD_STYLE
1103 tmFlags |= TIMELINE_COLUMNAR;
1104 #endif
1105 if( PB("ubg")!=0 ){
1106 tmFlags |= TIMELINE_UCOLOR;
1107 }else{
1108 tmFlags |= TIMELINE_BRCOLOR;
1109 }
1110
--- src/branch.c
+++ src/branch.c
@@ -843,10 +843,11 @@
843 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
844 style_set_current_feature("branch");
845 style_header("Branches");
846 style_adunit_config(ADUNIT_RIGHT_OK);
847 style_submenu_checkbox("colors", "Use Branch Colors", 0, 0);
848
849 login_anonymous_available();
850
851 brlist_create_temp_table();
852 db_prepare(&q, "SELECT * FROM tmp_brlist ORDER BY mtime DESC");
853 rNow = db_double(0.0, "SELECT julianday('now')");
@@ -1025,34 +1026,30 @@
1026 Stmt *pQuery, /* Current row of the timeline query */
1027 int tmFlags, /* Flags to www_print_timeline() */
1028 const char *zThisUser, /* Suppress links to this user */
1029 const char *zThisTag /* Suppress links to this tag */
1030 ){
1031 int rid;
1032 int tmFlagsNew;
1033 char *zBrName;
1034
1035 if( (tmFlags & TIMELINE_INLINE)!=0 ){
1036 tmFlagsNew = (tmFlags & ~TIMELINE_VIEWS) | TIMELINE_MODERN;
1037 cgi_printf("(");
1038 }else{
1039 tmFlagsNew = tmFlags;
1040 }
1041 timeline_extra(pQuery,tmFlagsNew,zThisUser,zThisTag);
1042
1043 if( !g.perm.Hyperlink ) return;
1044 rid = db_column_int(pQuery,0);
1045 zBrName = branch_of_rid(rid);
1046 @ branch:&nbsp;<span class='timelineHash'>\
1047 @ %z(href("%R/timeline?r=%T",zBrName))%h(zBrName)</a></span>
1048 if( (tmFlags & TIMELINE_INLINE)!=0 ){
1049 cgi_printf(")");
1050 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051 }
1052
1053 /*
1054 ** WEBPAGE: brtimeline
1055 **
@@ -1070,21 +1067,19 @@
1067 int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
1068 int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
1069
1070 login_check_credentials();
1071 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
1072 if( robot_restrict("timelineX") ) return;
1073
1074 style_set_current_feature("branch");
1075 style_header("Branches");
1076 style_submenu_element("Branch List", "brlist");
1077 login_anonymous_available();
 
1078 timeline_ss_submenu();
 
1079 cgi_check_for_malice();
1080 @ <h2>The initial check-in for each branch:</h2>
 
1081 blob_append(&sql, timeline_query_for_www(), -1);
1082 blob_append_sql(&sql,
1083 "AND blob.rid IN (SELECT rid FROM tagxref"
1084 " WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
1085 if( fNoHidden || fOnlyHidden ){
@@ -1097,13 +1092,10 @@
1092 db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
1093 blob_reset(&sql);
1094 /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
1095 ** many descenders to (off-screen) parents. */
1096 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
 
 
 
1097 if( PB("ubg")!=0 ){
1098 tmFlags |= TIMELINE_UCOLOR;
1099 }else{
1100 tmFlags |= TIMELINE_BRCOLOR;
1101 }
1102
--- src/browse.c
+++ src/browse.c
@@ -229,10 +229,13 @@
229229
}else{
230230
zMatch = "";
231231
}
232232
style_header("%s", zHeader);
233233
fossil_free(zHeader);
234
+ if( rid && zD==0 && zMatch[0]==0 && g.perm.Zip ){
235
+ style_submenu_element("Download","%R/rchvdwnld/%!S",zUuid);
236
+ }
234237
style_adunit_config(ADUNIT_RIGHT_OK);
235238
sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
236239
pathelementFunc, 0, 0);
237240
url_initialize(&sURI, "dir");
238241
cgi_check_for_malice();
@@ -814,10 +817,13 @@
814817
style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
815818
}
816819
}
817820
style_submenu_element("Flat-View", "%s",
818821
url_render(&sURI, "type", "flat", 0, 0));
822
+ if( rid && zD==0 && zRE==0 && !showDirOnly && g.perm.Zip ){
823
+ style_submenu_element("Download","%R/rchvdwnld/%!S", zUuid);
824
+ }
819825
820826
/* Compute the file hierarchy.
821827
*/
822828
if( zCI ){
823829
Stmt q;
824830
--- src/browse.c
+++ src/browse.c
@@ -229,10 +229,13 @@
229 }else{
230 zMatch = "";
231 }
232 style_header("%s", zHeader);
233 fossil_free(zHeader);
 
 
 
234 style_adunit_config(ADUNIT_RIGHT_OK);
235 sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
236 pathelementFunc, 0, 0);
237 url_initialize(&sURI, "dir");
238 cgi_check_for_malice();
@@ -814,10 +817,13 @@
814 style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
815 }
816 }
817 style_submenu_element("Flat-View", "%s",
818 url_render(&sURI, "type", "flat", 0, 0));
 
 
 
819
820 /* Compute the file hierarchy.
821 */
822 if( zCI ){
823 Stmt q;
824
--- src/browse.c
+++ src/browse.c
@@ -229,10 +229,13 @@
229 }else{
230 zMatch = "";
231 }
232 style_header("%s", zHeader);
233 fossil_free(zHeader);
234 if( rid && zD==0 && zMatch[0]==0 && g.perm.Zip ){
235 style_submenu_element("Download","%R/rchvdwnld/%!S",zUuid);
236 }
237 style_adunit_config(ADUNIT_RIGHT_OK);
238 sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
239 pathelementFunc, 0, 0);
240 url_initialize(&sURI, "dir");
241 cgi_check_for_malice();
@@ -814,10 +817,13 @@
817 style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
818 }
819 }
820 style_submenu_element("Flat-View", "%s",
821 url_render(&sURI, "type", "flat", 0, 0));
822 if( rid && zD==0 && zRE==0 && !showDirOnly && g.perm.Zip ){
823 style_submenu_element("Download","%R/rchvdwnld/%!S", zUuid);
824 }
825
826 /* Compute the file hierarchy.
827 */
828 if( zCI ){
829 Stmt q;
830
+7 -27
--- src/clone.c
+++ src/clone.c
@@ -405,40 +405,18 @@
405405
db_protect_pop();
406406
}
407407
}
408408
409409
/*
410
-** WEBPAGE: download
410
+** WEBPAGE: howtoclone
411411
**
412
-** Provide a simple page that enables newbies to download the latest tarball or
413
-** ZIP archive, and provides instructions on how to clone.
412
+** Provide instructions on how to clone this repository.
414413
*/
415
-void download_page(void){
414
+void howtoclone_page(void){
416415
login_check_credentials();
417416
cgi_check_for_malice();
418
- style_header("Download Page");
419
- if( !g.perm.Zip ){
420
- @ <p>Bummer. You do not have permission to download.
421
- if( g.zLogin==0 || g.zLogin[0]==0 ){
422
- @ Maybe it would work better if you
423
- @ %z(href("%R/login"))logged in</a>.
424
- }else{
425
- @ Contact the site administrator and ask them to give
426
- @ you "Download Zip" privileges.
427
- }
428
- }else{
429
- const char *zDLTag = db_get("download-tag","trunk");
430
- const char *zNm = db_get("short-project-name","download");
431
- char *zUrl = href("%R/zip/%t/%t.zip", zDLTag, zNm);
432
- @ <p>ZIP Archive: %z(zUrl)%h(zNm).zip</a>
433
- zUrl = href("%R/tarball/%t/%t.tar.gz", zDLTag, zNm);
434
- @ <p>Tarball: %z(zUrl)%h(zNm).tar.gz</a>
435
- if( g.zLogin!=0 ){
436
- zUrl = href("%R/sqlar/%t/%t.sqlar", zDLTag, zNm);
437
- @ <p>SQLite Archive: %z(zUrl)%h(zNm).sqlar</a>
438
- }
439
- }
417
+ style_header("How To Clone This Repository");
440418
if( !g.perm.Clone ){
441419
@ <p>You are not authorized to clone this repository.
442420
if( g.zLogin==0 || g.zLogin[0]==0 ){
443421
@ Maybe you would be able to clone if you
444422
@ %z(href("%R/login"))logged in</a>.
@@ -446,12 +424,14 @@
446424
@ Contact the site administrator and ask them to give
447425
@ you "Clone" privileges in order to clone.
448426
}
449427
}else{
450428
const char *zNm = db_get("short-project-name","clone");
451
- @ <p>Clone the repository using this command:
429
+ @ <p>Clone this repository by running a command like the following:
452430
@ <blockquote><pre>
453431
@ fossil clone %s(g.zBaseURL) %h(zNm).fossil
454432
@ </pre></blockquote>
433
+ @ <p>Do a web search for "fossil clone" or similar to find additional
434
+ @ information about using a cloned Fossil repository.
455435
}
456436
style_finish_page();
457437
}
458438
--- src/clone.c
+++ src/clone.c
@@ -405,40 +405,18 @@
405 db_protect_pop();
406 }
407 }
408
409 /*
410 ** WEBPAGE: download
411 **
412 ** Provide a simple page that enables newbies to download the latest tarball or
413 ** ZIP archive, and provides instructions on how to clone.
414 */
415 void download_page(void){
416 login_check_credentials();
417 cgi_check_for_malice();
418 style_header("Download Page");
419 if( !g.perm.Zip ){
420 @ <p>Bummer. You do not have permission to download.
421 if( g.zLogin==0 || g.zLogin[0]==0 ){
422 @ Maybe it would work better if you
423 @ %z(href("%R/login"))logged in</a>.
424 }else{
425 @ Contact the site administrator and ask them to give
426 @ you "Download Zip" privileges.
427 }
428 }else{
429 const char *zDLTag = db_get("download-tag","trunk");
430 const char *zNm = db_get("short-project-name","download");
431 char *zUrl = href("%R/zip/%t/%t.zip", zDLTag, zNm);
432 @ <p>ZIP Archive: %z(zUrl)%h(zNm).zip</a>
433 zUrl = href("%R/tarball/%t/%t.tar.gz", zDLTag, zNm);
434 @ <p>Tarball: %z(zUrl)%h(zNm).tar.gz</a>
435 if( g.zLogin!=0 ){
436 zUrl = href("%R/sqlar/%t/%t.sqlar", zDLTag, zNm);
437 @ <p>SQLite Archive: %z(zUrl)%h(zNm).sqlar</a>
438 }
439 }
440 if( !g.perm.Clone ){
441 @ <p>You are not authorized to clone this repository.
442 if( g.zLogin==0 || g.zLogin[0]==0 ){
443 @ Maybe you would be able to clone if you
444 @ %z(href("%R/login"))logged in</a>.
@@ -446,12 +424,14 @@
446 @ Contact the site administrator and ask them to give
447 @ you "Clone" privileges in order to clone.
448 }
449 }else{
450 const char *zNm = db_get("short-project-name","clone");
451 @ <p>Clone the repository using this command:
452 @ <blockquote><pre>
453 @ fossil clone %s(g.zBaseURL) %h(zNm).fossil
454 @ </pre></blockquote>
 
 
455 }
456 style_finish_page();
457 }
458
--- src/clone.c
+++ src/clone.c
@@ -405,40 +405,18 @@
405 db_protect_pop();
406 }
407 }
408
409 /*
410 ** WEBPAGE: howtoclone
411 **
412 ** Provide instructions on how to clone this repository.
 
413 */
414 void howtoclone_page(void){
415 login_check_credentials();
416 cgi_check_for_malice();
417 style_header("How To Clone This Repository");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418 if( !g.perm.Clone ){
419 @ <p>You are not authorized to clone this repository.
420 if( g.zLogin==0 || g.zLogin[0]==0 ){
421 @ Maybe you would be able to clone if you
422 @ %z(href("%R/login"))logged in</a>.
@@ -446,12 +424,14 @@
424 @ Contact the site administrator and ask them to give
425 @ you "Clone" privileges in order to clone.
426 }
427 }else{
428 const char *zNm = db_get("short-project-name","clone");
429 @ <p>Clone this repository by running a command like the following:
430 @ <blockquote><pre>
431 @ fossil clone %s(g.zBaseURL) %h(zNm).fossil
432 @ </pre></blockquote>
433 @ <p>Do a web search for "fossil clone" or similar to find additional
434 @ information about using a cloned Fossil repository.
435 }
436 style_finish_page();
437 }
438
+2 -2
--- src/db.c
+++ src/db.c
@@ -1228,12 +1228,12 @@
12281228
}
12291229
12301230
/*
12311231
** Execute a query. Return the first column of the first row
12321232
** of the result set as a string. Space to hold the string is
1233
-** obtained from malloc(). If the result set is empty, return
1234
-** zDefault instead.
1233
+** obtained from fossil_strdup() and should be freed using fossil_free().
1234
+** If the result set is empty, return a copy of zDefault instead.
12351235
*/
12361236
char *db_text(const char *zDefault, const char *zSql, ...){
12371237
va_list ap;
12381238
Stmt s;
12391239
char *z;
12401240
--- src/db.c
+++ src/db.c
@@ -1228,12 +1228,12 @@
1228 }
1229
1230 /*
1231 ** Execute a query. Return the first column of the first row
1232 ** of the result set as a string. Space to hold the string is
1233 ** obtained from malloc(). If the result set is empty, return
1234 ** zDefault instead.
1235 */
1236 char *db_text(const char *zDefault, const char *zSql, ...){
1237 va_list ap;
1238 Stmt s;
1239 char *z;
1240
--- src/db.c
+++ src/db.c
@@ -1228,12 +1228,12 @@
1228 }
1229
1230 /*
1231 ** Execute a query. Return the first column of the first row
1232 ** of the result set as a string. Space to hold the string is
1233 ** obtained from fossil_strdup() and should be freed using fossil_free().
1234 ** If the result set is empty, return a copy of zDefault instead.
1235 */
1236 char *db_text(const char *zDefault, const char *zSql, ...){
1237 va_list ap;
1238 Stmt s;
1239 char *z;
1240
--- src/default.css
+++ src/default.css
@@ -57,10 +57,13 @@
5757
border-width: 0;
5858
}
5959
span.timelineLeaf {
6060
font-weight: bold;
6161
}
62
+span.timelineHash {
63
+ font-weight: bold;
64
+}
6265
span.timelineHistDsp {
6366
font-weight: bold;
6467
}
6568
td.timelineTime {
6669
vertical-align: top;
6770
--- src/default.css
+++ src/default.css
@@ -57,10 +57,13 @@
57 border-width: 0;
58 }
59 span.timelineLeaf {
60 font-weight: bold;
61 }
 
 
 
62 span.timelineHistDsp {
63 font-weight: bold;
64 }
65 td.timelineTime {
66 vertical-align: top;
67
--- src/default.css
+++ src/default.css
@@ -57,10 +57,13 @@
57 border-width: 0;
58 }
59 span.timelineLeaf {
60 font-weight: bold;
61 }
62 span.timelineHash {
63 font-weight: bold;
64 }
65 span.timelineHistDsp {
66 font-weight: bold;
67 }
68 td.timelineTime {
69 vertical-align: top;
70
+14 -7
--- src/finfo.c
+++ src/finfo.c
@@ -394,10 +394,12 @@
394394
tmFlags = timeline_ss_submenu();
395395
if( tmFlags & TIMELINE_COLUMNAR ){
396396
zStyle = "Columnar";
397397
}else if( tmFlags & TIMELINE_COMPACT ){
398398
zStyle = "Compact";
399
+ }else if( tmFlags & TIMELINE_SIMPLE ){
400
+ zStyle = "Simple";
399401
}else if( tmFlags & TIMELINE_VERBOSE ){
400402
zStyle = "Verbose";
401403
}else if( tmFlags & TIMELINE_CLASSIC ){
402404
zStyle = "Classic";
403405
}else{
@@ -731,14 +733,14 @@
731733
}
732734
if( tmFlags & TIMELINE_COMPACT ){
733735
cgi_printf("<span class='clutter' id='detail-%d'>",frid);
734736
}
735737
cgi_printf("<span class='timeline%sDetail'>", zStyle);
736
- if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ) cgi_printf("(");
738
+ if( tmFlags & TIMELINE_INLINE ) cgi_printf("(");
737739
if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
738740
@ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
739
- @ [%S(zUuid)]</a>
741
+ @ %S(zUuid)</a>
740742
if( fShowId ){
741743
int srcId = delta_source_rid(frid);
742744
if( srcId>0 ){
743745
@ id:&nbsp;%z(href("%R/deltachain/%d",frid))\
744746
@ %d(frid)&larr;%d(srcId)</a>
@@ -745,20 +747,25 @@
745747
}else{
746748
@ id:&nbsp;%z(href("%R/deltachain/%d",frid))%d(frid)</a>
747749
}
748750
}
749751
}
752
+ if( tmFlags & TIMELINE_SIMPLE ){
753
+ @ <span class='timelineEllipsis' data-id='%d(frid)' \
754
+ @ id='ellipsis-%d(frid)'>...</span>
755
+ @ <span class='clutter' id='detail-%d(frid)'>
756
+ }
750757
@ check-in:&nbsp;\
751758
hyperlink_to_version(zCkin);
752759
if( fShowId ){
753760
@ (%d(fmid))
754761
}
755762
@ user:&nbsp;\
756763
hyperlink_to_user(zUser, zDate, ",");
757764
@ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
758
- if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
759
- @ size:&nbsp;%d(szFile))
765
+ if( tmFlags & TIMELINE_INLINE ){
766
+ @ size:&nbsp;%d(szFile)
760767
}else{
761768
@ size:&nbsp;%d(szFile)
762769
}
763770
if( g.perm.Hyperlink && zUuid ){
764771
const char *z = zFName;
@@ -793,14 +800,14 @@
793800
zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
794801
@ %z(zAncLink)[ancestry]</a>
795802
}
796803
tag_private_status(frid);
797804
/* End timelineDetail */
798
- if( tmFlags & TIMELINE_COMPACT ){
799
- @ </span></span>
805
+ if( tmFlags & (TIMELINE_COMPACT|TIMELINE_SIMPLE) ){
806
+ @ </span>)</span>
800807
}else{
801
- @ </span>
808
+ @ )</span>
802809
}
803810
@ </td></tr>
804811
}
805812
db_finalize(&q);
806813
db_finalize(&qparent);
807814
--- src/finfo.c
+++ src/finfo.c
@@ -394,10 +394,12 @@
394 tmFlags = timeline_ss_submenu();
395 if( tmFlags & TIMELINE_COLUMNAR ){
396 zStyle = "Columnar";
397 }else if( tmFlags & TIMELINE_COMPACT ){
398 zStyle = "Compact";
 
 
399 }else if( tmFlags & TIMELINE_VERBOSE ){
400 zStyle = "Verbose";
401 }else if( tmFlags & TIMELINE_CLASSIC ){
402 zStyle = "Classic";
403 }else{
@@ -731,14 +733,14 @@
731 }
732 if( tmFlags & TIMELINE_COMPACT ){
733 cgi_printf("<span class='clutter' id='detail-%d'>",frid);
734 }
735 cgi_printf("<span class='timeline%sDetail'>", zStyle);
736 if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ) cgi_printf("(");
737 if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
738 @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
739 @ [%S(zUuid)]</a>
740 if( fShowId ){
741 int srcId = delta_source_rid(frid);
742 if( srcId>0 ){
743 @ id:&nbsp;%z(href("%R/deltachain/%d",frid))\
744 @ %d(frid)&larr;%d(srcId)</a>
@@ -745,20 +747,25 @@
745 }else{
746 @ id:&nbsp;%z(href("%R/deltachain/%d",frid))%d(frid)</a>
747 }
748 }
749 }
 
 
 
 
 
750 @ check-in:&nbsp;\
751 hyperlink_to_version(zCkin);
752 if( fShowId ){
753 @ (%d(fmid))
754 }
755 @ user:&nbsp;\
756 hyperlink_to_user(zUser, zDate, ",");
757 @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
758 if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
759 @ size:&nbsp;%d(szFile))
760 }else{
761 @ size:&nbsp;%d(szFile)
762 }
763 if( g.perm.Hyperlink && zUuid ){
764 const char *z = zFName;
@@ -793,14 +800,14 @@
793 zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
794 @ %z(zAncLink)[ancestry]</a>
795 }
796 tag_private_status(frid);
797 /* End timelineDetail */
798 if( tmFlags & TIMELINE_COMPACT ){
799 @ </span></span>
800 }else{
801 @ </span>
802 }
803 @ </td></tr>
804 }
805 db_finalize(&q);
806 db_finalize(&qparent);
807
--- src/finfo.c
+++ src/finfo.c
@@ -394,10 +394,12 @@
394 tmFlags = timeline_ss_submenu();
395 if( tmFlags & TIMELINE_COLUMNAR ){
396 zStyle = "Columnar";
397 }else if( tmFlags & TIMELINE_COMPACT ){
398 zStyle = "Compact";
399 }else if( tmFlags & TIMELINE_SIMPLE ){
400 zStyle = "Simple";
401 }else if( tmFlags & TIMELINE_VERBOSE ){
402 zStyle = "Verbose";
403 }else if( tmFlags & TIMELINE_CLASSIC ){
404 zStyle = "Classic";
405 }else{
@@ -731,14 +733,14 @@
733 }
734 if( tmFlags & TIMELINE_COMPACT ){
735 cgi_printf("<span class='clutter' id='detail-%d'>",frid);
736 }
737 cgi_printf("<span class='timeline%sDetail'>", zStyle);
738 if( tmFlags & TIMELINE_INLINE ) cgi_printf("(");
739 if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
740 @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
741 @ %S(zUuid)</a>
742 if( fShowId ){
743 int srcId = delta_source_rid(frid);
744 if( srcId>0 ){
745 @ id:&nbsp;%z(href("%R/deltachain/%d",frid))\
746 @ %d(frid)&larr;%d(srcId)</a>
@@ -745,20 +747,25 @@
747 }else{
748 @ id:&nbsp;%z(href("%R/deltachain/%d",frid))%d(frid)</a>
749 }
750 }
751 }
752 if( tmFlags & TIMELINE_SIMPLE ){
753 @ <span class='timelineEllipsis' data-id='%d(frid)' \
754 @ id='ellipsis-%d(frid)'>...</span>
755 @ <span class='clutter' id='detail-%d(frid)'>
756 }
757 @ check-in:&nbsp;\
758 hyperlink_to_version(zCkin);
759 if( fShowId ){
760 @ (%d(fmid))
761 }
762 @ user:&nbsp;\
763 hyperlink_to_user(zUser, zDate, ",");
764 @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
765 if( tmFlags & TIMELINE_INLINE ){
766 @ size:&nbsp;%d(szFile)
767 }else{
768 @ size:&nbsp;%d(szFile)
769 }
770 if( g.perm.Hyperlink && zUuid ){
771 const char *z = zFName;
@@ -793,14 +800,14 @@
800 zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
801 @ %z(zAncLink)[ancestry]</a>
802 }
803 tag_private_status(frid);
804 /* End timelineDetail */
805 if( tmFlags & (TIMELINE_COMPACT|TIMELINE_SIMPLE) ){
806 @ </span>)</span>
807 }else{
808 @ )</span>
809 }
810 @ </td></tr>
811 }
812 db_finalize(&q);
813 db_finalize(&qparent);
814
+4 -4
--- src/graph.js
+++ src/graph.js
@@ -726,15 +726,15 @@
726726
function toggleDetail(){
727727
var id = parseInt(this.getAttribute('data-id'))
728728
var x = document.getElementById("detail-"+id);
729729
if( x.style.display=="inline" ){
730730
x.style.display="none";
731
- changeDisplayById("ellipsis-"+id,"inline");
731
+ document.getElementById("ellipsis-"+id).textContent = "...";
732732
changeDisplayById("links-"+id,"none");
733733
}else{
734734
x.style.display="inline";
735
- changeDisplayById("ellipsis-"+id,"none");
735
+ document.getElementById("ellipsis-"+id).textContent = "←";
736736
changeDisplayById("links-"+id,"inline");
737737
}
738738
checkHeight();
739739
}
740740
function scrollToSelected(){
@@ -764,12 +764,12 @@
764764
}
765765
if( tx.scrollToSelect ){
766766
scrollToSelected();
767767
}
768768
769
- /* Set the onclick= attributes for elements of the "Compact" display
770
- ** mode so that clicking turns the details on and off.
769
+ /* Set the onclick= attributes for elements of the "Compact" and
770
+ ** "Simple" views so that clicking turns the details on and off.
771771
*/
772772
var lx = topObj.getElementsByClassName('timelineEllipsis');
773773
var i;
774774
for(i=0; i<lx.length; i++){
775775
if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
776776
--- src/graph.js
+++ src/graph.js
@@ -726,15 +726,15 @@
726 function toggleDetail(){
727 var id = parseInt(this.getAttribute('data-id'))
728 var x = document.getElementById("detail-"+id);
729 if( x.style.display=="inline" ){
730 x.style.display="none";
731 changeDisplayById("ellipsis-"+id,"inline");
732 changeDisplayById("links-"+id,"none");
733 }else{
734 x.style.display="inline";
735 changeDisplayById("ellipsis-"+id,"none");
736 changeDisplayById("links-"+id,"inline");
737 }
738 checkHeight();
739 }
740 function scrollToSelected(){
@@ -764,12 +764,12 @@
764 }
765 if( tx.scrollToSelect ){
766 scrollToSelected();
767 }
768
769 /* Set the onclick= attributes for elements of the "Compact" display
770 ** mode so that clicking turns the details on and off.
771 */
772 var lx = topObj.getElementsByClassName('timelineEllipsis');
773 var i;
774 for(i=0; i<lx.length; i++){
775 if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
776
--- src/graph.js
+++ src/graph.js
@@ -726,15 +726,15 @@
726 function toggleDetail(){
727 var id = parseInt(this.getAttribute('data-id'))
728 var x = document.getElementById("detail-"+id);
729 if( x.style.display=="inline" ){
730 x.style.display="none";
731 document.getElementById("ellipsis-"+id).textContent = "...";
732 changeDisplayById("links-"+id,"none");
733 }else{
734 x.style.display="inline";
735 document.getElementById("ellipsis-"+id).textContent = "←";
736 changeDisplayById("links-"+id,"inline");
737 }
738 checkHeight();
739 }
740 function scrollToSelected(){
@@ -764,12 +764,12 @@
764 }
765 if( tx.scrollToSelect ){
766 scrollToSelected();
767 }
768
769 /* Set the onclick= attributes for elements of the "Compact" and
770 ** "Simple" views so that clicking turns the details on and off.
771 */
772 var lx = topObj.getElementsByClassName('timelineEllipsis');
773 var i;
774 for(i=0; i<lx.length; i++){
775 if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
776
+13 -24
--- src/info.c
+++ src/info.c
@@ -993,34 +993,23 @@
993993
@ <tr><th>Comment:</th><td class="infoComment">\
994994
@ %!W(zEComment?zEComment:zComment)</td></tr>
995995
996996
/* The Download: line */
997997
if( g.perm.Zip ){
998
- char *zPJ = db_get("short-project-name", 0);
999
- char *zUrl;
1000
- Blob projName;
1001
- int jj;
1002
- if( zPJ==0 ) zPJ = db_get("project-name", "unnamed");
1003
- blob_zero(&projName);
1004
- blob_append(&projName, zPJ, -1);
1005
- blob_trim(&projName);
1006
- zPJ = blob_str(&projName);
1007
- for(jj=0; zPJ[jj]; jj++){
1008
- if( (zPJ[jj]>0 && zPJ[jj]<' ') || strchr("\"*/:<>?\\|", zPJ[jj]) ){
1009
- zPJ[jj] = '_';
1010
- }
1011
- }
1012
- zUrl = mprintf("%R/tarball/%S/%t-%S.tar.gz", zUuid, zPJ, zUuid);
1013998
@ <tr><th>Downloads:</th><td>
1014
- @ %z(href("%s",zUrl))Tarball</a>
1015
- @ | %z(href("%R/zip/%S/%t-%S.zip",zUuid, zPJ,zUuid))ZIP archive</a>
1016
- if( g.zLogin!=0 ){
1017
- @ | %z(href("%R/sqlar/%S/%t-%S.sqlar",zUuid,zPJ,zUuid))\
1018
- @ SQL archive</a></td></tr>
1019
- }
1020
- fossil_free(zUrl);
1021
- blob_reset(&projName);
999
+ if( robot_would_be_restricted("download") ){
1000
+ @ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
1001
+ }else{
1002
+ char *zBase = archive_base_name(rid);
1003
+ @ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a>
1004
+ @ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a>
1005
+ if( g.zLogin!=0 ){
1006
+ @ | %z(href("%R/sqlar/%s.sqlar",zBase))\
1007
+ @ SQL archive</a></td></tr>
1008
+ }
1009
+ fossil_free(zBase);
1010
+ }
10221011
}
10231012
10241013
@ <tr><th>Timelines:</th><td>
10251014
@ %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a>
10261015
if( zParent ){
@@ -1950,11 +1939,11 @@
19501939
int dflt;
19511940
int res;
19521941
int isBot;
19531942
static char zDflt[2]
19541943
/*static b/c cookie_link_parameter() does not copy it!*/;
1955
- if( client_might_be_a_robot() && robot_restrict_has_tag("diff") ){
1944
+ if( robot_would_be_restricted("diff") ){
19561945
dflt = 0;
19571946
isBot = 1;
19581947
}else{
19591948
dflt = db_get_int("preferred-diff-type",-99);
19601949
if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2;
19611950
--- src/info.c
+++ src/info.c
@@ -993,34 +993,23 @@
993 @ <tr><th>Comment:</th><td class="infoComment">\
994 @ %!W(zEComment?zEComment:zComment)</td></tr>
995
996 /* The Download: line */
997 if( g.perm.Zip ){
998 char *zPJ = db_get("short-project-name", 0);
999 char *zUrl;
1000 Blob projName;
1001 int jj;
1002 if( zPJ==0 ) zPJ = db_get("project-name", "unnamed");
1003 blob_zero(&projName);
1004 blob_append(&projName, zPJ, -1);
1005 blob_trim(&projName);
1006 zPJ = blob_str(&projName);
1007 for(jj=0; zPJ[jj]; jj++){
1008 if( (zPJ[jj]>0 && zPJ[jj]<' ') || strchr("\"*/:<>?\\|", zPJ[jj]) ){
1009 zPJ[jj] = '_';
1010 }
1011 }
1012 zUrl = mprintf("%R/tarball/%S/%t-%S.tar.gz", zUuid, zPJ, zUuid);
1013 @ <tr><th>Downloads:</th><td>
1014 @ %z(href("%s",zUrl))Tarball</a>
1015 @ | %z(href("%R/zip/%S/%t-%S.zip",zUuid, zPJ,zUuid))ZIP archive</a>
1016 if( g.zLogin!=0 ){
1017 @ | %z(href("%R/sqlar/%S/%t-%S.sqlar",zUuid,zPJ,zUuid))\
1018 @ SQL archive</a></td></tr>
1019 }
1020 fossil_free(zUrl);
1021 blob_reset(&projName);
 
 
 
 
1022 }
1023
1024 @ <tr><th>Timelines:</th><td>
1025 @ %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a>
1026 if( zParent ){
@@ -1950,11 +1939,11 @@
1950 int dflt;
1951 int res;
1952 int isBot;
1953 static char zDflt[2]
1954 /*static b/c cookie_link_parameter() does not copy it!*/;
1955 if( client_might_be_a_robot() && robot_restrict_has_tag("diff") ){
1956 dflt = 0;
1957 isBot = 1;
1958 }else{
1959 dflt = db_get_int("preferred-diff-type",-99);
1960 if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2;
1961
--- src/info.c
+++ src/info.c
@@ -993,34 +993,23 @@
993 @ <tr><th>Comment:</th><td class="infoComment">\
994 @ %!W(zEComment?zEComment:zComment)</td></tr>
995
996 /* The Download: line */
997 if( g.perm.Zip ){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
998 @ <tr><th>Downloads:</th><td>
999 if( robot_would_be_restricted("download") ){
1000 @ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
1001 }else{
1002 char *zBase = archive_base_name(rid);
1003 @ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a>
1004 @ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a>
1005 if( g.zLogin!=0 ){
1006 @ | %z(href("%R/sqlar/%s.sqlar",zBase))\
1007 @ SQL archive</a></td></tr>
1008 }
1009 fossil_free(zBase);
1010 }
1011 }
1012
1013 @ <tr><th>Timelines:</th><td>
1014 @ %z(href("%R/timeline?f=%!S&unhide",zUuid))family</a>
1015 if( zParent ){
@@ -1950,11 +1939,11 @@
1939 int dflt;
1940 int res;
1941 int isBot;
1942 static char zDflt[2]
1943 /*static b/c cookie_link_parameter() does not copy it!*/;
1944 if( robot_would_be_restricted("diff") ){
1945 dflt = 0;
1946 isBot = 1;
1947 }else{
1948 dflt = db_get_int("preferred-diff-type",-99);
1949 if( dflt<=0 ) dflt = user_agent_is_likely_mobile() ? 1 : 2;
1950
+48 -19
--- src/robot.c
+++ src/robot.c
@@ -263,19 +263,19 @@
263263
** The VALUE of this setting is a list of GLOB patterns that match
264264
** pages for which complex HTTP requests from unauthenticated clients
265265
** should be disallowed. "Unauthenticated" means the user is "nobody".
266266
** The recommended value for this setting is:
267267
**
268
-** timelineX,diff,annotate,fileage,file,finfo,reports
268
+** timelineX,diff,annotate,fileage,file,finfo,reports,tree,download,hexdump
269269
**
270270
** The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
271271
** /vpatch. The "annotate" tag also covers /blame and /praise. "zip"
272
-** also covers /tarball and /sqlar. If a tag has an "X" character appended,
272
+** also covers /tarball and /sqlar. If a tag has an "X" character appended
273273
** then it only applies if query parameters are such that the page is
274274
** particularly difficult to compute. In all other case, the tag should
275
-** exactly match the page name. Useful "X" tags include "timelineX"
276
-** and "zipX". See the robot-zip-leaf and robot-zip-tag settings
275
+** exactly match the page name. Useful "X" tags include "timelineX" and
276
+** "zipX". See the [[robot-zip-leaf]] and [[robot-zip-tag]] settings
277277
** for additional controls associated with the "zipX" restriction.
278278
**
279279
** Change this setting "off" to disable all robot restrictions.
280280
*/
281281
/*
@@ -299,36 +299,37 @@
299299
/*
300300
** SETTING: robot-zip-leaf boolean
301301
**
302302
** If this setting is true, the robots are allowed to download tarballs,
303303
** ZIP-archives, and SQL-archives even though "zipX" is found in
304
-** the robot-restrict setting as long as the specific check-in being
304
+** the [[robot-restrict]] setting as long as the specific check-in being
305305
** downloaded is a leaf check-in.
306306
*/
307307
/*
308308
** SETTING: robot-zip-tag width=40 block-text
309309
**
310310
** If this setting is a list of GLOB patterns matching tags,
311311
** then robots are allowed to download tarballs, ZIP-archives, and
312
-** SQL-archives even though "zipX" appears in robot-restrict, as long as
312
+** SQL-archives even though "zipX" appears in [[robot-restrict]], as long as
313313
** the specific check-in being downloaded has a tags that matches
314314
** the GLOB list of this setting. Recommended value:
315315
** "release,robot-access".
316316
*/
317317
318318
/*
319319
** Return the default restriction GLOB
320320
*/
321321
const char *robot_restrict_default(void){
322
- return "timelineX,diff,annotate,fileage,file,finfo,reports";
322
+ return "timelineX,diff,annotate,fileage,file,finfo,reports,"
323
+ "tree,hexdump,download";
323324
}
324325
325326
/*
326327
** Return true if zTag matches one of the tags in the robot-restrict
327328
** setting.
328329
*/
329
-int robot_restrict_has_tag(const char *zTag){
330
+static int robot_restrict_has_tag(const char *zTag){
330331
static const char *zGlob = 0;
331332
if( zGlob==0 ){
332333
zGlob = db_get("robot-restrict",robot_restrict_default());
333334
if( zGlob==0 ) zGlob = "";
334335
}
@@ -408,32 +409,60 @@
408409
fossil_free(zRequest);
409410
return bMatch;
410411
}
411412
412413
/*
413
-** Check to see if the page named in the argument is on the
414
-** robot-restrict list. If it is on the list and if the user
415
-** is "nobody" then bring up a captcha to test to make sure that
416
-** client is not a robot.
414
+** Return true if one or more of the conditions below are true.
415
+** Return false if all of the following are false:
416
+**
417
+** * The zTag is on the robot-restrict list
418
+**
419
+** * The client that submitted the HTTP request might be
420
+** a robot
421
+**
422
+** * The Request URI does not match any of the exceptions
423
+** in the robot-exception setting.
424
+**
425
+** In other words, return true if a call to robot_restrict() would
426
+** return true and false if a call to robot_restrict() would return
427
+** false.
417428
**
418
-** This routine returns true if a captcha was rendered and if subsequent
419
-** page generation should be aborted. It returns false if the page
420
-** should not be restricted and should be rendered normally.
429
+** The difference between this routine an robot_restrict() is that
430
+** this routine does not generate a proof-of-work captcha. This
431
+** routine does not change the HTTP reply in any way. It simply
432
+** returns true or false.
421433
*/
422
-int robot_restrict(const char *zTag){
434
+int robot_would_be_restricted(const char *zTag){
423435
if( robot.resultCache==KNOWN_NOT_ROBOT ) return 0;
424436
if( !robot_restrict_has_tag(zTag) ) return 0;
425437
if( !client_might_be_a_robot() ) return 0;
426438
if( robot_exception() ){
427439
robot.resultCache = KNOWN_NOT_ROBOT;
428440
return 0;
429441
}
430
-
431
- /* Generate the proof-of-work captcha */
432
- ask_for_proof_that_client_is_not_robot();
433442
return 1;
434443
}
444
+
445
+/*
446
+** Check to see if the page named in the argument is on the
447
+** robot-restrict list. If it is on the list and if the user
448
+** is might be a robot, then bring up a captcha to test to make
449
+** sure that client is not a robot.
450
+**
451
+** This routine returns true if a captcha was rendered and if subsequent
452
+** page generation should be aborted. It returns false if the page
453
+** should not be restricted and should be rendered normally.
454
+*/
455
+int robot_restrict(const char *zTag){
456
+ if( robot_would_be_restricted(zTag) ){
457
+ /* Generate the proof-of-work captcha */
458
+ ask_for_proof_that_client_is_not_robot();
459
+ return 1;
460
+ }else{
461
+ return 0;
462
+ }
463
+}
435464
436465
/*
437466
** Check to see if a robot is allowed to download a tarball, ZIP archive,
438467
** or SQL Archive for a particular check-in identified by the "rid"
439468
** argument. Return true to block the download. Return false to
440469
--- src/robot.c
+++ src/robot.c
@@ -263,19 +263,19 @@
263 ** The VALUE of this setting is a list of GLOB patterns that match
264 ** pages for which complex HTTP requests from unauthenticated clients
265 ** should be disallowed. "Unauthenticated" means the user is "nobody".
266 ** The recommended value for this setting is:
267 **
268 ** timelineX,diff,annotate,fileage,file,finfo,reports
269 **
270 ** The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
271 ** /vpatch. The "annotate" tag also covers /blame and /praise. "zip"
272 ** also covers /tarball and /sqlar. If a tag has an "X" character appended,
273 ** then it only applies if query parameters are such that the page is
274 ** particularly difficult to compute. In all other case, the tag should
275 ** exactly match the page name. Useful "X" tags include "timelineX"
276 ** and "zipX". See the robot-zip-leaf and robot-zip-tag settings
277 ** for additional controls associated with the "zipX" restriction.
278 **
279 ** Change this setting "off" to disable all robot restrictions.
280 */
281 /*
@@ -299,36 +299,37 @@
299 /*
300 ** SETTING: robot-zip-leaf boolean
301 **
302 ** If this setting is true, the robots are allowed to download tarballs,
303 ** ZIP-archives, and SQL-archives even though "zipX" is found in
304 ** the robot-restrict setting as long as the specific check-in being
305 ** downloaded is a leaf check-in.
306 */
307 /*
308 ** SETTING: robot-zip-tag width=40 block-text
309 **
310 ** If this setting is a list of GLOB patterns matching tags,
311 ** then robots are allowed to download tarballs, ZIP-archives, and
312 ** SQL-archives even though "zipX" appears in robot-restrict, as long as
313 ** the specific check-in being downloaded has a tags that matches
314 ** the GLOB list of this setting. Recommended value:
315 ** "release,robot-access".
316 */
317
318 /*
319 ** Return the default restriction GLOB
320 */
321 const char *robot_restrict_default(void){
322 return "timelineX,diff,annotate,fileage,file,finfo,reports";
 
323 }
324
325 /*
326 ** Return true if zTag matches one of the tags in the robot-restrict
327 ** setting.
328 */
329 int robot_restrict_has_tag(const char *zTag){
330 static const char *zGlob = 0;
331 if( zGlob==0 ){
332 zGlob = db_get("robot-restrict",robot_restrict_default());
333 if( zGlob==0 ) zGlob = "";
334 }
@@ -408,32 +409,60 @@
408 fossil_free(zRequest);
409 return bMatch;
410 }
411
412 /*
413 ** Check to see if the page named in the argument is on the
414 ** robot-restrict list. If it is on the list and if the user
415 ** is "nobody" then bring up a captcha to test to make sure that
416 ** client is not a robot.
 
 
 
 
 
 
 
 
 
 
417 **
418 ** This routine returns true if a captcha was rendered and if subsequent
419 ** page generation should be aborted. It returns false if the page
420 ** should not be restricted and should be rendered normally.
 
421 */
422 int robot_restrict(const char *zTag){
423 if( robot.resultCache==KNOWN_NOT_ROBOT ) return 0;
424 if( !robot_restrict_has_tag(zTag) ) return 0;
425 if( !client_might_be_a_robot() ) return 0;
426 if( robot_exception() ){
427 robot.resultCache = KNOWN_NOT_ROBOT;
428 return 0;
429 }
430
431 /* Generate the proof-of-work captcha */
432 ask_for_proof_that_client_is_not_robot();
433 return 1;
434 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
436 /*
437 ** Check to see if a robot is allowed to download a tarball, ZIP archive,
438 ** or SQL Archive for a particular check-in identified by the "rid"
439 ** argument. Return true to block the download. Return false to
440
--- src/robot.c
+++ src/robot.c
@@ -263,19 +263,19 @@
263 ** The VALUE of this setting is a list of GLOB patterns that match
264 ** pages for which complex HTTP requests from unauthenticated clients
265 ** should be disallowed. "Unauthenticated" means the user is "nobody".
266 ** The recommended value for this setting is:
267 **
268 ** timelineX,diff,annotate,fileage,file,finfo,reports,tree,download,hexdump
269 **
270 ** The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
271 ** /vpatch. The "annotate" tag also covers /blame and /praise. "zip"
272 ** also covers /tarball and /sqlar. If a tag has an "X" character appended
273 ** then it only applies if query parameters are such that the page is
274 ** particularly difficult to compute. In all other case, the tag should
275 ** exactly match the page name. Useful "X" tags include "timelineX" and
276 ** "zipX". See the [[robot-zip-leaf]] and [[robot-zip-tag]] settings
277 ** for additional controls associated with the "zipX" restriction.
278 **
279 ** Change this setting "off" to disable all robot restrictions.
280 */
281 /*
@@ -299,36 +299,37 @@
299 /*
300 ** SETTING: robot-zip-leaf boolean
301 **
302 ** If this setting is true, the robots are allowed to download tarballs,
303 ** ZIP-archives, and SQL-archives even though "zipX" is found in
304 ** the [[robot-restrict]] setting as long as the specific check-in being
305 ** downloaded is a leaf check-in.
306 */
307 /*
308 ** SETTING: robot-zip-tag width=40 block-text
309 **
310 ** If this setting is a list of GLOB patterns matching tags,
311 ** then robots are allowed to download tarballs, ZIP-archives, and
312 ** SQL-archives even though "zipX" appears in [[robot-restrict]], as long as
313 ** the specific check-in being downloaded has a tags that matches
314 ** the GLOB list of this setting. Recommended value:
315 ** "release,robot-access".
316 */
317
318 /*
319 ** Return the default restriction GLOB
320 */
321 const char *robot_restrict_default(void){
322 return "timelineX,diff,annotate,fileage,file,finfo,reports,"
323 "tree,hexdump,download";
324 }
325
326 /*
327 ** Return true if zTag matches one of the tags in the robot-restrict
328 ** setting.
329 */
330 static int robot_restrict_has_tag(const char *zTag){
331 static const char *zGlob = 0;
332 if( zGlob==0 ){
333 zGlob = db_get("robot-restrict",robot_restrict_default());
334 if( zGlob==0 ) zGlob = "";
335 }
@@ -408,32 +409,60 @@
409 fossil_free(zRequest);
410 return bMatch;
411 }
412
413 /*
414 ** Return true if one or more of the conditions below are true.
415 ** Return false if all of the following are false:
416 **
417 ** * The zTag is on the robot-restrict list
418 **
419 ** * The client that submitted the HTTP request might be
420 ** a robot
421 **
422 ** * The Request URI does not match any of the exceptions
423 ** in the robot-exception setting.
424 **
425 ** In other words, return true if a call to robot_restrict() would
426 ** return true and false if a call to robot_restrict() would return
427 ** false.
428 **
429 ** The difference between this routine an robot_restrict() is that
430 ** this routine does not generate a proof-of-work captcha. This
431 ** routine does not change the HTTP reply in any way. It simply
432 ** returns true or false.
433 */
434 int robot_would_be_restricted(const char *zTag){
435 if( robot.resultCache==KNOWN_NOT_ROBOT ) return 0;
436 if( !robot_restrict_has_tag(zTag) ) return 0;
437 if( !client_might_be_a_robot() ) return 0;
438 if( robot_exception() ){
439 robot.resultCache = KNOWN_NOT_ROBOT;
440 return 0;
441 }
 
 
 
442 return 1;
443 }
444
445 /*
446 ** Check to see if the page named in the argument is on the
447 ** robot-restrict list. If it is on the list and if the user
448 ** is might be a robot, then bring up a captcha to test to make
449 ** sure that client is not a robot.
450 **
451 ** This routine returns true if a captcha was rendered and if subsequent
452 ** page generation should be aborted. It returns false if the page
453 ** should not be restricted and should be rendered normally.
454 */
455 int robot_restrict(const char *zTag){
456 if( robot_would_be_restricted(zTag) ){
457 /* Generate the proof-of-work captcha */
458 ask_for_proof_that_client_is_not_robot();
459 return 1;
460 }else{
461 return 0;
462 }
463 }
464
465 /*
466 ** Check to see if a robot is allowed to download a tarball, ZIP archive,
467 ** or SQL Archive for a particular check-in identified by the "rid"
468 ** argument. Return true to block the download. Return false to
469
+73 -40
--- src/setup.c
+++ src/setup.c
@@ -118,10 +118,12 @@
118118
setup_menu_entry("Settings", "setup_settings",
119119
"Web interface to the \"fossil settings\" command");
120120
}
121121
setup_menu_entry("Timeline", "setup_timeline",
122122
"Timeline display preferences");
123
+ setup_menu_entry("Tarballs and ZIPs", "setup_download",
124
+ "Preferences for auto-generated tarballs and ZIP files");
123125
if( setup_user ){
124126
setup_menu_entry("Login-Group", "setup_login_group",
125127
"Manage single sign-on between this repository and others"
126128
" on the same server");
127129
setup_menu_entry("Tickets", "tktsetup",
@@ -140,12 +142,14 @@
140142
setup_menu_entry("URL Aliases", "waliassetup",
141143
"Configure URL aliases");
142144
if( setup_user ){
143145
setup_menu_entry("Notification", "setup_notification",
144146
"Automatic notifications of changes via outbound email");
147
+#if 0 /* Disabled for now. Does this even work? */
145148
setup_menu_entry("Transfers", "xfersetup",
146149
"Configure the transfer system for this repository");
150
+#endif
147151
}
148152
setup_menu_entry("Skins", "setup_skin_admin",
149153
"Select and/or modify the web interface \"skins\"");
150154
setup_menu_entry("Moderation", "setup_modreq",
151155
"Enable/Disable requiring moderator approval of Wiki and/or Ticket"
@@ -488,14 +492,15 @@
488492
@ <p>
489493
@ &emsp;&emsp;&emsp;<tt>%h(robot_restrict_default())</tt>
490494
@ <p>
491495
@ The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
492496
@ /vpatch. The "annotate" tag covers /annotate and also /blame and
493
- @ /praise. The "zip" covers itself and also /tarball and /sqlar. If a
494
- @ tag has an "X" character appended, then it only applies if query
495
- @ parameters are such that the page is expensive and/or unusual.
496
- @ In all other case, the tag should exactly match the page name.
497
+ @ /praise. The "zip" covers itself and also /tarball and /sqlar.
498
+ @ If a tag has an "X" character appended (ex: "timelineX") then it only
499
+ @ applies if query parameters are such that the page is expensive
500
+ @ and/or unusual. In all other case, the tag should exactly match
501
+ @ the page name.
497502
@
498503
@ To disable robot restrictions, change this setting to "off".
499504
@ (Property: robot-restrict)
500505
@ <br>
501506
textarea_attribute("", 2, 80,
@@ -985,10 +990,15 @@
985990
"1", "HH:MM:SS",
986991
"2", "YYYY-MM-DD HH:MM",
987992
"3", "YYMMDD HH:MM",
988993
"4", "(off)"
989994
};
995
+ static const char *const azLeafMark[] = {
996
+ "0", "No",
997
+ "1", "Yes",
998
+ "2", "Yes - with emphasis",
999
+ };
9901000
login_check_credentials();
9911001
if( !g.perm.Admin ){
9921002
login_needed(0);
9931003
return;
9941004
}
@@ -1073,10 +1083,16 @@
10731083
@ in a separate box (using CSS class "timelineDate") whenever the date
10741084
@ changes. With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
10751085
@ the complete date and time is shown on every timeline entry using the
10761086
@ CSS class "timelineTime". (Property: "timeline-date-format")</p>
10771087
1088
+ @ <hr>
1089
+ multiple_choice_attribute("Leaf Markings", "timeline-mark-leaves",
1090
+ "tml", "1", count(azLeafMark)/2, azLeafMark);
1091
+ @ <p>Should timeline entries for leaf check-ins be identified in the
1092
+ @ detail section. (Property: "timeline-mark-leaves")</p>
1093
+
10781094
@ <hr>
10791095
entry_attribute("Max timeline comment length", 6,
10801096
"timeline-max-comment", "tmc", "0", 0);
10811097
@ <p>The maximum length of a comment to be displayed in a timeline.
10821098
@ "0" there is no length limit.
@@ -1326,28 +1342,10 @@
13261342
@ Omit the trailing "/".
13271343
@ If this repo will not be set up as a persistent server and will not
13281344
@ be sending email alerts, then leave this entry blank.
13291345
@ Suggested value: "%h(g.zBaseURL)"
13301346
@ (Property: "email-url")</p>
1331
- @ <hr>
1332
- entry_attribute("Tarball and ZIP-archive Prefix", 20, "short-project-name",
1333
- "spn", "", 0);
1334
- @ <p>This is used as a prefix on the names of generated tarballs and
1335
- @ ZIP archive. For best results, keep this prefix brief and avoid special
1336
- @ characters such as "/" and "\".
1337
- @ If no tarball prefix is specified, then the full Project Name above is used.
1338
- @ (Property: "short-project-name")
1339
- @ </p>
1340
- @ <hr>
1341
- entry_attribute("Download Tag", 20, "download-tag", "dlt", "trunk", 0);
1342
- @ <p>The <a href='%R/download'>/download</a> page is designed to provide
1343
- @ a convenient place for newbies
1344
- @ to download a ZIP archive or a tarball of the project. By default,
1345
- @ the latest trunk check-in is downloaded. Change this tag to something
1346
- @ else (ex: release) to alter the behavior of the /download page.
1347
- @ (Property: "download-tag")
1348
- @ </p>
13491347
@ <hr>
13501348
entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
13511349
@ <p>Enter the pathname of the page to display when the "Home" menu
13521350
@ option is selected and when no pathname is
13531351
@ specified in the URL. For example, if you visit the url:</p>
@@ -1426,30 +1424,65 @@
14261424
@ (Property: sitemap-extra)
14271425
@ <p>
14281426
textarea_attribute("Custom Sitemap Entries", 8, 80,
14291427
"sitemap-extra", "smextra", "", 0);
14301428
@ <hr>
1431
- @ <p>Configuration for the <a href="%R/tarlist">/tarlist</a> page.
1432
- @ The value is a TCL list divided into pairs.
1429
+ @ <p><input type="submit" name="submit" value="Apply Changes"></p>
1430
+ @ </div></form>
1431
+ db_end_transaction(0);
1432
+ style_finish_page();
1433
+}
1434
+
1435
+/*
1436
+** WEBPAGE: setup_download
1437
+**
1438
+** The "Admin/Download" page. Requires Setup privilege.
1439
+*/
1440
+void setup_download(void){
1441
+ login_check_credentials();
1442
+ if( !g.perm.Setup ){
1443
+ login_needed(0);
1444
+ return;
1445
+ }
1446
+
1447
+ style_set_current_feature("setup");
1448
+ style_header("Tarball and ZIP Downloads");
1449
+ db_begin_transaction();
1450
+ @ <form action="%R/setup_download" method="post"><div>
1451
+ login_insert_csrf_secret();
1452
+ @ <input type="submit" name="submit" value="Apply Changes"></p>
1453
+ @ <hr>
1454
+ entry_attribute("Tarball and ZIP Name Prefix", 20, "short-project-name",
1455
+ "spn", "", 0);
1456
+ @ <p>This is used as a prefix for the names of generated tarballs and
1457
+ @ ZIP archive. Keep this prefix brief and use only lower-case ASCII
1458
+ @ characters, digits, "_", "-" in the name. If this setting is blank,
1459
+ @ then the full <a href='%R/help/project-name'>project-name</a> setting
1460
+ @ is used instead.
1461
+ @ (Property: "short-project-name")
1462
+ @ </p>
1463
+ @ <hr>
1464
+ @ <p><b>Configuration for the <a href="%R/download">/download</a> page.</b>
1465
+ @ <p>The value is a TCL list divided into groups of four tokens:
14331466
@ <ol>
1434
- @ <li> The first term of each pair is an integer (N).
1435
- @ <li> The second term of each pair is a glob pattern (PATTERN).
1467
+ @ <li> Maximum number of matches (COUNT).
1468
+ @ <li> Tag to match using glob (TAG).
1469
+ @ <li> Maximum age of check-ins to match (MAX_AGE).
1470
+ @ <li> Comment to apply to matches (COMMENT).
14361471
@ </ol>
1437
- @ For each pair, the most recent N check-ins that have a tag that
1438
- @ matches PATTERN are included in on the /tarlist page. The special
1439
- @ pattern of "OPEN-LEAF" matches all open leaf check-ins. Example:
1440
- @ <blockquote><tt>1 trunk 3 release 5 OPEN-LEAF</tt></blockquote>
1441
- @ The example pattern above shows the union of the most recent trunk
1442
- @ check-in, the 5 most recent open leaf check-ins, and the 3 most
1443
- @ recent check-ins tagged with "release".
1444
- @ <p>
1445
- @ The /tarlist page is omitted from the <a href="%R/sitemap">/sitemap</a>
1446
- @ if the first token is "0". The default value is "1 trunk".
1447
- @ (Property: suggested-tarlist)
1448
- @ <p>
1449
- textarea_attribute("Check-ins To Show On /tarlist", 2, 80,
1450
- "suggested-tarlist", "sgtrlst", "", 0);
1472
+ @ Each 4-tuple will match zero or more check-ins. The /download page
1473
+ @ displays the union of matches from all 4-tuples.
1474
+ @ See the <a href="%R/help/suggested-downloads">suggested-downloads</a>
1475
+ @ setting documentation for further detail.
1476
+ @ <p>
1477
+ @ The /download page is omitted from the <a href="%R/sitemap">/sitemap</a>
1478
+ @ if the first token is "0" or "off" or "no". The default value
1479
+ @ for this setting is "off".
1480
+ @ (Property: <a href="%R/help/suggested-downloads">suggested-downloads</a>)
1481
+ @ <p>
1482
+ textarea_attribute("", 4, 80,
1483
+ "suggested-downloads", "sgtrlst", "off", 0);
14511484
@ <hr>
14521485
@ <p><input type="submit" name="submit" value="Apply Changes"></p>
14531486
@ </div></form>
14541487
db_end_transaction(0);
14551488
style_finish_page();
14561489
--- src/setup.c
+++ src/setup.c
@@ -118,10 +118,12 @@
118 setup_menu_entry("Settings", "setup_settings",
119 "Web interface to the \"fossil settings\" command");
120 }
121 setup_menu_entry("Timeline", "setup_timeline",
122 "Timeline display preferences");
 
 
123 if( setup_user ){
124 setup_menu_entry("Login-Group", "setup_login_group",
125 "Manage single sign-on between this repository and others"
126 " on the same server");
127 setup_menu_entry("Tickets", "tktsetup",
@@ -140,12 +142,14 @@
140 setup_menu_entry("URL Aliases", "waliassetup",
141 "Configure URL aliases");
142 if( setup_user ){
143 setup_menu_entry("Notification", "setup_notification",
144 "Automatic notifications of changes via outbound email");
 
145 setup_menu_entry("Transfers", "xfersetup",
146 "Configure the transfer system for this repository");
 
147 }
148 setup_menu_entry("Skins", "setup_skin_admin",
149 "Select and/or modify the web interface \"skins\"");
150 setup_menu_entry("Moderation", "setup_modreq",
151 "Enable/Disable requiring moderator approval of Wiki and/or Ticket"
@@ -488,14 +492,15 @@
488 @ <p>
489 @ &emsp;&emsp;&emsp;<tt>%h(robot_restrict_default())</tt>
490 @ <p>
491 @ The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
492 @ /vpatch. The "annotate" tag covers /annotate and also /blame and
493 @ /praise. The "zip" covers itself and also /tarball and /sqlar. If a
494 @ tag has an "X" character appended, then it only applies if query
495 @ parameters are such that the page is expensive and/or unusual.
496 @ In all other case, the tag should exactly match the page name.
 
497 @
498 @ To disable robot restrictions, change this setting to "off".
499 @ (Property: robot-restrict)
500 @ <br>
501 textarea_attribute("", 2, 80,
@@ -985,10 +990,15 @@
985 "1", "HH:MM:SS",
986 "2", "YYYY-MM-DD HH:MM",
987 "3", "YYMMDD HH:MM",
988 "4", "(off)"
989 };
 
 
 
 
 
990 login_check_credentials();
991 if( !g.perm.Admin ){
992 login_needed(0);
993 return;
994 }
@@ -1073,10 +1083,16 @@
1073 @ in a separate box (using CSS class "timelineDate") whenever the date
1074 @ changes. With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
1075 @ the complete date and time is shown on every timeline entry using the
1076 @ CSS class "timelineTime". (Property: "timeline-date-format")</p>
1077
 
 
 
 
 
 
1078 @ <hr>
1079 entry_attribute("Max timeline comment length", 6,
1080 "timeline-max-comment", "tmc", "0", 0);
1081 @ <p>The maximum length of a comment to be displayed in a timeline.
1082 @ "0" there is no length limit.
@@ -1326,28 +1342,10 @@
1326 @ Omit the trailing "/".
1327 @ If this repo will not be set up as a persistent server and will not
1328 @ be sending email alerts, then leave this entry blank.
1329 @ Suggested value: "%h(g.zBaseURL)"
1330 @ (Property: "email-url")</p>
1331 @ <hr>
1332 entry_attribute("Tarball and ZIP-archive Prefix", 20, "short-project-name",
1333 "spn", "", 0);
1334 @ <p>This is used as a prefix on the names of generated tarballs and
1335 @ ZIP archive. For best results, keep this prefix brief and avoid special
1336 @ characters such as "/" and "\".
1337 @ If no tarball prefix is specified, then the full Project Name above is used.
1338 @ (Property: "short-project-name")
1339 @ </p>
1340 @ <hr>
1341 entry_attribute("Download Tag", 20, "download-tag", "dlt", "trunk", 0);
1342 @ <p>The <a href='%R/download'>/download</a> page is designed to provide
1343 @ a convenient place for newbies
1344 @ to download a ZIP archive or a tarball of the project. By default,
1345 @ the latest trunk check-in is downloaded. Change this tag to something
1346 @ else (ex: release) to alter the behavior of the /download page.
1347 @ (Property: "download-tag")
1348 @ </p>
1349 @ <hr>
1350 entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
1351 @ <p>Enter the pathname of the page to display when the "Home" menu
1352 @ option is selected and when no pathname is
1353 @ specified in the URL. For example, if you visit the url:</p>
@@ -1426,30 +1424,65 @@
1426 @ (Property: sitemap-extra)
1427 @ <p>
1428 textarea_attribute("Custom Sitemap Entries", 8, 80,
1429 "sitemap-extra", "smextra", "", 0);
1430 @ <hr>
1431 @ <p>Configuration for the <a href="%R/tarlist">/tarlist</a> page.
1432 @ The value is a TCL list divided into pairs.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1433 @ <ol>
1434 @ <li> The first term of each pair is an integer (N).
1435 @ <li> The second term of each pair is a glob pattern (PATTERN).
 
 
1436 @ </ol>
1437 @ For each pair, the most recent N check-ins that have a tag that
1438 @ matches PATTERN are included in on the /tarlist page. The special
1439 @ pattern of "OPEN-LEAF" matches all open leaf check-ins. Example:
1440 @ <blockquote><tt>1 trunk 3 release 5 OPEN-LEAF</tt></blockquote>
1441 @ The example pattern above shows the union of the most recent trunk
1442 @ check-in, the 5 most recent open leaf check-ins, and the 3 most
1443 @ recent check-ins tagged with "release".
1444 @ <p>
1445 @ The /tarlist page is omitted from the <a href="%R/sitemap">/sitemap</a>
1446 @ if the first token is "0". The default value is "1 trunk".
1447 @ (Property: suggested-tarlist)
1448 @ <p>
1449 textarea_attribute("Check-ins To Show On /tarlist", 2, 80,
1450 "suggested-tarlist", "sgtrlst", "", 0);
1451 @ <hr>
1452 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
1453 @ </div></form>
1454 db_end_transaction(0);
1455 style_finish_page();
1456
--- src/setup.c
+++ src/setup.c
@@ -118,10 +118,12 @@
118 setup_menu_entry("Settings", "setup_settings",
119 "Web interface to the \"fossil settings\" command");
120 }
121 setup_menu_entry("Timeline", "setup_timeline",
122 "Timeline display preferences");
123 setup_menu_entry("Tarballs and ZIPs", "setup_download",
124 "Preferences for auto-generated tarballs and ZIP files");
125 if( setup_user ){
126 setup_menu_entry("Login-Group", "setup_login_group",
127 "Manage single sign-on between this repository and others"
128 " on the same server");
129 setup_menu_entry("Tickets", "tktsetup",
@@ -140,12 +142,14 @@
142 setup_menu_entry("URL Aliases", "waliassetup",
143 "Configure URL aliases");
144 if( setup_user ){
145 setup_menu_entry("Notification", "setup_notification",
146 "Automatic notifications of changes via outbound email");
147 #if 0 /* Disabled for now. Does this even work? */
148 setup_menu_entry("Transfers", "xfersetup",
149 "Configure the transfer system for this repository");
150 #endif
151 }
152 setup_menu_entry("Skins", "setup_skin_admin",
153 "Select and/or modify the web interface \"skins\"");
154 setup_menu_entry("Moderation", "setup_modreq",
155 "Enable/Disable requiring moderator approval of Wiki and/or Ticket"
@@ -488,14 +492,15 @@
492 @ <p>
493 @ &emsp;&emsp;&emsp;<tt>%h(robot_restrict_default())</tt>
494 @ <p>
495 @ The "diff" tag covers all diffing pages such as /vdiff, /fdiff, and
496 @ /vpatch. The "annotate" tag covers /annotate and also /blame and
497 @ /praise. The "zip" covers itself and also /tarball and /sqlar.
498 @ If a tag has an "X" character appended (ex: "timelineX") then it only
499 @ applies if query parameters are such that the page is expensive
500 @ and/or unusual. In all other case, the tag should exactly match
501 @ the page name.
502 @
503 @ To disable robot restrictions, change this setting to "off".
504 @ (Property: robot-restrict)
505 @ <br>
506 textarea_attribute("", 2, 80,
@@ -985,10 +990,15 @@
990 "1", "HH:MM:SS",
991 "2", "YYYY-MM-DD HH:MM",
992 "3", "YYMMDD HH:MM",
993 "4", "(off)"
994 };
995 static const char *const azLeafMark[] = {
996 "0", "No",
997 "1", "Yes",
998 "2", "Yes - with emphasis",
999 };
1000 login_check_credentials();
1001 if( !g.perm.Admin ){
1002 login_needed(0);
1003 return;
1004 }
@@ -1073,10 +1083,16 @@
1083 @ in a separate box (using CSS class "timelineDate") whenever the date
1084 @ changes. With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
1085 @ the complete date and time is shown on every timeline entry using the
1086 @ CSS class "timelineTime". (Property: "timeline-date-format")</p>
1087
1088 @ <hr>
1089 multiple_choice_attribute("Leaf Markings", "timeline-mark-leaves",
1090 "tml", "1", count(azLeafMark)/2, azLeafMark);
1091 @ <p>Should timeline entries for leaf check-ins be identified in the
1092 @ detail section. (Property: "timeline-mark-leaves")</p>
1093
1094 @ <hr>
1095 entry_attribute("Max timeline comment length", 6,
1096 "timeline-max-comment", "tmc", "0", 0);
1097 @ <p>The maximum length of a comment to be displayed in a timeline.
1098 @ "0" there is no length limit.
@@ -1326,28 +1342,10 @@
1342 @ Omit the trailing "/".
1343 @ If this repo will not be set up as a persistent server and will not
1344 @ be sending email alerts, then leave this entry blank.
1345 @ Suggested value: "%h(g.zBaseURL)"
1346 @ (Property: "email-url")</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1347 @ <hr>
1348 entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
1349 @ <p>Enter the pathname of the page to display when the "Home" menu
1350 @ option is selected and when no pathname is
1351 @ specified in the URL. For example, if you visit the url:</p>
@@ -1426,30 +1424,65 @@
1424 @ (Property: sitemap-extra)
1425 @ <p>
1426 textarea_attribute("Custom Sitemap Entries", 8, 80,
1427 "sitemap-extra", "smextra", "", 0);
1428 @ <hr>
1429 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
1430 @ </div></form>
1431 db_end_transaction(0);
1432 style_finish_page();
1433 }
1434
1435 /*
1436 ** WEBPAGE: setup_download
1437 **
1438 ** The "Admin/Download" page. Requires Setup privilege.
1439 */
1440 void setup_download(void){
1441 login_check_credentials();
1442 if( !g.perm.Setup ){
1443 login_needed(0);
1444 return;
1445 }
1446
1447 style_set_current_feature("setup");
1448 style_header("Tarball and ZIP Downloads");
1449 db_begin_transaction();
1450 @ <form action="%R/setup_download" method="post"><div>
1451 login_insert_csrf_secret();
1452 @ <input type="submit" name="submit" value="Apply Changes"></p>
1453 @ <hr>
1454 entry_attribute("Tarball and ZIP Name Prefix", 20, "short-project-name",
1455 "spn", "", 0);
1456 @ <p>This is used as a prefix for the names of generated tarballs and
1457 @ ZIP archive. Keep this prefix brief and use only lower-case ASCII
1458 @ characters, digits, "_", "-" in the name. If this setting is blank,
1459 @ then the full <a href='%R/help/project-name'>project-name</a> setting
1460 @ is used instead.
1461 @ (Property: "short-project-name")
1462 @ </p>
1463 @ <hr>
1464 @ <p><b>Configuration for the <a href="%R/download">/download</a> page.</b>
1465 @ <p>The value is a TCL list divided into groups of four tokens:
1466 @ <ol>
1467 @ <li> Maximum number of matches (COUNT).
1468 @ <li> Tag to match using glob (TAG).
1469 @ <li> Maximum age of check-ins to match (MAX_AGE).
1470 @ <li> Comment to apply to matches (COMMENT).
1471 @ </ol>
1472 @ Each 4-tuple will match zero or more check-ins. The /download page
1473 @ displays the union of matches from all 4-tuples.
1474 @ See the <a href="%R/help/suggested-downloads">suggested-downloads</a>
1475 @ setting documentation for further detail.
1476 @ <p>
1477 @ The /download page is omitted from the <a href="%R/sitemap">/sitemap</a>
1478 @ if the first token is "0" or "off" or "no". The default value
1479 @ for this setting is "off".
1480 @ (Property: <a href="%R/help/suggested-downloads">suggested-downloads</a>)
1481 @ <p>
1482 textarea_attribute("", 4, 80,
1483 "suggested-downloads", "sgtrlst", "off", 0);
 
 
1484 @ <hr>
1485 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
1486 @ </div></form>
1487 db_end_transaction(0);
1488 style_finish_page();
1489
+2 -2
--- src/sitemap.c
+++ src/sitemap.c
@@ -133,12 +133,12 @@
133133
if( g.perm.Write && zEditGlob[0]!=0 ){
134134
@ <li>%z(href("%R/fileedit"))On-line File Editor</li>
135135
}
136136
@ </ul>
137137
}
138
- if( g.perm.Zip && db_get_boolean("suggested-tarlist",1)!=0 ){
139
- @ <li>%z(href("%R/tarlist"))Tarballs and ZIPs</a>
138
+ if( g.perm.Zip && db_get_boolean("suggested-downloads",0)!=0 ){
139
+ @ <li>%z(href("%R/download"))Tarballs and ZIPs</a>
140140
}
141141
if( g.perm.Read ){
142142
@ <li>%z(href("%R/timeline"))Project Timeline</a>
143143
@ <ul>
144144
@ <li>%z(href("%R/reports"))Activity Reports</a></li>
145145
--- src/sitemap.c
+++ src/sitemap.c
@@ -133,12 +133,12 @@
133 if( g.perm.Write && zEditGlob[0]!=0 ){
134 @ <li>%z(href("%R/fileedit"))On-line File Editor</li>
135 }
136 @ </ul>
137 }
138 if( g.perm.Zip && db_get_boolean("suggested-tarlist",1)!=0 ){
139 @ <li>%z(href("%R/tarlist"))Tarballs and ZIPs</a>
140 }
141 if( g.perm.Read ){
142 @ <li>%z(href("%R/timeline"))Project Timeline</a>
143 @ <ul>
144 @ <li>%z(href("%R/reports"))Activity Reports</a></li>
145
--- src/sitemap.c
+++ src/sitemap.c
@@ -133,12 +133,12 @@
133 if( g.perm.Write && zEditGlob[0]!=0 ){
134 @ <li>%z(href("%R/fileedit"))On-line File Editor</li>
135 }
136 @ </ul>
137 }
138 if( g.perm.Zip && db_get_boolean("suggested-downloads",0)!=0 ){
139 @ <li>%z(href("%R/download"))Tarballs and ZIPs</a>
140 }
141 if( g.perm.Read ){
142 @ <li>%z(href("%R/timeline"))Project Timeline</a>
143 @ <ul>
144 @ <li>%z(href("%R/reports"))Activity Reports</a></li>
145
+350 -76
--- src/tar.c
+++ src/tar.c
@@ -31,10 +31,65 @@
3131
char *zPrevDir; /* Name of directory for previous entry */
3232
int nPrevDirAlloc; /* size of zPrevDir */
3333
Blob pax; /* PAX data */
3434
} tball;
3535
36
+/*
37
+** Convert a string so that it contains only lower-case ASCII, digits,
38
+** "_" and "-". Changes are made in-place.
39
+*/
40
+static void sanitize_name(char *zName){
41
+ int i;
42
+ char c;
43
+ for(i=0; (c = zName[i])!=0; i++){
44
+ if( fossil_isupper(c) ){
45
+ zName[i] = fossil_tolower(c);
46
+ }else if( !fossil_isalnum(c) && c!='_' && c!='-' ){
47
+ if( c<=0x7f ){
48
+ zName[i] = '_';
49
+ }else{
50
+ /* 123456789 123456789 123456 */
51
+ zName[i] = "abcdefghijklmnopqrstuvwxyz"[(unsigned)c%26];
52
+ }
53
+ }
54
+ }
55
+}
56
+
57
+/*
58
+** Compute a sensible base-name for an archive file (tarball, ZIP, or SQLAR)
59
+** based on the rid of the check-in contained in that file.
60
+**
61
+** PROJECTNAME-DATETIME-HASHPREFIX
62
+**
63
+** So that the name will be safe to use as a URL or a filename on any system,
64
+** the name is only allowed to contain lower-case ASCII alphabetics,
65
+** digits, '_' and '-'. Upper-case ASCII is converted to lower-case. All
66
+** other bytes are mapped into a lower-case alphabetic.
67
+**
68
+** The value returned is obtained from mprintf() or fossil_strdup() and should
69
+** be released by the caller using fossil_free().
70
+*/
71
+char *archive_base_name(int rid){
72
+ char *zPrefix;
73
+ char *zName;
74
+ zPrefix = db_get("short-project-name",0);
75
+ if( zPrefix==0 || zPrefix[0]==0 ){
76
+ zPrefix = db_get("project-name","unnamed");
77
+ }
78
+ zName = db_text(0,
79
+ "SELECT %Q||"
80
+ " strftime('-%%Y%%m%%d%%H%%M%%S-',event.mtime)||"
81
+ " substr(blob.uuid,1,10)"
82
+ " FROM blob, event LEFT JOIN config"
83
+ " WHERE blob.rid=%d"
84
+ " AND event.objid=%d"
85
+ " AND config.name='project-name'",
86
+ zPrefix, rid, rid);
87
+ fossil_free(zPrefix);
88
+ sanitize_name(zName);
89
+ return zName;
90
+}
3691
3792
/*
3893
** field lengths of 'ustar' name and prefix fields.
3994
*/
4095
#define USTAR_NAME_LEN 100
@@ -653,19 +708,11 @@
653708
if( fossil_strcmp("/dev/null",zOut)==0 || fossil_strcmp("",zOut)==0 ){
654709
zOut = 0;
655710
}
656711
657712
if( zName==0 ){
658
- zName = db_text("default-name",
659
- "SELECT replace(%Q,' ','_') "
660
- " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
661
- " || substr(blob.uuid, 1, 10)"
662
- " FROM event, blob"
663
- " WHERE event.objid=%d"
664
- " AND blob.rid=%d",
665
- db_get("project-name", "unnamed"), rid, rid
666
- );
713
+ zName = archive_base_name(rid);
667714
}
668715
tarball_of_checkin(rid, zOut ? &tarball : 0,
669716
zName, pInclude, pExclude, listFlag);
670717
glob_free(pInclude);
671718
glob_free(pExclude);
@@ -673,27 +720,76 @@
673720
if( zOut ){
674721
blob_write_to_file(&tarball, zOut);
675722
blob_reset(&tarball);
676723
}
677724
}
725
+
726
+/*
727
+** This is a helper routine for tar_uuid_from_name(). It handles
728
+** the case where *pzName contains no "/" character. Check for
729
+** format (3). Return the hash if the name matches format (3),
730
+** or return NULL if it does not.
731
+*/
732
+static char *format_three_parser(const char *zName){
733
+ int iDot = 0; /* Index in zName[] of the first '.' */
734
+ int iDash1 = 0; /* Index in zName[] of the '-' before the timestamp */
735
+ int iDash2 = 0; /* Index in zName[] of the '-' between timestamp and hash */
736
+ int nHash; /* Size of the hash */
737
+ char *zHash; /* A copy of the hash value */
738
+ char *zDate; /* Copy of the timestamp */
739
+ char *zUuid; /* Final result */
740
+ int i; /* Loop query */
741
+ Stmt q; /* Query to verify that hash and timestamp agree */
742
+
743
+ for(i=0; zName[i]; i++){
744
+ char c = zName[i];
745
+ if( c=='.' ){ iDot = i; break; }
746
+ if( c=='-' ){ iDash1 = iDash2; iDash2 = i; }
747
+ if( !fossil_isalnum(c) && c!='_' && c!='-' ){ break; }
748
+ }
749
+ if( iDot==0 ) return 0;
750
+ if( iDash1==0 ) return 0;
751
+ nHash = iDot - iDash2 - 1;
752
+ if( nHash<8 ) return 0; /* HASH value too short */
753
+ if( (iDash2 - iDash1)!=15 ) return 0; /* Wrong timestamp size */
754
+ zHash = fossil_strndup(&zName[iDash2+1], nHash);
755
+ zDate = fossil_strndup(&zName[iDash1+1], 14);
756
+ db_prepare(&q,
757
+ "SELECT blob.uuid"
758
+ " FROM blob JOIN event ON event.objid=blob.rid"
759
+ " WHERE blob.uuid GLOB '%q*'"
760
+ " AND strftime('%%Y%%m%%d%%H%%M%%S',event.mtime)='%q'",
761
+ zHash, zDate
762
+ );
763
+ fossil_free(zHash);
764
+ fossil_free(zDate);
765
+ if( db_step(&q)==SQLITE_ROW ){
766
+ zUuid = fossil_strdup(db_column_text(&q,0));
767
+ }else{
768
+ zUuid = 0;
769
+ }
770
+ db_finalize(&q);
771
+ return zUuid;
772
+}
678773
679774
/*
680775
** Check to see if the input string is of one of the following
681776
** two the forms:
682777
**
683778
** check-in-name/filename.ext (1)
684
-** tag-name/check-in-name/filename.txt (2)
779
+** tag-name/check-in-name/filename.ext (2)
780
+** project-datetime-hash.ext (3)
685781
**
686782
** In other words, check to see if the input string contains either
687783
** a check-in name or a tag-name and a check-in name separated by
688
-** a slash. There must be either 1 or 2 "/" characters. In the
784
+** a slash. There must be between 0 or 2 "/" characters. In the
689785
** second form, tag-name must be an individual tag (not a branch-tag)
690786
** that is found on the check-in identified by the check-in-name.
691787
**
692788
** If the condition is true, then:
693789
**
694
-** * Make *pzName point to the fielname suffix only
790
+** * Make *pzName point to the filename suffix only
695791
** * return a copy of the check-in name in memory from mprintf().
696792
**
697793
** If the condition is false, leave *pzName unchanged and return either
698794
** NULL or an empty string. Normally NULL is returned, however an
699795
** empty string is returned for format (2) if check-in-name does not
@@ -705,10 +801,19 @@
705801
**
706802
** Such URLs will pass through most anti-robot filters because of the
707803
** "/tarball/release" prefix will match the suggested "robot-exception"
708804
** pattern and can still refer to an historic release rather than just
709805
** the most recent release.
806
+**
807
+** Format (3) is designed to allow URLs like this:
808
+**
809
+** /tarball/fossil-20251018193920-d6c9aee97df.tar.gz
810
+**
811
+** In other words, filename itself contains sufficient information to
812
+** uniquely identify the check-in, including a timestamp of the form
813
+** YYYYMMDDHHMMSS and a prefix of the check-in hash. The timestamp
814
+** and hash must immediately preceed the first "." in the name.
710815
*/
711816
char *tar_uuid_from_name(char **pzName){
712817
char *zName = *pzName; /* Original input */
713818
int n1 = 0; /* Bytes in first prefix (tag-name) */
714819
int n2 = 0; /* Bytes in second prefix (check-in-name) */
@@ -724,11 +829,12 @@
724829
return 0; /* More than two "/" characters seen */
725830
}
726831
}
727832
}
728833
if( n1==0 ){
729
- return 0; /* No prefix of any kind */
834
+ /* Check for format (3) */
835
+ return format_three_parser(*pzName);
730836
}
731837
if( zName[n+1]==0 ){
732838
return 0; /* No filename suffix */
733839
}
734840
if( n2==0 ){
@@ -933,142 +1039,310 @@
9331039
cgi_set_content(&tarball);
9341040
cgi_set_content_type("application/x-compressed");
9351041
}
9361042
9371043
/*
938
-** This routine is called for each check-in on the /tarlist page to
1044
+** This routine is called for each check-in on the /download page to
9391045
** construct the "extra" information after the description.
9401046
*/
941
-static void tarlist_extra(
1047
+void download_extra(
9421048
Stmt *pQuery, /* Current row of the timeline query */
9431049
int tmFlags, /* Flags to www_print_timeline() */
9441050
const char *zThisUser, /* Suppress links to this user */
9451051
const char *zThisTag /* Suppress links to this tag */
9461052
){
947
- int rid = db_column_int(pQuery, 0);
948
- const char *zUuid = db_column_text(pQuery, 1);
949
- const char *zDate = db_column_text(pQuery, 2);
950
- char *zBrName = branch_of_rid(rid);
951
- static const char *zProject = 0;
952
- int nProject;
953
- char *zNm;
954
-
955
- if( zProject==0 ) zProject = db_get("project-name","unnamed");
956
- zNm = mprintf("%s-%sZ-%.8s", zProject, zDate, zUuid);
957
- nProject = (int)strlen(zProject);
958
- zNm[nProject+11] = 'T';
959
- @ <strong>%h(zBrName)</strong><br>\
960
- @ %z(href("%R/timeline?c=%!S&y=ci&n=11",zUuid))<button>Context</button></a>
961
- @ %z(href("%R/tarball/%!S/%t.tar.gz",zUuid,zNm))\
962
- @ <button>Tarball</button></a>
963
- @ %z(href("%R/zip/%!S/%t.zip",zUuid,zNm))\
964
- @ <button>ZIP&nbsp;Archive</button></a>
965
- fossil_free(zBrName);
966
- fossil_free(zNm);
967
-}
968
-
969
-/*
970
-** SETTING: suggested-tarlist width=70 block-text
971
-**
972
-** This setting controls the suggested tarball/ZIP downloads on the
973
-** [[/tarlist]] page. The value is a TCL list. Each pair of items
974
-** defines a set of check-ins to be added to the suggestion list.
975
-** The first item of each pair is an integer count (N) and second
976
-** item is a tag GLOB pattern (PATTERN). For each pair, the most
977
-** recent N check-ins that have a tag matching PATTERN are added
978
-** to the list. The special pattern "OPEN-LEAF" matches any open
979
-** leaf check-in.
980
-**
981
-** Example:
982
-**
983
-** 3 OPEN-LEAF 3 release 1 trunk
984
-**
985
-** The value causes the /tarlist page to show the union of the 3
986
-** most recent open leaves, the three most recent check-ins marked
987
-** "release", and the single most recent trunk check-in.
988
-*/
989
-
990
-/*
991
-** WEBPAGE: /tarlist
1053
+ const char *zType = db_column_text(pQuery, 7);
1054
+ assert( zType!=0 );
1055
+ if( zType[0]!='c' ){
1056
+ timeline_extra(pQuery, tmFlags, zThisUser, zThisTag);
1057
+ }else{
1058
+ int rid = db_column_int(pQuery, 0);
1059
+ const char *zUuid = db_column_text(pQuery, 1);
1060
+ char *zBrName = branch_of_rid(rid);
1061
+ char *zNm;
1062
+
1063
+ if( tmFlags & TIMELINE_COLUMNAR ){
1064
+ @ <nobr>check-in:&nbsp;\
1065
+ @ %z(href("%R/info/%!S",zUuid))<span class='timelineHash'>\
1066
+ @ %S(zUuid)</span></a></nobr><br>
1067
+ if( fossil_strcmp(zBrName,"trunk")!=0 ){
1068
+ @ <nobr>branch:&nbsp;\
1069
+ @ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a></nobr><br>\
1070
+ }
1071
+ }else{
1072
+ if( (tmFlags & TIMELINE_CLASSIC)==0 ){
1073
+ @ check-in:&nbsp;%z(href("%R/info/%!S",zUuid))\
1074
+ @ <span class='timelineHash'>%S(zUuid)</span></a>
1075
+ }
1076
+ if( (tmFlags & TIMELINE_GRAPH)==0 && fossil_strcmp(zBrName,"trunk")!=0 ){
1077
+ @ branch:&nbsp;\
1078
+ @ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
1079
+ }
1080
+ }
1081
+ zNm = archive_base_name(rid);
1082
+ @ %z(href("%R/tarball/%s.tar.gz",zNm))\
1083
+ @ <button>Tarball</button></a>
1084
+ @ %z(href("%R/zip/%s.zip",zNm))\
1085
+ @ <button>ZIP&nbsp;Archive</button></a>
1086
+ fossil_free(zBrName);
1087
+ fossil_free(zNm);
1088
+ }
1089
+}
1090
+
1091
+/*
1092
+** SETTING: suggested-downloads width=70 block-text
1093
+**
1094
+** This setting controls the suggested tarball/ZIP downloads on the
1095
+** [[/download]] page. The value is a TCL list. Each set of four items
1096
+** defines a set of check-ins to be added to the suggestion list.
1097
+** The items in each group are:
1098
+**
1099
+** | COUNT TAG MAX_AGE COMMENT
1100
+**
1101
+** COUNT is the number of check-ins to match, starting with the most
1102
+** recent and working bacwards in time. Check-ins match if they contain
1103
+** the tag TAG. If MAX_AGE is not an empty string, then it specifies
1104
+** the maximum age of any matching check-in. COMMENT is an optional
1105
+** comment for each match.
1106
+**
1107
+** The special value of "OPEN-LEAF" for TAG matches any check-in that
1108
+** is an open leaf.
1109
+**
1110
+** MAX_AGE is of the form "{AMT UNITS}" where AMT is a floating point
1111
+** value and UNITS is one of "seconds", "hours", "days", "weeks", "months",
1112
+** or "years". If MAX_AGE is an empty string then there is no age limit.
1113
+**
1114
+** If COMMENT is not an empty string, then it is an additional comment
1115
+** added to the output description of the suggested download. The idea of
1116
+** COMMENT is to explain to the reader why a check-in is a suggested
1117
+** download.
1118
+**
1119
+** Example:
1120
+**
1121
+** | 1 trunk {} {Latest Trunk Check-in}
1122
+** | 5 OPEN-LEAF {1 month} {Active Branch}
1123
+** | 999 release {1 year} {Official Release}
1124
+**
1125
+** The value causes the /download page to show the union of the most
1126
+** recent trunk check-in of any age, the five most recent
1127
+** open leaves within the past month, and essentially
1128
+** all releases within the past year. If the same check-in matches more
1129
+** than one rule, the COMMENT of the first match is used.
1130
+*/
1131
+
1132
+/*
1133
+** WEBPAGE: /download
9921134
**
9931135
** Show a special no-graph timeline of recent important check-ins with
9941136
** an opportunity to pull tarballs and ZIPs.
9951137
*/
996
-void tarlist_page(void){
1138
+void download_page(void){
9971139
Stmt q; /* The actual timeline query */
9981140
const char *zTarlistCfg; /* Configuration string */
9991141
char **azItem; /* Decomposed elements of zTarlistCfg */
10001142
int *anItem; /* Bytes in each term of azItem[] */
10011143
int nItem; /* Number of terms in azItem[] */
10021144
int i; /* Loop counter */
10031145
int tmFlags; /* Timeline display flags */
10041146
int n; /* Number of suggested downloads */
1147
+ double rNow; /* Current time. Julian day number */
1148
+ int bPlainTextCom; /* Use plain-text comments */
10051149
10061150
login_check_credentials();
10071151
if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
10081152
10091153
style_set_current_feature("timeline");
1010
- style_header("Suggested Tarballs And ZIP Archives");
1154
+ style_header("Suggested Downloads");
10111155
1012
- zTarlistCfg = db_get("suggested-tarlist","1 trunk");
1156
+ zTarlistCfg = db_get("suggested-downloads","off");
10131157
db_multi_exec(
1014
- "CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY);"
1158
+ "CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY, com TEXT);"
10151159
);
1160
+ rNow = db_double(0.0,"SELECT julianday()");
10161161
if( !g.interp ) Th_FossilInit(0);
10171162
Th_SplitList(g.interp, zTarlistCfg, (int)strlen(zTarlistCfg),
10181163
&azItem, &anItem, &nItem);
1019
- for(i=0; i<nItem-1; i+=2){
1020
- int cnt;
1021
- char *zLabel;
1164
+ bPlainTextCom = db_get_boolean("timeline-plaintext",0);
1165
+ for(i=0; i<nItem-3; i+=4){
1166
+ int cnt; /* The number of instances of zLabel to use */
1167
+ char *zLabel; /* The label to match */
1168
+ double rStart; /* Starting time, julian day number */
1169
+ char *zComment = 0; /* Comment to apply */
10221170
if( anItem[i]==1 && azItem[i][0]=='*' ){
10231171
cnt = -1;
10241172
}else if( anItem[i]<1 ){
10251173
cnt = 0;
10261174
}else{
10271175
cnt = atoi(azItem[i]);
10281176
}
10291177
if( cnt==0 ) continue;
10301178
zLabel = fossil_strndup(azItem[i+1],anItem[i+1]);
1179
+ if( anItem[i+2]==0 ){
1180
+ rStart = 0.0;
1181
+ }else{
1182
+ char *zMax = fossil_strndup(azItem[i+2], anItem[i+2]);
1183
+ double r = atof(zMax);
1184
+ if( strstr(zMax,"sec") ){
1185
+ rStart = rNow - r/86400.0;
1186
+ }else
1187
+ if( strstr(zMax,"hou") ){
1188
+ rStart = rNow - r/24.0;
1189
+ }else
1190
+ if( strstr(zMax,"da") ){
1191
+ rStart = rNow - r;
1192
+ }else
1193
+ if( strstr(zMax,"wee") ){
1194
+ rStart = rNow - r*7.0;
1195
+ }else
1196
+ if( strstr(zMax,"mon") ){
1197
+ rStart = rNow - r*30.44;
1198
+ }else
1199
+ if( strstr(zMax,"yea") ){
1200
+ rStart = rNow - r*365.24;
1201
+ }else
1202
+ { /* Default to seconds */
1203
+ rStart = rNow - r/86400.0;
1204
+ }
1205
+ }
1206
+ if( anItem[i+3]==0 ){
1207
+ zComment = fossil_strdup("");
1208
+ }else if( bPlainTextCom ){
1209
+ zComment = mprintf("** %.*s ** ", anItem[i+3], azItem[i+3]);
1210
+ }else{
1211
+ zComment = mprintf("<b>%.*s</b>\n<p>", anItem[i+3], azItem[i+3]);
1212
+ }
10311213
if( fossil_strcmp("OPEN-LEAF",zLabel)==0 ){
10321214
db_multi_exec(
1033
- "INSERT OR IGNORE INTO tarlist(rid)"
1034
- " SELECT leaf.rid FROM leaf, event"
1215
+ "INSERT OR IGNORE INTO tarlist(rid,com)"
1216
+ " SELECT leaf.rid, %Q FROM leaf, event"
10351217
" WHERE event.objid=leaf.rid"
1218
+ " AND event.mtime>=%.6f"
10361219
" AND NOT EXISTS(SELECT 1 FROM tagxref"
10371220
" WHERE tagxref.rid=leaf.rid"
10381221
" AND tagid=%d AND tagtype>0)"
1039
- " ORDER BY event.mtime DESC LIMIT %d", TAG_CLOSED, cnt
1222
+ " ORDER BY event.mtime DESC LIMIT %d",
1223
+ zComment, rStart, TAG_CLOSED, cnt
10401224
);
10411225
}else{
10421226
db_multi_exec(
10431227
"WITH taglist(tid) AS"
10441228
" (SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q')"
1045
- "INSERT OR IGNORE INTO tarlist(rid)"
1046
- " SELECT event.objid FROM event CROSS JOIN tagxref"
1229
+ "INSERT OR IGNORE INTO tarlist(rid,com)"
1230
+ " SELECT event.objid, %Q FROM event CROSS JOIN tagxref"
10471231
" WHERE event.type='ci'"
1232
+ " AND event.mtime>=%.6f"
10481233
" AND tagxref.tagid IN taglist"
10491234
" AND tagtype>0"
10501235
" AND tagxref.rid=event.objid"
1051
- " ORDER BY event.mtime DESC LIMIT %d", zLabel, cnt
1236
+ " ORDER BY event.mtime DESC LIMIT %d",
1237
+ zLabel, zComment, rStart, cnt
10521238
);
10531239
}
10541240
fossil_free(zLabel);
1241
+ fossil_free(zComment);
10551242
}
10561243
Th_Free(g.interp, azItem);
10571244
10581245
n = db_int(0, "SELECT count(*) FROM tarlist");
10591246
if( n==0 ){
10601247
@ <h2>No tarball/ZIP suggestions are available at this time</h2>
10611248
}else{
1062
- @ <h2>%d(n) Tarball/ZIP Download Suggestions:</h2>
1249
+ @ <h2>%d(n) Tarball/ZIP Download Suggestion%s(n>1?"s":""):</h2>
10631250
db_prepare(&q,
1064
- "%s AND blob.rid IN tarlist ORDER BY event.mtime DESC",
1251
+ "WITH matches AS (%s AND blob.rid IN (SELECT rid FROM tarlist))\n"
1252
+ "SELECT blobRid, uuid, timestamp,"
1253
+ " com||comment,"
1254
+ " user, leaf, bgColor, eventType, tags, tagid, brief, mtime"
1255
+ " FROM matches JOIN tarlist ON tarlist.rid=blobRid"
1256
+ " ORDER BY matches.mtime DESC",
10651257
timeline_query_for_www()
10661258
);
10671259
10681260
tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL | TIMELINE_COLUMNAR
10691261
| TIMELINE_BRCOLOR;
1070
- www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, tarlist_extra);
1262
+ www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, download_extra);
10711263
db_finalize(&q);
10721264
}
1265
+ if( g.perm.Clone ){
1266
+ char *zNm = fossil_strdup(db_get("project-name","clone"));
1267
+ sanitize_name(zNm);
1268
+ @ <hr>
1269
+ @ <h2>You Can Clone This Repository</h2>
1270
+ @
1271
+ @ <p>Clone this repository by running a command similar to the following:
1272
+ @ <blockquote><pre>
1273
+ @ fossil clone %s(g.zBaseURL) %h(zNm).fossil
1274
+ @ </pre></blockquote>
1275
+ @ <p>A clone gives you local access to all historical content.
1276
+ @ Cloning is a bandwidth- and CPU-efficient alternative to extracting
1277
+ @ multiple tarballs and ZIPs.
1278
+ @ Do a web search for "fossil clone" or similar to find additional
1279
+ @ information about using a cloned Fossil repository. Or ask your
1280
+ @ favorite AI how to extract content from a Fossil clone.
1281
+ fossil_free(zNm);
1282
+ }
1283
+
1284
+ style_finish_page();
1285
+}
1286
+
1287
+/*
1288
+** WEBPAGE: rchvdwnld
1289
+**
1290
+** Short for "archive download". This page should have a single name=
1291
+** query parameter that is a check-in hash. It present a menu of possible
1292
+** download options for that check-in, including tarball, ZIP, or SQLAR.
1293
+**
1294
+** This is a utility page. The /dir and /tree pages sometimes have a
1295
+** "Download" option in their submenu which redirects here. Those pages
1296
+** used to have separate "Tarball" and "ZIP" submenu entries, but as
1297
+** submenu entries appear in alphabetical order, that caused the two
1298
+** submenu entries to be separated from one another, which is distracting.
1299
+*/
1300
+void rchvdwnld_page(void){
1301
+ const char *zUuid;
1302
+ char *zBase;
1303
+ int nUuid;
1304
+ int rid;
1305
+ login_check_credentials();
1306
+ if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
1307
+ if( robot_restrict("zip") || robot_restrict("download") ) return;
1308
+
1309
+ zUuid = P("name");
1310
+ if( zUuid==0
1311
+ || (nUuid = (int)strlen(zUuid))<6
1312
+ || !validate16(zUuid,-1)
1313
+ || (rid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUuid))==0
1314
+ || !db_exists("SELECT 1 from event WHERE type='ci' AND objid=%d",rid)
1315
+ ){
1316
+ fossil_redirect_home();
1317
+ }
1318
+ zUuid = db_text(zUuid, "SELECT uuid FROM blob WHERE rid=%d", rid);
1319
+ style_header("Downloads For Check-in %!S", zUuid);
1320
+ zBase = archive_base_name(rid);
1321
+ @ <div class="section accordion">Downloads for check-in \
1322
+ @ %z(href("%R/info/%!S",zUuid))%S(zUuid)</a></div>
1323
+ @ <div class="accordion_panel">
1324
+ @ <table class="label-value">
1325
+ @ <tr>
1326
+ @ <th>Tarball:</th>
1327
+ @ <td>%z(href("%R/tarball/%s.tar.gz",zBase))\
1328
+ @ %s(g.zBaseURL)/tarball/%s(zBase).tar.gz</a></td>
1329
+ @ </tr>
1330
+ @
1331
+ @ <tr>
1332
+ @ <th>ZIP:</th>
1333
+ @ <td>%z(href("%R/zip/%s.zip",zBase))\
1334
+ @ %s(g.zBaseURL)/zip/%s(zBase).zip</a></td>
1335
+ @ </tr>
1336
+ @
1337
+ @ <tr>
1338
+ @ <th>SQLAR:</th>
1339
+ @ <td>%z(href("%R/sqlar/%s.sqlar",zBase))\
1340
+ @ %s(g.zBaseURL)/sqlar/%s(zBase).sqlar</a></td>
1341
+ @ </tr>
1342
+ @ </table></div>
1343
+ fossil_free(zBase);
1344
+ @ <div class="section accordion">Context</div><div class="accordion_panel">
1345
+ render_checkin_context(rid, 0, 0, 0);
1346
+ @ </div>
10731347
style_finish_page();
10741348
}
10751349
--- src/tar.c
+++ src/tar.c
@@ -31,10 +31,65 @@
31 char *zPrevDir; /* Name of directory for previous entry */
32 int nPrevDirAlloc; /* size of zPrevDir */
33 Blob pax; /* PAX data */
34 } tball;
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
37 /*
38 ** field lengths of 'ustar' name and prefix fields.
39 */
40 #define USTAR_NAME_LEN 100
@@ -653,19 +708,11 @@
653 if( fossil_strcmp("/dev/null",zOut)==0 || fossil_strcmp("",zOut)==0 ){
654 zOut = 0;
655 }
656
657 if( zName==0 ){
658 zName = db_text("default-name",
659 "SELECT replace(%Q,' ','_') "
660 " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
661 " || substr(blob.uuid, 1, 10)"
662 " FROM event, blob"
663 " WHERE event.objid=%d"
664 " AND blob.rid=%d",
665 db_get("project-name", "unnamed"), rid, rid
666 );
667 }
668 tarball_of_checkin(rid, zOut ? &tarball : 0,
669 zName, pInclude, pExclude, listFlag);
670 glob_free(pInclude);
671 glob_free(pExclude);
@@ -673,27 +720,76 @@
673 if( zOut ){
674 blob_write_to_file(&tarball, zOut);
675 blob_reset(&tarball);
676 }
677 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
679 /*
680 ** Check to see if the input string is of one of the following
681 ** two the forms:
682 **
683 ** check-in-name/filename.ext (1)
684 ** tag-name/check-in-name/filename.txt (2)
 
685 **
686 ** In other words, check to see if the input string contains either
687 ** a check-in name or a tag-name and a check-in name separated by
688 ** a slash. There must be either 1 or 2 "/" characters. In the
689 ** second form, tag-name must be an individual tag (not a branch-tag)
690 ** that is found on the check-in identified by the check-in-name.
691 **
692 ** If the condition is true, then:
693 **
694 ** * Make *pzName point to the fielname suffix only
695 ** * return a copy of the check-in name in memory from mprintf().
696 **
697 ** If the condition is false, leave *pzName unchanged and return either
698 ** NULL or an empty string. Normally NULL is returned, however an
699 ** empty string is returned for format (2) if check-in-name does not
@@ -705,10 +801,19 @@
705 **
706 ** Such URLs will pass through most anti-robot filters because of the
707 ** "/tarball/release" prefix will match the suggested "robot-exception"
708 ** pattern and can still refer to an historic release rather than just
709 ** the most recent release.
 
 
 
 
 
 
 
 
 
710 */
711 char *tar_uuid_from_name(char **pzName){
712 char *zName = *pzName; /* Original input */
713 int n1 = 0; /* Bytes in first prefix (tag-name) */
714 int n2 = 0; /* Bytes in second prefix (check-in-name) */
@@ -724,11 +829,12 @@
724 return 0; /* More than two "/" characters seen */
725 }
726 }
727 }
728 if( n1==0 ){
729 return 0; /* No prefix of any kind */
 
730 }
731 if( zName[n+1]==0 ){
732 return 0; /* No filename suffix */
733 }
734 if( n2==0 ){
@@ -933,142 +1039,310 @@
933 cgi_set_content(&tarball);
934 cgi_set_content_type("application/x-compressed");
935 }
936
937 /*
938 ** This routine is called for each check-in on the /tarlist page to
939 ** construct the "extra" information after the description.
940 */
941 static void tarlist_extra(
942 Stmt *pQuery, /* Current row of the timeline query */
943 int tmFlags, /* Flags to www_print_timeline() */
944 const char *zThisUser, /* Suppress links to this user */
945 const char *zThisTag /* Suppress links to this tag */
946 ){
947 int rid = db_column_int(pQuery, 0);
948 const char *zUuid = db_column_text(pQuery, 1);
949 const char *zDate = db_column_text(pQuery, 2);
950 char *zBrName = branch_of_rid(rid);
951 static const char *zProject = 0;
952 int nProject;
953 char *zNm;
954
955 if( zProject==0 ) zProject = db_get("project-name","unnamed");
956 zNm = mprintf("%s-%sZ-%.8s", zProject, zDate, zUuid);
957 nProject = (int)strlen(zProject);
958 zNm[nProject+11] = 'T';
959 @ <strong>%h(zBrName)</strong><br>\
960 @ %z(href("%R/timeline?c=%!S&y=ci&n=11",zUuid))<button>Context</button></a>
961 @ %z(href("%R/tarball/%!S/%t.tar.gz",zUuid,zNm))\
962 @ <button>Tarball</button></a>
963 @ %z(href("%R/zip/%!S/%t.zip",zUuid,zNm))\
964 @ <button>ZIP&nbsp;Archive</button></a>
965 fossil_free(zBrName);
966 fossil_free(zNm);
967 }
968
969 /*
970 ** SETTING: suggested-tarlist width=70 block-text
971 **
972 ** This setting controls the suggested tarball/ZIP downloads on the
973 ** [[/tarlist]] page. The value is a TCL list. Each pair of items
974 ** defines a set of check-ins to be added to the suggestion list.
975 ** The first item of each pair is an integer count (N) and second
976 ** item is a tag GLOB pattern (PATTERN). For each pair, the most
977 ** recent N check-ins that have a tag matching PATTERN are added
978 ** to the list. The special pattern "OPEN-LEAF" matches any open
979 ** leaf check-in.
980 **
981 ** Example:
982 **
983 ** 3 OPEN-LEAF 3 release 1 trunk
984 **
985 ** The value causes the /tarlist page to show the union of the 3
986 ** most recent open leaves, the three most recent check-ins marked
987 ** "release", and the single most recent trunk check-in.
988 */
989
990 /*
991 ** WEBPAGE: /tarlist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
992 **
993 ** Show a special no-graph timeline of recent important check-ins with
994 ** an opportunity to pull tarballs and ZIPs.
995 */
996 void tarlist_page(void){
997 Stmt q; /* The actual timeline query */
998 const char *zTarlistCfg; /* Configuration string */
999 char **azItem; /* Decomposed elements of zTarlistCfg */
1000 int *anItem; /* Bytes in each term of azItem[] */
1001 int nItem; /* Number of terms in azItem[] */
1002 int i; /* Loop counter */
1003 int tmFlags; /* Timeline display flags */
1004 int n; /* Number of suggested downloads */
 
 
1005
1006 login_check_credentials();
1007 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
1008
1009 style_set_current_feature("timeline");
1010 style_header("Suggested Tarballs And ZIP Archives");
1011
1012 zTarlistCfg = db_get("suggested-tarlist","1 trunk");
1013 db_multi_exec(
1014 "CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY);"
1015 );
 
1016 if( !g.interp ) Th_FossilInit(0);
1017 Th_SplitList(g.interp, zTarlistCfg, (int)strlen(zTarlistCfg),
1018 &azItem, &anItem, &nItem);
1019 for(i=0; i<nItem-1; i+=2){
1020 int cnt;
1021 char *zLabel;
 
 
 
1022 if( anItem[i]==1 && azItem[i][0]=='*' ){
1023 cnt = -1;
1024 }else if( anItem[i]<1 ){
1025 cnt = 0;
1026 }else{
1027 cnt = atoi(azItem[i]);
1028 }
1029 if( cnt==0 ) continue;
1030 zLabel = fossil_strndup(azItem[i+1],anItem[i+1]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031 if( fossil_strcmp("OPEN-LEAF",zLabel)==0 ){
1032 db_multi_exec(
1033 "INSERT OR IGNORE INTO tarlist(rid)"
1034 " SELECT leaf.rid FROM leaf, event"
1035 " WHERE event.objid=leaf.rid"
 
1036 " AND NOT EXISTS(SELECT 1 FROM tagxref"
1037 " WHERE tagxref.rid=leaf.rid"
1038 " AND tagid=%d AND tagtype>0)"
1039 " ORDER BY event.mtime DESC LIMIT %d", TAG_CLOSED, cnt
 
1040 );
1041 }else{
1042 db_multi_exec(
1043 "WITH taglist(tid) AS"
1044 " (SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q')"
1045 "INSERT OR IGNORE INTO tarlist(rid)"
1046 " SELECT event.objid FROM event CROSS JOIN tagxref"
1047 " WHERE event.type='ci'"
 
1048 " AND tagxref.tagid IN taglist"
1049 " AND tagtype>0"
1050 " AND tagxref.rid=event.objid"
1051 " ORDER BY event.mtime DESC LIMIT %d", zLabel, cnt
 
1052 );
1053 }
1054 fossil_free(zLabel);
 
1055 }
1056 Th_Free(g.interp, azItem);
1057
1058 n = db_int(0, "SELECT count(*) FROM tarlist");
1059 if( n==0 ){
1060 @ <h2>No tarball/ZIP suggestions are available at this time</h2>
1061 }else{
1062 @ <h2>%d(n) Tarball/ZIP Download Suggestions:</h2>
1063 db_prepare(&q,
1064 "%s AND blob.rid IN tarlist ORDER BY event.mtime DESC",
 
 
 
 
 
1065 timeline_query_for_www()
1066 );
1067
1068 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL | TIMELINE_COLUMNAR
1069 | TIMELINE_BRCOLOR;
1070 www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, tarlist_extra);
1071 db_finalize(&q);
1072 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073 style_finish_page();
1074 }
1075
--- src/tar.c
+++ src/tar.c
@@ -31,10 +31,65 @@
31 char *zPrevDir; /* Name of directory for previous entry */
32 int nPrevDirAlloc; /* size of zPrevDir */
33 Blob pax; /* PAX data */
34 } tball;
35
36 /*
37 ** Convert a string so that it contains only lower-case ASCII, digits,
38 ** "_" and "-". Changes are made in-place.
39 */
40 static void sanitize_name(char *zName){
41 int i;
42 char c;
43 for(i=0; (c = zName[i])!=0; i++){
44 if( fossil_isupper(c) ){
45 zName[i] = fossil_tolower(c);
46 }else if( !fossil_isalnum(c) && c!='_' && c!='-' ){
47 if( c<=0x7f ){
48 zName[i] = '_';
49 }else{
50 /* 123456789 123456789 123456 */
51 zName[i] = "abcdefghijklmnopqrstuvwxyz"[(unsigned)c%26];
52 }
53 }
54 }
55 }
56
57 /*
58 ** Compute a sensible base-name for an archive file (tarball, ZIP, or SQLAR)
59 ** based on the rid of the check-in contained in that file.
60 **
61 ** PROJECTNAME-DATETIME-HASHPREFIX
62 **
63 ** So that the name will be safe to use as a URL or a filename on any system,
64 ** the name is only allowed to contain lower-case ASCII alphabetics,
65 ** digits, '_' and '-'. Upper-case ASCII is converted to lower-case. All
66 ** other bytes are mapped into a lower-case alphabetic.
67 **
68 ** The value returned is obtained from mprintf() or fossil_strdup() and should
69 ** be released by the caller using fossil_free().
70 */
71 char *archive_base_name(int rid){
72 char *zPrefix;
73 char *zName;
74 zPrefix = db_get("short-project-name",0);
75 if( zPrefix==0 || zPrefix[0]==0 ){
76 zPrefix = db_get("project-name","unnamed");
77 }
78 zName = db_text(0,
79 "SELECT %Q||"
80 " strftime('-%%Y%%m%%d%%H%%M%%S-',event.mtime)||"
81 " substr(blob.uuid,1,10)"
82 " FROM blob, event LEFT JOIN config"
83 " WHERE blob.rid=%d"
84 " AND event.objid=%d"
85 " AND config.name='project-name'",
86 zPrefix, rid, rid);
87 fossil_free(zPrefix);
88 sanitize_name(zName);
89 return zName;
90 }
91
92 /*
93 ** field lengths of 'ustar' name and prefix fields.
94 */
95 #define USTAR_NAME_LEN 100
@@ -653,19 +708,11 @@
708 if( fossil_strcmp("/dev/null",zOut)==0 || fossil_strcmp("",zOut)==0 ){
709 zOut = 0;
710 }
711
712 if( zName==0 ){
713 zName = archive_base_name(rid);
 
 
 
 
 
 
 
 
714 }
715 tarball_of_checkin(rid, zOut ? &tarball : 0,
716 zName, pInclude, pExclude, listFlag);
717 glob_free(pInclude);
718 glob_free(pExclude);
@@ -673,27 +720,76 @@
720 if( zOut ){
721 blob_write_to_file(&tarball, zOut);
722 blob_reset(&tarball);
723 }
724 }
725
726 /*
727 ** This is a helper routine for tar_uuid_from_name(). It handles
728 ** the case where *pzName contains no "/" character. Check for
729 ** format (3). Return the hash if the name matches format (3),
730 ** or return NULL if it does not.
731 */
732 static char *format_three_parser(const char *zName){
733 int iDot = 0; /* Index in zName[] of the first '.' */
734 int iDash1 = 0; /* Index in zName[] of the '-' before the timestamp */
735 int iDash2 = 0; /* Index in zName[] of the '-' between timestamp and hash */
736 int nHash; /* Size of the hash */
737 char *zHash; /* A copy of the hash value */
738 char *zDate; /* Copy of the timestamp */
739 char *zUuid; /* Final result */
740 int i; /* Loop query */
741 Stmt q; /* Query to verify that hash and timestamp agree */
742
743 for(i=0; zName[i]; i++){
744 char c = zName[i];
745 if( c=='.' ){ iDot = i; break; }
746 if( c=='-' ){ iDash1 = iDash2; iDash2 = i; }
747 if( !fossil_isalnum(c) && c!='_' && c!='-' ){ break; }
748 }
749 if( iDot==0 ) return 0;
750 if( iDash1==0 ) return 0;
751 nHash = iDot - iDash2 - 1;
752 if( nHash<8 ) return 0; /* HASH value too short */
753 if( (iDash2 - iDash1)!=15 ) return 0; /* Wrong timestamp size */
754 zHash = fossil_strndup(&zName[iDash2+1], nHash);
755 zDate = fossil_strndup(&zName[iDash1+1], 14);
756 db_prepare(&q,
757 "SELECT blob.uuid"
758 " FROM blob JOIN event ON event.objid=blob.rid"
759 " WHERE blob.uuid GLOB '%q*'"
760 " AND strftime('%%Y%%m%%d%%H%%M%%S',event.mtime)='%q'",
761 zHash, zDate
762 );
763 fossil_free(zHash);
764 fossil_free(zDate);
765 if( db_step(&q)==SQLITE_ROW ){
766 zUuid = fossil_strdup(db_column_text(&q,0));
767 }else{
768 zUuid = 0;
769 }
770 db_finalize(&q);
771 return zUuid;
772 }
773
774 /*
775 ** Check to see if the input string is of one of the following
776 ** two the forms:
777 **
778 ** check-in-name/filename.ext (1)
779 ** tag-name/check-in-name/filename.ext (2)
780 ** project-datetime-hash.ext (3)
781 **
782 ** In other words, check to see if the input string contains either
783 ** a check-in name or a tag-name and a check-in name separated by
784 ** a slash. There must be between 0 or 2 "/" characters. In the
785 ** second form, tag-name must be an individual tag (not a branch-tag)
786 ** that is found on the check-in identified by the check-in-name.
787 **
788 ** If the condition is true, then:
789 **
790 ** * Make *pzName point to the filename suffix only
791 ** * return a copy of the check-in name in memory from mprintf().
792 **
793 ** If the condition is false, leave *pzName unchanged and return either
794 ** NULL or an empty string. Normally NULL is returned, however an
795 ** empty string is returned for format (2) if check-in-name does not
@@ -705,10 +801,19 @@
801 **
802 ** Such URLs will pass through most anti-robot filters because of the
803 ** "/tarball/release" prefix will match the suggested "robot-exception"
804 ** pattern and can still refer to an historic release rather than just
805 ** the most recent release.
806 **
807 ** Format (3) is designed to allow URLs like this:
808 **
809 ** /tarball/fossil-20251018193920-d6c9aee97df.tar.gz
810 **
811 ** In other words, filename itself contains sufficient information to
812 ** uniquely identify the check-in, including a timestamp of the form
813 ** YYYYMMDDHHMMSS and a prefix of the check-in hash. The timestamp
814 ** and hash must immediately preceed the first "." in the name.
815 */
816 char *tar_uuid_from_name(char **pzName){
817 char *zName = *pzName; /* Original input */
818 int n1 = 0; /* Bytes in first prefix (tag-name) */
819 int n2 = 0; /* Bytes in second prefix (check-in-name) */
@@ -724,11 +829,12 @@
829 return 0; /* More than two "/" characters seen */
830 }
831 }
832 }
833 if( n1==0 ){
834 /* Check for format (3) */
835 return format_three_parser(*pzName);
836 }
837 if( zName[n+1]==0 ){
838 return 0; /* No filename suffix */
839 }
840 if( n2==0 ){
@@ -933,142 +1039,310 @@
1039 cgi_set_content(&tarball);
1040 cgi_set_content_type("application/x-compressed");
1041 }
1042
1043 /*
1044 ** This routine is called for each check-in on the /download page to
1045 ** construct the "extra" information after the description.
1046 */
1047 void download_extra(
1048 Stmt *pQuery, /* Current row of the timeline query */
1049 int tmFlags, /* Flags to www_print_timeline() */
1050 const char *zThisUser, /* Suppress links to this user */
1051 const char *zThisTag /* Suppress links to this tag */
1052 ){
1053 const char *zType = db_column_text(pQuery, 7);
1054 assert( zType!=0 );
1055 if( zType[0]!='c' ){
1056 timeline_extra(pQuery, tmFlags, zThisUser, zThisTag);
1057 }else{
1058 int rid = db_column_int(pQuery, 0);
1059 const char *zUuid = db_column_text(pQuery, 1);
1060 char *zBrName = branch_of_rid(rid);
1061 char *zNm;
1062
1063 if( tmFlags & TIMELINE_COLUMNAR ){
1064 @ <nobr>check-in:&nbsp;\
1065 @ %z(href("%R/info/%!S",zUuid))<span class='timelineHash'>\
1066 @ %S(zUuid)</span></a></nobr><br>
1067 if( fossil_strcmp(zBrName,"trunk")!=0 ){
1068 @ <nobr>branch:&nbsp;\
1069 @ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a></nobr><br>\
1070 }
1071 }else{
1072 if( (tmFlags & TIMELINE_CLASSIC)==0 ){
1073 @ check-in:&nbsp;%z(href("%R/info/%!S",zUuid))\
1074 @ <span class='timelineHash'>%S(zUuid)</span></a>
1075 }
1076 if( (tmFlags & TIMELINE_GRAPH)==0 && fossil_strcmp(zBrName,"trunk")!=0 ){
1077 @ branch:&nbsp;\
1078 @ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
1079 }
1080 }
1081 zNm = archive_base_name(rid);
1082 @ %z(href("%R/tarball/%s.tar.gz",zNm))\
1083 @ <button>Tarball</button></a>
1084 @ %z(href("%R/zip/%s.zip",zNm))\
1085 @ <button>ZIP&nbsp;Archive</button></a>
1086 fossil_free(zBrName);
1087 fossil_free(zNm);
1088 }
1089 }
1090
1091 /*
1092 ** SETTING: suggested-downloads width=70 block-text
1093 **
1094 ** This setting controls the suggested tarball/ZIP downloads on the
1095 ** [[/download]] page. The value is a TCL list. Each set of four items
1096 ** defines a set of check-ins to be added to the suggestion list.
1097 ** The items in each group are:
1098 **
1099 ** | COUNT TAG MAX_AGE COMMENT
1100 **
1101 ** COUNT is the number of check-ins to match, starting with the most
1102 ** recent and working bacwards in time. Check-ins match if they contain
1103 ** the tag TAG. If MAX_AGE is not an empty string, then it specifies
1104 ** the maximum age of any matching check-in. COMMENT is an optional
1105 ** comment for each match.
1106 **
1107 ** The special value of "OPEN-LEAF" for TAG matches any check-in that
1108 ** is an open leaf.
1109 **
1110 ** MAX_AGE is of the form "{AMT UNITS}" where AMT is a floating point
1111 ** value and UNITS is one of "seconds", "hours", "days", "weeks", "months",
1112 ** or "years". If MAX_AGE is an empty string then there is no age limit.
1113 **
1114 ** If COMMENT is not an empty string, then it is an additional comment
1115 ** added to the output description of the suggested download. The idea of
1116 ** COMMENT is to explain to the reader why a check-in is a suggested
1117 ** download.
1118 **
1119 ** Example:
1120 **
1121 ** | 1 trunk {} {Latest Trunk Check-in}
1122 ** | 5 OPEN-LEAF {1 month} {Active Branch}
1123 ** | 999 release {1 year} {Official Release}
1124 **
1125 ** The value causes the /download page to show the union of the most
1126 ** recent trunk check-in of any age, the five most recent
1127 ** open leaves within the past month, and essentially
1128 ** all releases within the past year. If the same check-in matches more
1129 ** than one rule, the COMMENT of the first match is used.
1130 */
1131
1132 /*
1133 ** WEBPAGE: /download
1134 **
1135 ** Show a special no-graph timeline of recent important check-ins with
1136 ** an opportunity to pull tarballs and ZIPs.
1137 */
1138 void download_page(void){
1139 Stmt q; /* The actual timeline query */
1140 const char *zTarlistCfg; /* Configuration string */
1141 char **azItem; /* Decomposed elements of zTarlistCfg */
1142 int *anItem; /* Bytes in each term of azItem[] */
1143 int nItem; /* Number of terms in azItem[] */
1144 int i; /* Loop counter */
1145 int tmFlags; /* Timeline display flags */
1146 int n; /* Number of suggested downloads */
1147 double rNow; /* Current time. Julian day number */
1148 int bPlainTextCom; /* Use plain-text comments */
1149
1150 login_check_credentials();
1151 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
1152
1153 style_set_current_feature("timeline");
1154 style_header("Suggested Downloads");
1155
1156 zTarlistCfg = db_get("suggested-downloads","off");
1157 db_multi_exec(
1158 "CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY, com TEXT);"
1159 );
1160 rNow = db_double(0.0,"SELECT julianday()");
1161 if( !g.interp ) Th_FossilInit(0);
1162 Th_SplitList(g.interp, zTarlistCfg, (int)strlen(zTarlistCfg),
1163 &azItem, &anItem, &nItem);
1164 bPlainTextCom = db_get_boolean("timeline-plaintext",0);
1165 for(i=0; i<nItem-3; i+=4){
1166 int cnt; /* The number of instances of zLabel to use */
1167 char *zLabel; /* The label to match */
1168 double rStart; /* Starting time, julian day number */
1169 char *zComment = 0; /* Comment to apply */
1170 if( anItem[i]==1 && azItem[i][0]=='*' ){
1171 cnt = -1;
1172 }else if( anItem[i]<1 ){
1173 cnt = 0;
1174 }else{
1175 cnt = atoi(azItem[i]);
1176 }
1177 if( cnt==0 ) continue;
1178 zLabel = fossil_strndup(azItem[i+1],anItem[i+1]);
1179 if( anItem[i+2]==0 ){
1180 rStart = 0.0;
1181 }else{
1182 char *zMax = fossil_strndup(azItem[i+2], anItem[i+2]);
1183 double r = atof(zMax);
1184 if( strstr(zMax,"sec") ){
1185 rStart = rNow - r/86400.0;
1186 }else
1187 if( strstr(zMax,"hou") ){
1188 rStart = rNow - r/24.0;
1189 }else
1190 if( strstr(zMax,"da") ){
1191 rStart = rNow - r;
1192 }else
1193 if( strstr(zMax,"wee") ){
1194 rStart = rNow - r*7.0;
1195 }else
1196 if( strstr(zMax,"mon") ){
1197 rStart = rNow - r*30.44;
1198 }else
1199 if( strstr(zMax,"yea") ){
1200 rStart = rNow - r*365.24;
1201 }else
1202 { /* Default to seconds */
1203 rStart = rNow - r/86400.0;
1204 }
1205 }
1206 if( anItem[i+3]==0 ){
1207 zComment = fossil_strdup("");
1208 }else if( bPlainTextCom ){
1209 zComment = mprintf("** %.*s ** ", anItem[i+3], azItem[i+3]);
1210 }else{
1211 zComment = mprintf("<b>%.*s</b>\n<p>", anItem[i+3], azItem[i+3]);
1212 }
1213 if( fossil_strcmp("OPEN-LEAF",zLabel)==0 ){
1214 db_multi_exec(
1215 "INSERT OR IGNORE INTO tarlist(rid,com)"
1216 " SELECT leaf.rid, %Q FROM leaf, event"
1217 " WHERE event.objid=leaf.rid"
1218 " AND event.mtime>=%.6f"
1219 " AND NOT EXISTS(SELECT 1 FROM tagxref"
1220 " WHERE tagxref.rid=leaf.rid"
1221 " AND tagid=%d AND tagtype>0)"
1222 " ORDER BY event.mtime DESC LIMIT %d",
1223 zComment, rStart, TAG_CLOSED, cnt
1224 );
1225 }else{
1226 db_multi_exec(
1227 "WITH taglist(tid) AS"
1228 " (SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q')"
1229 "INSERT OR IGNORE INTO tarlist(rid,com)"
1230 " SELECT event.objid, %Q FROM event CROSS JOIN tagxref"
1231 " WHERE event.type='ci'"
1232 " AND event.mtime>=%.6f"
1233 " AND tagxref.tagid IN taglist"
1234 " AND tagtype>0"
1235 " AND tagxref.rid=event.objid"
1236 " ORDER BY event.mtime DESC LIMIT %d",
1237 zLabel, zComment, rStart, cnt
1238 );
1239 }
1240 fossil_free(zLabel);
1241 fossil_free(zComment);
1242 }
1243 Th_Free(g.interp, azItem);
1244
1245 n = db_int(0, "SELECT count(*) FROM tarlist");
1246 if( n==0 ){
1247 @ <h2>No tarball/ZIP suggestions are available at this time</h2>
1248 }else{
1249 @ <h2>%d(n) Tarball/ZIP Download Suggestion%s(n>1?"s":""):</h2>
1250 db_prepare(&q,
1251 "WITH matches AS (%s AND blob.rid IN (SELECT rid FROM tarlist))\n"
1252 "SELECT blobRid, uuid, timestamp,"
1253 " com||comment,"
1254 " user, leaf, bgColor, eventType, tags, tagid, brief, mtime"
1255 " FROM matches JOIN tarlist ON tarlist.rid=blobRid"
1256 " ORDER BY matches.mtime DESC",
1257 timeline_query_for_www()
1258 );
1259
1260 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL | TIMELINE_COLUMNAR
1261 | TIMELINE_BRCOLOR;
1262 www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, download_extra);
1263 db_finalize(&q);
1264 }
1265 if( g.perm.Clone ){
1266 char *zNm = fossil_strdup(db_get("project-name","clone"));
1267 sanitize_name(zNm);
1268 @ <hr>
1269 @ <h2>You Can Clone This Repository</h2>
1270 @
1271 @ <p>Clone this repository by running a command similar to the following:
1272 @ <blockquote><pre>
1273 @ fossil clone %s(g.zBaseURL) %h(zNm).fossil
1274 @ </pre></blockquote>
1275 @ <p>A clone gives you local access to all historical content.
1276 @ Cloning is a bandwidth- and CPU-efficient alternative to extracting
1277 @ multiple tarballs and ZIPs.
1278 @ Do a web search for "fossil clone" or similar to find additional
1279 @ information about using a cloned Fossil repository. Or ask your
1280 @ favorite AI how to extract content from a Fossil clone.
1281 fossil_free(zNm);
1282 }
1283
1284 style_finish_page();
1285 }
1286
1287 /*
1288 ** WEBPAGE: rchvdwnld
1289 **
1290 ** Short for "archive download". This page should have a single name=
1291 ** query parameter that is a check-in hash. It present a menu of possible
1292 ** download options for that check-in, including tarball, ZIP, or SQLAR.
1293 **
1294 ** This is a utility page. The /dir and /tree pages sometimes have a
1295 ** "Download" option in their submenu which redirects here. Those pages
1296 ** used to have separate "Tarball" and "ZIP" submenu entries, but as
1297 ** submenu entries appear in alphabetical order, that caused the two
1298 ** submenu entries to be separated from one another, which is distracting.
1299 */
1300 void rchvdwnld_page(void){
1301 const char *zUuid;
1302 char *zBase;
1303 int nUuid;
1304 int rid;
1305 login_check_credentials();
1306 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
1307 if( robot_restrict("zip") || robot_restrict("download") ) return;
1308
1309 zUuid = P("name");
1310 if( zUuid==0
1311 || (nUuid = (int)strlen(zUuid))<6
1312 || !validate16(zUuid,-1)
1313 || (rid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUuid))==0
1314 || !db_exists("SELECT 1 from event WHERE type='ci' AND objid=%d",rid)
1315 ){
1316 fossil_redirect_home();
1317 }
1318 zUuid = db_text(zUuid, "SELECT uuid FROM blob WHERE rid=%d", rid);
1319 style_header("Downloads For Check-in %!S", zUuid);
1320 zBase = archive_base_name(rid);
1321 @ <div class="section accordion">Downloads for check-in \
1322 @ %z(href("%R/info/%!S",zUuid))%S(zUuid)</a></div>
1323 @ <div class="accordion_panel">
1324 @ <table class="label-value">
1325 @ <tr>
1326 @ <th>Tarball:</th>
1327 @ <td>%z(href("%R/tarball/%s.tar.gz",zBase))\
1328 @ %s(g.zBaseURL)/tarball/%s(zBase).tar.gz</a></td>
1329 @ </tr>
1330 @
1331 @ <tr>
1332 @ <th>ZIP:</th>
1333 @ <td>%z(href("%R/zip/%s.zip",zBase))\
1334 @ %s(g.zBaseURL)/zip/%s(zBase).zip</a></td>
1335 @ </tr>
1336 @
1337 @ <tr>
1338 @ <th>SQLAR:</th>
1339 @ <td>%z(href("%R/sqlar/%s.sqlar",zBase))\
1340 @ %s(g.zBaseURL)/sqlar/%s(zBase).sqlar</a></td>
1341 @ </tr>
1342 @ </table></div>
1343 fossil_free(zBase);
1344 @ <div class="section accordion">Context</div><div class="accordion_panel">
1345 render_checkin_context(rid, 0, 0, 0);
1346 @ </div>
1347 style_finish_page();
1348 }
1349
+79 -46
--- src/timeline.c
+++ src/timeline.c
@@ -115,11 +115,13 @@
115115
#define TIMELINE_COMPACT 0x0001000 /* Use the "compact" view style */
116116
#define TIMELINE_VERBOSE 0x0002000 /* Use the "detailed" view style */
117117
#define TIMELINE_MODERN 0x0004000 /* Use the "modern" view style */
118118
#define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */
119119
#define TIMELINE_CLASSIC 0x0010000 /* Use the "classic" view style */
120
-#define TIMELINE_VIEWS 0x001f000 /* Mask for all of the view styles */
120
+#define TIMELINE_SIMPLE 0x0020000 /* Use the "simple" view style */
121
+#define TIMELINE_INLINE 0x0033000 /* Mask for views with in-line display */
122
+#define TIMELINE_VIEWS 0x003f000 /* Mask for all of the view styles */
121123
#define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */
122124
#define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */
123125
#define TIMELINE_CHPICK 0x0400000 /* Show cherrypick merges */
124126
#define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */
125127
#define TIMELINE_XMERGE 0x1000000 /* Omit merges from off-graph nodes */
@@ -179,11 +181,11 @@
179181
** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
180182
**
181183
** This routine is used if no xExtra argument is supplied to
182184
** www_print_timeline().
183185
*/
184
-static void defaultExtra(
186
+void timeline_extra(
185187
Stmt *pQuery, /* Current row of the timeline query */
186188
int tmFlags, /* Flags to www_print_timeline() */
187189
const char *zThisUser, /* Suppress links to this user */
188190
const char *zThisTag /* Suppress links to this tag */
189191
){
@@ -194,29 +196,40 @@
194196
const char *zUser = db_column_text(pQuery, 4);
195197
const char *zTagList = db_column_text(pQuery, 8);
196198
int tagid = db_column_int(pQuery, 9);
197199
const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
198200
199
- if( (tmFlags & (TIMELINE_CLASSIC|TIMELINE_VERBOSE|TIMELINE_COMPACT))!=0 ){
201
+ if( (tmFlags & TIMELINE_INLINE)!=0 ){
200202
cgi_printf("(");
201203
}
202204
203
-/* Set to 1 for historical appearance. Set to 0 for new experimental look */
204
-#define OLD_STYLE 1
205
-#if OLD_STYLE
206205
if( (tmFlags & TIMELINE_CLASSIC)==0 ){
207206
if( zType[0]=='c' ){
208
- int isLeaf = db_column_int(pQuery, 5);
209
- if( isLeaf ){
210
- if( has_closed_tag(rid) ){
211
- @ <span class='timelineLeaf'>Closed-Leaf</span>
207
+ const char *zPrefix = 0;
208
+ static int markLeaves = -1;
209
+ if( markLeaves<0 ){
210
+ markLeaves = db_get_int("timeline-mark-leaves",1);
211
+ if( markLeaves<0 ) markLeaves = 1;
212
+ }
213
+ if( strcmp(zUuid, MANIFEST_UUID)==0 ){
214
+ /* This will only ever happen when Fossil is drawing a timeline for
215
+ ** its own self-host repository. If the timeline shows the specific
216
+ ** check-in corresponding to the current executable, then tag that
217
+ ** check-in with "self" */
218
+ zPrefix = "self&nbsp;";
219
+ }else if( markLeaves && db_column_int(pQuery,5) ){
220
+ if( markLeaves==1 ){
221
+ zPrefix = has_closed_tag(rid) ? "closed&nbsp;" : "leaf&nbsp;";
212222
}else{
213
- @ <span class='timelineLeaf'>Leaf</span>
223
+ zPrefix = has_closed_tag(rid) ?
224
+ "<span class='timelineLeaf'>Closed-Leaf</span>\n" :
225
+ "<span class='timelineLeaf'>Leaf</span>\n";
214226
}
215227
}
216
- cgi_printf("check-in:&nbsp;%z%S</a> ",
217
- href("%R/info/%!S",zUuid),zUuid);
228
+ cgi_printf("%scheck-in:&nbsp;%z<span class='timelineHash'>"
229
+ "%S</span></a> ",
230
+ zPrefix, href("%R/info/%!S",zUuid),zUuid);
218231
}else if( zType[0]=='e' && tagid ){
219232
cgi_printf("technote:&nbsp;");
220233
hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
221234
}else{
222235
cgi_printf("artifact:&nbsp;%z%S</a> ",
@@ -224,11 +237,16 @@
224237
}
225238
}else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t'
226239
|| zType[0]=='n' || zType[0]=='f'){
227240
cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
228241
}
229
-#endif /* OLD_STYLE */
242
+
243
+ if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
244
+ @ <span class='timelineEllipsis' id='ellipsis-%d(rid)' \
245
+ @ data-id='%d(rid)'>...</span>
246
+ @ <span class='clutter' id='detail-%d(rid)'>
247
+ }
230248
231249
if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
232250
char *zLink;
233251
if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){
234252
zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate);
@@ -280,33 +298,18 @@
280298
href("%R/deltachain/%d",rid), rid);
281299
}
282300
}
283301
tag_private_status(rid);
284302
285
-#if !OLD_STYLE
286
- if( (tmFlags & TIMELINE_CLASSIC)==0 ){
287
- if( zType[0]=='e' && tagid ){
288
- char *zId = db_text(0,
289
- "SELECT substr(tagname,7) FROM tag WHERE tagid=abs(%d)", tagid);
290
- cgi_printf(" technote:&nbsp;%z%S</a>",
291
- href("%R/technote/%t",zId), zId);
292
- }else{
293
- cgi_printf(" hash:&nbsp;%z%S</a>", href("%R/info/%!S", zUuid), zUuid);
294
- }
295
- }
296
-#endif /* !OLD_STYLE */
303
+ if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
304
+ cgi_printf("</span>"); /* End of the declutter span */
305
+ }
297306
298307
/* End timelineDetail */
299
- if( (tmFlags & (TIMELINE_CLASSIC|TIMELINE_VERBOSE|TIMELINE_COMPACT))!=0 ){
308
+ if( (tmFlags & TIMELINE_INLINE)!=0 ){
300309
cgi_printf(")");
301310
}
302
-
303
- if( tmFlags & TIMELINE_COMPACT ){
304
- @ </span></span>
305
- }else{
306
- @ </span>
307
- }
308311
}
309312
310313
311314
/*
312315
** SETTING: timeline-truncate-at-blank boolean default=off
@@ -323,11 +326,27 @@
323326
** on the far left-hand side of the screen, normally targets another
324327
** /timeline page that shows the entry in context. However, if this
325328
** option is turned on, that hyperlink targets the /info page showing
326329
** the details of the entry.
327330
*/
328
-
331
+/*
332
+** SETTING: timeline-mark-leaves width=5 default=1
333
+**
334
+** Determine whether or not leaf check-ins are marked as such in the
335
+** details section of the timeline. The value is an integer between 0
336
+** and 2:
337
+**
338
+** 0 Do not show any special marking for leaf check-ins.
339
+**
340
+** 1 Show just "leaf" or "closed"
341
+**
342
+** 2 Show "Leaf" or "Closed-Leaf" with emphasis
343
+**
344
+** The default is currently 1. Prior to 2025-10-19, the default was 2.
345
+** This setting has no effect on the "Classic" view, which always behaves
346
+** as if the setting were 2.
347
+*/
329348
330349
/*
331350
** Output a timeline in the web format given a query. The query
332351
** should return these columns:
333352
**
@@ -375,11 +394,11 @@
375394
376395
377396
if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
378397
vid = db_lget_int("checkout", 0);
379398
}
380
- if( xExtra==0 ) xExtra = defaultExtra;
399
+ if( xExtra==0 ) xExtra = timeline_extra;
381400
zPrevDate[0] = 0;
382401
mxWikiLen = db_get_int("timeline-max-comment", 0);
383402
dateFormat = db_get_int("timeline-date-format", 0);
384403
bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
385404
bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
@@ -388,10 +407,12 @@
388407
}
389408
if( tmFlags & TIMELINE_COLUMNAR ){
390409
zStyle = "Columnar";
391410
}else if( tmFlags & TIMELINE_COMPACT ){
392411
zStyle = "Compact";
412
+ }else if( tmFlags & TIMELINE_SIMPLE ){
413
+ zStyle = "Simple";
393414
}else if( tmFlags & TIMELINE_VERBOSE ){
394415
zStyle = "Verbose";
395416
}else if( tmFlags & TIMELINE_CLASSIC ){
396417
zStyle = "Classic";
397418
}else{
@@ -738,18 +759,10 @@
738759
}else{
739760
cgi_printf("%W",blob_str(&comment));
740761
}
741762
}
742763
743
- if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){
744
- /* This will only ever happen when Fossil is drawing a timeline for
745
- ** its own self-host repository. If the timeline shows the specific
746
- ** check-in corresponding to the current executable, then tag that
747
- ** check-in with "This is me!". */
748
- @ <b>&larr; This is me!</b>
749
- }
750
-
751764
@ </span>
752765
blob_reset(&comment);
753766
754767
/* Generate extra information and hyperlinks that follow the comment.
755768
** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -1269,10 +1282,26 @@
12691282
if( i>2 ){
12701283
style_submenu_multichoice("y", i/2, az, isDisabled);
12711284
}
12721285
}
12731286
1287
+/*
1288
+** SETTING: timeline-default-style width=5 default=m
1289
+**
1290
+** This setting determines the default "view style" for timelines.
1291
+** The setting should be a single character, one of the following:
1292
+**
1293
+** c Compact
1294
+** j Columnar
1295
+** m Modern
1296
+** s Simple
1297
+** v Verbose
1298
+** x Classic
1299
+**
1300
+** The default value is m (Modern).
1301
+*/
1302
+
12741303
/*
12751304
** Return the default value for the "ss" cookie or query parameter.
12761305
** The "ss" cookie determines the graph style. See the
12771306
** timeline_view_styles[] global constant for a list of choices.
12781307
*/
@@ -1293,10 +1322,11 @@
12931322
switch( v[0] ){
12941323
case 'c': tmFlags = TIMELINE_COMPACT; break;
12951324
case 'v': tmFlags = TIMELINE_VERBOSE; break;
12961325
case 'j': tmFlags = TIMELINE_COLUMNAR; break;
12971326
case 'x': tmFlags = TIMELINE_CLASSIC; break;
1327
+ case 's': tmFlags = TIMELINE_SIMPLE; break;
12981328
default: tmFlags = TIMELINE_MODERN; break;
12991329
}
13001330
return tmFlags;
13011331
}
13021332
@@ -1305,15 +1335,16 @@
13051335
*/
13061336
const char *const timeline_view_styles[] = {
13071337
"m", "Modern View",
13081338
"j", "Columnar View",
13091339
"c", "Compact View",
1340
+ "s", "Simple View",
13101341
"v", "Verbose View",
13111342
"x", "Classic View",
13121343
};
13131344
#if INTERFACE
1314
-# define N_TIMELINE_VIEW_STYLE 5
1345
+# define N_TIMELINE_VIEW_STYLE 6
13151346
#endif
13161347
13171348
/*
13181349
** Add the select/option box to the timeline submenu that is used to
13191350
** set the ss= parameter that determines the viewing mode.
@@ -1670,11 +1701,11 @@
16701701
** ms=MATCHSTYLE Set tag name match algorithm. One of "exact", "glob",
16711702
** "like", or "regexp".
16721703
** u=USER Only show items associated with USER
16731704
** y=TYPE 'ci', 'w', 't', 'n', 'e', 'f', or 'all'.
16741705
** ss=VIEWSTYLE c: "Compact", v: "Verbose", m: "Modern", j: "Columnar",
1675
-* x: "Classic".
1706
+** x: "Classic".
16761707
** advm Use the "Advanced" or "Busy" menu design.
16771708
** ng No Graph.
16781709
** ncp Omit cherrypick merges
16791710
** nd Do not highlight the focus check-in
16801711
** nsm Omit the submenu
@@ -1893,11 +1924,13 @@
18931924
|| (bisectLocal && !g.perm.Setup)
18941925
){
18951926
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
18961927
return;
18971928
}
1898
- if( (zBefore || zCirca) && robot_restrict("timelineX") ) return;
1929
+ if( zBefore || zCirca ){
1930
+ if( robot_restrict("timelineX") ) return;
1931
+ }
18991932
if( !bisectLocal ){
19001933
etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0);
19011934
}
19021935
cookie_read_parameter("y","y");
19031936
zType = P("y");
19041937
--- src/timeline.c
+++ src/timeline.c
@@ -115,11 +115,13 @@
115 #define TIMELINE_COMPACT 0x0001000 /* Use the "compact" view style */
116 #define TIMELINE_VERBOSE 0x0002000 /* Use the "detailed" view style */
117 #define TIMELINE_MODERN 0x0004000 /* Use the "modern" view style */
118 #define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */
119 #define TIMELINE_CLASSIC 0x0010000 /* Use the "classic" view style */
120 #define TIMELINE_VIEWS 0x001f000 /* Mask for all of the view styles */
 
 
121 #define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */
122 #define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */
123 #define TIMELINE_CHPICK 0x0400000 /* Show cherrypick merges */
124 #define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */
125 #define TIMELINE_XMERGE 0x1000000 /* Omit merges from off-graph nodes */
@@ -179,11 +181,11 @@
179 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
180 **
181 ** This routine is used if no xExtra argument is supplied to
182 ** www_print_timeline().
183 */
184 static void defaultExtra(
185 Stmt *pQuery, /* Current row of the timeline query */
186 int tmFlags, /* Flags to www_print_timeline() */
187 const char *zThisUser, /* Suppress links to this user */
188 const char *zThisTag /* Suppress links to this tag */
189 ){
@@ -194,29 +196,40 @@
194 const char *zUser = db_column_text(pQuery, 4);
195 const char *zTagList = db_column_text(pQuery, 8);
196 int tagid = db_column_int(pQuery, 9);
197 const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
198
199 if( (tmFlags & (TIMELINE_CLASSIC|TIMELINE_VERBOSE|TIMELINE_COMPACT))!=0 ){
200 cgi_printf("(");
201 }
202
203 /* Set to 1 for historical appearance. Set to 0 for new experimental look */
204 #define OLD_STYLE 1
205 #if OLD_STYLE
206 if( (tmFlags & TIMELINE_CLASSIC)==0 ){
207 if( zType[0]=='c' ){
208 int isLeaf = db_column_int(pQuery, 5);
209 if( isLeaf ){
210 if( has_closed_tag(rid) ){
211 @ <span class='timelineLeaf'>Closed-Leaf</span>
 
 
 
 
 
 
 
 
 
 
 
212 }else{
213 @ <span class='timelineLeaf'>Leaf</span>
 
 
214 }
215 }
216 cgi_printf("check-in:&nbsp;%z%S</a> ",
217 href("%R/info/%!S",zUuid),zUuid);
 
218 }else if( zType[0]=='e' && tagid ){
219 cgi_printf("technote:&nbsp;");
220 hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
221 }else{
222 cgi_printf("artifact:&nbsp;%z%S</a> ",
@@ -224,11 +237,16 @@
224 }
225 }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t'
226 || zType[0]=='n' || zType[0]=='f'){
227 cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
228 }
229 #endif /* OLD_STYLE */
 
 
 
 
 
230
231 if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
232 char *zLink;
233 if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){
234 zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate);
@@ -280,33 +298,18 @@
280 href("%R/deltachain/%d",rid), rid);
281 }
282 }
283 tag_private_status(rid);
284
285 #if !OLD_STYLE
286 if( (tmFlags & TIMELINE_CLASSIC)==0 ){
287 if( zType[0]=='e' && tagid ){
288 char *zId = db_text(0,
289 "SELECT substr(tagname,7) FROM tag WHERE tagid=abs(%d)", tagid);
290 cgi_printf(" technote:&nbsp;%z%S</a>",
291 href("%R/technote/%t",zId), zId);
292 }else{
293 cgi_printf(" hash:&nbsp;%z%S</a>", href("%R/info/%!S", zUuid), zUuid);
294 }
295 }
296 #endif /* !OLD_STYLE */
297
298 /* End timelineDetail */
299 if( (tmFlags & (TIMELINE_CLASSIC|TIMELINE_VERBOSE|TIMELINE_COMPACT))!=0 ){
300 cgi_printf(")");
301 }
302
303 if( tmFlags & TIMELINE_COMPACT ){
304 @ </span></span>
305 }else{
306 @ </span>
307 }
308 }
309
310
311 /*
312 ** SETTING: timeline-truncate-at-blank boolean default=off
@@ -323,11 +326,27 @@
323 ** on the far left-hand side of the screen, normally targets another
324 ** /timeline page that shows the entry in context. However, if this
325 ** option is turned on, that hyperlink targets the /info page showing
326 ** the details of the entry.
327 */
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
330 /*
331 ** Output a timeline in the web format given a query. The query
332 ** should return these columns:
333 **
@@ -375,11 +394,11 @@
375
376
377 if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
378 vid = db_lget_int("checkout", 0);
379 }
380 if( xExtra==0 ) xExtra = defaultExtra;
381 zPrevDate[0] = 0;
382 mxWikiLen = db_get_int("timeline-max-comment", 0);
383 dateFormat = db_get_int("timeline-date-format", 0);
384 bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
385 bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
@@ -388,10 +407,12 @@
388 }
389 if( tmFlags & TIMELINE_COLUMNAR ){
390 zStyle = "Columnar";
391 }else if( tmFlags & TIMELINE_COMPACT ){
392 zStyle = "Compact";
 
 
393 }else if( tmFlags & TIMELINE_VERBOSE ){
394 zStyle = "Verbose";
395 }else if( tmFlags & TIMELINE_CLASSIC ){
396 zStyle = "Classic";
397 }else{
@@ -738,18 +759,10 @@
738 }else{
739 cgi_printf("%W",blob_str(&comment));
740 }
741 }
742
743 if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){
744 /* This will only ever happen when Fossil is drawing a timeline for
745 ** its own self-host repository. If the timeline shows the specific
746 ** check-in corresponding to the current executable, then tag that
747 ** check-in with "This is me!". */
748 @ <b>&larr; This is me!</b>
749 }
750
751 @ </span>
752 blob_reset(&comment);
753
754 /* Generate extra information and hyperlinks that follow the comment.
755 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -1269,10 +1282,26 @@
1269 if( i>2 ){
1270 style_submenu_multichoice("y", i/2, az, isDisabled);
1271 }
1272 }
1273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1274 /*
1275 ** Return the default value for the "ss" cookie or query parameter.
1276 ** The "ss" cookie determines the graph style. See the
1277 ** timeline_view_styles[] global constant for a list of choices.
1278 */
@@ -1293,10 +1322,11 @@
1293 switch( v[0] ){
1294 case 'c': tmFlags = TIMELINE_COMPACT; break;
1295 case 'v': tmFlags = TIMELINE_VERBOSE; break;
1296 case 'j': tmFlags = TIMELINE_COLUMNAR; break;
1297 case 'x': tmFlags = TIMELINE_CLASSIC; break;
 
1298 default: tmFlags = TIMELINE_MODERN; break;
1299 }
1300 return tmFlags;
1301 }
1302
@@ -1305,15 +1335,16 @@
1305 */
1306 const char *const timeline_view_styles[] = {
1307 "m", "Modern View",
1308 "j", "Columnar View",
1309 "c", "Compact View",
 
1310 "v", "Verbose View",
1311 "x", "Classic View",
1312 };
1313 #if INTERFACE
1314 # define N_TIMELINE_VIEW_STYLE 5
1315 #endif
1316
1317 /*
1318 ** Add the select/option box to the timeline submenu that is used to
1319 ** set the ss= parameter that determines the viewing mode.
@@ -1670,11 +1701,11 @@
1670 ** ms=MATCHSTYLE Set tag name match algorithm. One of "exact", "glob",
1671 ** "like", or "regexp".
1672 ** u=USER Only show items associated with USER
1673 ** y=TYPE 'ci', 'w', 't', 'n', 'e', 'f', or 'all'.
1674 ** ss=VIEWSTYLE c: "Compact", v: "Verbose", m: "Modern", j: "Columnar",
1675 * x: "Classic".
1676 ** advm Use the "Advanced" or "Busy" menu design.
1677 ** ng No Graph.
1678 ** ncp Omit cherrypick merges
1679 ** nd Do not highlight the focus check-in
1680 ** nsm Omit the submenu
@@ -1893,11 +1924,13 @@
1893 || (bisectLocal && !g.perm.Setup)
1894 ){
1895 login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
1896 return;
1897 }
1898 if( (zBefore || zCirca) && robot_restrict("timelineX") ) return;
 
 
1899 if( !bisectLocal ){
1900 etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0);
1901 }
1902 cookie_read_parameter("y","y");
1903 zType = P("y");
1904
--- src/timeline.c
+++ src/timeline.c
@@ -115,11 +115,13 @@
115 #define TIMELINE_COMPACT 0x0001000 /* Use the "compact" view style */
116 #define TIMELINE_VERBOSE 0x0002000 /* Use the "detailed" view style */
117 #define TIMELINE_MODERN 0x0004000 /* Use the "modern" view style */
118 #define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */
119 #define TIMELINE_CLASSIC 0x0010000 /* Use the "classic" view style */
120 #define TIMELINE_SIMPLE 0x0020000 /* Use the "simple" view style */
121 #define TIMELINE_INLINE 0x0033000 /* Mask for views with in-line display */
122 #define TIMELINE_VIEWS 0x003f000 /* Mask for all of the view styles */
123 #define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */
124 #define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */
125 #define TIMELINE_CHPICK 0x0400000 /* Show cherrypick merges */
126 #define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */
127 #define TIMELINE_XMERGE 0x1000000 /* Omit merges from off-graph nodes */
@@ -179,11 +181,11 @@
181 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
182 **
183 ** This routine is used if no xExtra argument is supplied to
184 ** www_print_timeline().
185 */
186 void timeline_extra(
187 Stmt *pQuery, /* Current row of the timeline query */
188 int tmFlags, /* Flags to www_print_timeline() */
189 const char *zThisUser, /* Suppress links to this user */
190 const char *zThisTag /* Suppress links to this tag */
191 ){
@@ -194,29 +196,40 @@
196 const char *zUser = db_column_text(pQuery, 4);
197 const char *zTagList = db_column_text(pQuery, 8);
198 int tagid = db_column_int(pQuery, 9);
199 const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
200
201 if( (tmFlags & TIMELINE_INLINE)!=0 ){
202 cgi_printf("(");
203 }
204
 
 
 
205 if( (tmFlags & TIMELINE_CLASSIC)==0 ){
206 if( zType[0]=='c' ){
207 const char *zPrefix = 0;
208 static int markLeaves = -1;
209 if( markLeaves<0 ){
210 markLeaves = db_get_int("timeline-mark-leaves",1);
211 if( markLeaves<0 ) markLeaves = 1;
212 }
213 if( strcmp(zUuid, MANIFEST_UUID)==0 ){
214 /* This will only ever happen when Fossil is drawing a timeline for
215 ** its own self-host repository. If the timeline shows the specific
216 ** check-in corresponding to the current executable, then tag that
217 ** check-in with "self" */
218 zPrefix = "self&nbsp;";
219 }else if( markLeaves && db_column_int(pQuery,5) ){
220 if( markLeaves==1 ){
221 zPrefix = has_closed_tag(rid) ? "closed&nbsp;" : "leaf&nbsp;";
222 }else{
223 zPrefix = has_closed_tag(rid) ?
224 "<span class='timelineLeaf'>Closed-Leaf</span>\n" :
225 "<span class='timelineLeaf'>Leaf</span>\n";
226 }
227 }
228 cgi_printf("%scheck-in:&nbsp;%z<span class='timelineHash'>"
229 "%S</span></a> ",
230 zPrefix, href("%R/info/%!S",zUuid),zUuid);
231 }else if( zType[0]=='e' && tagid ){
232 cgi_printf("technote:&nbsp;");
233 hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
234 }else{
235 cgi_printf("artifact:&nbsp;%z%S</a> ",
@@ -224,11 +237,16 @@
237 }
238 }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t'
239 || zType[0]=='n' || zType[0]=='f'){
240 cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
241 }
242
243 if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
244 @ <span class='timelineEllipsis' id='ellipsis-%d(rid)' \
245 @ data-id='%d(rid)'>...</span>
246 @ <span class='clutter' id='detail-%d(rid)'>
247 }
248
249 if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
250 char *zLink;
251 if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){
252 zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate);
@@ -280,33 +298,18 @@
298 href("%R/deltachain/%d",rid), rid);
299 }
300 }
301 tag_private_status(rid);
302
303 if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
304 cgi_printf("</span>"); /* End of the declutter span */
305 }
 
 
 
 
 
 
 
 
 
306
307 /* End timelineDetail */
308 if( (tmFlags & TIMELINE_INLINE)!=0 ){
309 cgi_printf(")");
310 }
 
 
 
 
 
 
311 }
312
313
314 /*
315 ** SETTING: timeline-truncate-at-blank boolean default=off
@@ -323,11 +326,27 @@
326 ** on the far left-hand side of the screen, normally targets another
327 ** /timeline page that shows the entry in context. However, if this
328 ** option is turned on, that hyperlink targets the /info page showing
329 ** the details of the entry.
330 */
331 /*
332 ** SETTING: timeline-mark-leaves width=5 default=1
333 **
334 ** Determine whether or not leaf check-ins are marked as such in the
335 ** details section of the timeline. The value is an integer between 0
336 ** and 2:
337 **
338 ** 0 Do not show any special marking for leaf check-ins.
339 **
340 ** 1 Show just "leaf" or "closed"
341 **
342 ** 2 Show "Leaf" or "Closed-Leaf" with emphasis
343 **
344 ** The default is currently 1. Prior to 2025-10-19, the default was 2.
345 ** This setting has no effect on the "Classic" view, which always behaves
346 ** as if the setting were 2.
347 */
348
349 /*
350 ** Output a timeline in the web format given a query. The query
351 ** should return these columns:
352 **
@@ -375,11 +394,11 @@
394
395
396 if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
397 vid = db_lget_int("checkout", 0);
398 }
399 if( xExtra==0 ) xExtra = timeline_extra;
400 zPrevDate[0] = 0;
401 mxWikiLen = db_get_int("timeline-max-comment", 0);
402 dateFormat = db_get_int("timeline-date-format", 0);
403 bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
404 bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
@@ -388,10 +407,12 @@
407 }
408 if( tmFlags & TIMELINE_COLUMNAR ){
409 zStyle = "Columnar";
410 }else if( tmFlags & TIMELINE_COMPACT ){
411 zStyle = "Compact";
412 }else if( tmFlags & TIMELINE_SIMPLE ){
413 zStyle = "Simple";
414 }else if( tmFlags & TIMELINE_VERBOSE ){
415 zStyle = "Verbose";
416 }else if( tmFlags & TIMELINE_CLASSIC ){
417 zStyle = "Classic";
418 }else{
@@ -738,18 +759,10 @@
759 }else{
760 cgi_printf("%W",blob_str(&comment));
761 }
762 }
763
 
 
 
 
 
 
 
 
764 @ </span>
765 blob_reset(&comment);
766
767 /* Generate extra information and hyperlinks that follow the comment.
768 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -1269,10 +1282,26 @@
1282 if( i>2 ){
1283 style_submenu_multichoice("y", i/2, az, isDisabled);
1284 }
1285 }
1286
1287 /*
1288 ** SETTING: timeline-default-style width=5 default=m
1289 **
1290 ** This setting determines the default "view style" for timelines.
1291 ** The setting should be a single character, one of the following:
1292 **
1293 ** c Compact
1294 ** j Columnar
1295 ** m Modern
1296 ** s Simple
1297 ** v Verbose
1298 ** x Classic
1299 **
1300 ** The default value is m (Modern).
1301 */
1302
1303 /*
1304 ** Return the default value for the "ss" cookie or query parameter.
1305 ** The "ss" cookie determines the graph style. See the
1306 ** timeline_view_styles[] global constant for a list of choices.
1307 */
@@ -1293,10 +1322,11 @@
1322 switch( v[0] ){
1323 case 'c': tmFlags = TIMELINE_COMPACT; break;
1324 case 'v': tmFlags = TIMELINE_VERBOSE; break;
1325 case 'j': tmFlags = TIMELINE_COLUMNAR; break;
1326 case 'x': tmFlags = TIMELINE_CLASSIC; break;
1327 case 's': tmFlags = TIMELINE_SIMPLE; break;
1328 default: tmFlags = TIMELINE_MODERN; break;
1329 }
1330 return tmFlags;
1331 }
1332
@@ -1305,15 +1335,16 @@
1335 */
1336 const char *const timeline_view_styles[] = {
1337 "m", "Modern View",
1338 "j", "Columnar View",
1339 "c", "Compact View",
1340 "s", "Simple View",
1341 "v", "Verbose View",
1342 "x", "Classic View",
1343 };
1344 #if INTERFACE
1345 # define N_TIMELINE_VIEW_STYLE 6
1346 #endif
1347
1348 /*
1349 ** Add the select/option box to the timeline submenu that is used to
1350 ** set the ss= parameter that determines the viewing mode.
@@ -1670,11 +1701,11 @@
1701 ** ms=MATCHSTYLE Set tag name match algorithm. One of "exact", "glob",
1702 ** "like", or "regexp".
1703 ** u=USER Only show items associated with USER
1704 ** y=TYPE 'ci', 'w', 't', 'n', 'e', 'f', or 'all'.
1705 ** ss=VIEWSTYLE c: "Compact", v: "Verbose", m: "Modern", j: "Columnar",
1706 ** x: "Classic".
1707 ** advm Use the "Advanced" or "Busy" menu design.
1708 ** ng No Graph.
1709 ** ncp Omit cherrypick merges
1710 ** nd Do not highlight the focus check-in
1711 ** nsm Omit the submenu
@@ -1893,11 +1924,13 @@
1924 || (bisectLocal && !g.perm.Setup)
1925 ){
1926 login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
1927 return;
1928 }
1929 if( zBefore || zCirca ){
1930 if( robot_restrict("timelineX") ) return;
1931 }
1932 if( !bisectLocal ){
1933 etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0);
1934 }
1935 cookie_read_parameter("y","y");
1936 zType = P("y");
1937
+1 -9
--- src/zip.c
+++ src/zip.c
@@ -864,19 +864,11 @@
864864
if( fossil_strcmp(zOut,"")==0 || fossil_strcmp(zOut,"/dev/null")==0 ){
865865
zOut = 0;
866866
}
867867
868868
if( zName==0 ){
869
- zName = db_text("default-name",
870
- "SELECT replace(%Q,' ','_') "
871
- " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
872
- " || substr(blob.uuid, 1, 10)"
873
- " FROM event, blob"
874
- " WHERE event.objid=%d"
875
- " AND blob.rid=%d",
876
- db_get("project-name", "unnamed"), rid, rid
877
- );
869
+ zName = archive_base_name(rid);
878870
}
879871
zip_of_checkin(eType, rid, zOut ? &zip : 0,
880872
zName, pInclude, pExclude, listFlag);
881873
glob_free(pInclude);
882874
glob_free(pExclude);
883875
--- src/zip.c
+++ src/zip.c
@@ -864,19 +864,11 @@
864 if( fossil_strcmp(zOut,"")==0 || fossil_strcmp(zOut,"/dev/null")==0 ){
865 zOut = 0;
866 }
867
868 if( zName==0 ){
869 zName = db_text("default-name",
870 "SELECT replace(%Q,' ','_') "
871 " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
872 " || substr(blob.uuid, 1, 10)"
873 " FROM event, blob"
874 " WHERE event.objid=%d"
875 " AND blob.rid=%d",
876 db_get("project-name", "unnamed"), rid, rid
877 );
878 }
879 zip_of_checkin(eType, rid, zOut ? &zip : 0,
880 zName, pInclude, pExclude, listFlag);
881 glob_free(pInclude);
882 glob_free(pExclude);
883
--- src/zip.c
+++ src/zip.c
@@ -864,19 +864,11 @@
864 if( fossil_strcmp(zOut,"")==0 || fossil_strcmp(zOut,"/dev/null")==0 ){
865 zOut = 0;
866 }
867
868 if( zName==0 ){
869 zName = archive_base_name(rid);
 
 
 
 
 
 
 
 
870 }
871 zip_of_checkin(eType, rid, zOut ? &zip : 0,
872 zName, pInclude, pExclude, listFlag);
873 glob_free(pInclude);
874 glob_free(pExclude);
875
+34 -2
--- www/changes.wiki
+++ www/changes.wiki
@@ -2,16 +2,48 @@
22
33
<h2 id='v2_28'>Changes for version 2.28 (pending)</h2><ol>
44
<li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
55
<li> The default configuration now allows robots to download any tarball
66
or similar, to better support of automated build systems.
7
- <li> No special tag "zipX" for the [/help/robot-restrict|robot-restrict]
7
+ <li> New special tag "zipX" for the [/help/robot-restrict|robot-restrict]
88
setting blocks robot access to tarballs, but with exceptions to support
99
automated build systems.
10
+ <li> Enhancements to the default value for the
11
+ [/help/robot-restrict|robot-restrict setting].
1012
</ol>
1113
<li> A drop-down menu of recent branches is now possible for the submenu, and
12
- is used in the code browser.
14
+ is used in the [/dir?ci=trunk|code browser].
15
+ <li> Easier access to generated tarballs and ZIPs:<ol type="a">
16
+ <li> When in the [/dir?ci=trunk|code browser] at the top-level,
17
+ a new "Download" submenu option is available to take the
18
+ user to a page where he can download a tarball or ZIP archive.
19
+ <li> New [/help/www/download|/download page] is available. When
20
+ configured using the new
21
+ [/help/suggested-downloads|suggested-downloads setting], a
22
+ link to [/download] named "Tarballs and ZIPs" appears in the
23
+ [/sitemap] and thus on the hamburger menu.
24
+ <li> The filenames for tarballs and ZIPs are now standardized to
25
+ include a timestamp and a hash prefix.
26
+ </ol>
27
+ <li> Timeline enhancements:<ol type="a">
28
+ <li> A new "Simple" view is available. This is compromise between "Verbose"
29
+ and "Compact" that shows only the check-in hash rather than the full
30
+ detail section. There is an ellipsis that one can click on to see the
31
+ full detail text.
32
+ <li> The artifact hash in the detail section of each timeline entry is now
33
+ emphasized (controlled by CSS) to make it easier to locate amid all
34
+ the other text and links.
35
+ <li> When clicking on the ellipsis in "Compact" or "Simple" views, the ellipsis
36
+ is replaced by a left arrow ("←") which can be clicked to hide the extra
37
+ detail again.
38
+ <li> New setting [/help/timeline-mark-leaves|timeline-mark-leaves] controls
39
+ whether or not leaf check-ins are marked in the timeline.
40
+ <li> "No-graph" timelines (using the "ng" query parameter) now show
41
+ branch colors and bare check-in circles on the left. The check-in
42
+ circles appear, but no lines connecting them.
43
+ ([/timeline?ng|example]).
44
+ </ol>
1345
<li> The [/help/timeline|timeline command] is enhanced with the new
1446
"<tt>-u|--for-user</tt>" option.
1547
</ol>
1648
1749
<h2 id='v2_27'>Changes for version 2.27 (2025-09-30)</h2><ol>
1850
--- www/changes.wiki
+++ www/changes.wiki
@@ -2,16 +2,48 @@
2
3 <h2 id='v2_28'>Changes for version 2.28 (pending)</h2><ol>
4 <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
5 <li> The default configuration now allows robots to download any tarball
6 or similar, to better support of automated build systems.
7 <li> No special tag "zipX" for the [/help/robot-restrict|robot-restrict]
8 setting blocks robot access to tarballs, but with exceptions to support
9 automated build systems.
 
 
10 </ol>
11 <li> A drop-down menu of recent branches is now possible for the submenu, and
12 is used in the code browser.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13 <li> The [/help/timeline|timeline command] is enhanced with the new
14 "<tt>-u|--for-user</tt>" option.
15 </ol>
16
17 <h2 id='v2_27'>Changes for version 2.27 (2025-09-30)</h2><ol>
18
--- www/changes.wiki
+++ www/changes.wiki
@@ -2,16 +2,48 @@
2
3 <h2 id='v2_28'>Changes for version 2.28 (pending)</h2><ol>
4 <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
5 <li> The default configuration now allows robots to download any tarball
6 or similar, to better support of automated build systems.
7 <li> New special tag "zipX" for the [/help/robot-restrict|robot-restrict]
8 setting blocks robot access to tarballs, but with exceptions to support
9 automated build systems.
10 <li> Enhancements to the default value for the
11 [/help/robot-restrict|robot-restrict setting].
12 </ol>
13 <li> A drop-down menu of recent branches is now possible for the submenu, and
14 is used in the [/dir?ci=trunk|code browser].
15 <li> Easier access to generated tarballs and ZIPs:<ol type="a">
16 <li> When in the [/dir?ci=trunk|code browser] at the top-level,
17 a new "Download" submenu option is available to take the
18 user to a page where he can download a tarball or ZIP archive.
19 <li> New [/help/www/download|/download page] is available. When
20 configured using the new
21 [/help/suggested-downloads|suggested-downloads setting], a
22 link to [/download] named "Tarballs and ZIPs" appears in the
23 [/sitemap] and thus on the hamburger menu.
24 <li> The filenames for tarballs and ZIPs are now standardized to
25 include a timestamp and a hash prefix.
26 </ol>
27 <li> Timeline enhancements:<ol type="a">
28 <li> A new "Simple" view is available. This is compromise between "Verbose"
29 and "Compact" that shows only the check-in hash rather than the full
30 detail section. There is an ellipsis that one can click on to see the
31 full detail text.
32 <li> The artifact hash in the detail section of each timeline entry is now
33 emphasized (controlled by CSS) to make it easier to locate amid all
34 the other text and links.
35 <li> When clicking on the ellipsis in "Compact" or "Simple" views, the ellipsis
36 is replaced by a left arrow ("←") which can be clicked to hide the extra
37 detail again.
38 <li> New setting [/help/timeline-mark-leaves|timeline-mark-leaves] controls
39 whether or not leaf check-ins are marked in the timeline.
40 <li> "No-graph" timelines (using the "ng" query parameter) now show
41 branch colors and bare check-in circles on the left. The check-in
42 circles appear, but no lines connecting them.
43 ([/timeline?ng|example]).
44 </ol>
45 <li> The [/help/timeline|timeline command] is enhanced with the new
46 "<tt>-u|--for-user</tt>" option.
47 </ol>
48
49 <h2 id='v2_27'>Changes for version 2.27 (2025-09-30)</h2><ol>
50

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button