Fossil

Check-in [5fc0f7c33a]
Login

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

Overview
Comment:/fileedit now embeds the current open leaf list and the file list for the current checkin (defaulting to the most recent leaf) in the page content, saving 2 XHR requests at startup. If passed filename= without checkin= then it tries to load the given file from the most recent leaf.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 5fc0f7c33ab41e81327e20ec8b0eadc9121b95a4d27a247646b2b803168d9425
User & Date: stephan 2020-08-10 11:53:07.873
Context
2020-08-10
12:08
Improved handling of long posts in the forum. check-in: c265daea91 user: drh tags: trunk
11:53
/fileedit now embeds the current open leaf list and the file list for the current checkin (defaulting to the most recent leaf) in the page content, saving 2 XHR requests at startup. If passed filename= without checkin= then it tries to load the given file from the most recent leaf. check-in: 5fc0f7c33a user: stephan tags: trunk
05:38
Amend OpenBSD httpd Fossil server instructions to use the mount_mfs -P option to automatically populate the chroot /dev tree as suggested by anon on the forum. check-in: 4b240ec31e user: jamsek tags: trunk
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/fileedit.c.
963
964
965
966
967
968
969
970

971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988

989
990
991
992
993
994
995
996
997
998
999
1000
1001


1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
** Passed the values of the "checkin" and "filename" request
** properties, this function verifies that they are valid and
** populates:
**
** - *zRevUuid = the fully-expanded value of zRev (owned by the
**    caller). zRevUuid may be NULL.
**
** - *vid = the RID of zRevUuid. May not be NULL.

**
** - *frid = the RID of zFilename's blob content. May not be NULL
**   unless zFilename is also NULL. If BOTH of zFilename and frid are
**   NULL then no confirmation is done on the filename argument - only
**   zRev is checked.
**
** Returns 0 if the given file is not in the given checkin or if
** fileedit_ajax_check_filename() fails, else returns true.  If it
** returns false, it queues up an error response and the caller must
** return immediately.
*/
static int fileedit_ajax_setup_filerev(const char * zRev,
                                       char ** zRevUuid,
                                       int * vid,
                                       const char * zFilename,
                                       int * frid){
  char * zFileUuid = 0;             /* file content UUID */
  const int checkFile = zFilename!=0 || frid!=0;

  
  if(checkFile && !fileedit_ajax_check_filename(zFilename)){
    return 0;
  }
  *vid = symbolic_name_to_rid(zRev, "ci");
  if(0==*vid){
    ajax_route_error(404,"Cannot resolve name as a checkin: %s",
                     zRev);
    return 0;
  }else if(*vid<0){
    ajax_route_error(400,"Checkin name is ambiguous: %s",
                     zRev);
    return 0;


  }
  if(checkFile){
    zFileUuid = fileedit_file_uuid(zFilename, *vid, 0);
    if(zFileUuid==0){
      ajax_route_error(404, "Checkin does not contain file.");
      return 0;
    }
  }
  if(zRevUuid!=0){
    *zRevUuid = rid_to_uuid(*vid);
  }
  if(checkFile){
    assert(zFileUuid!=0);
    if(frid!=0){
      *frid = fast_uuid_to_rid(zFileUuid);
    }
    fossil_free(zFileUuid);







|
>













|




>




|
|



|



>
>


|






|







963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
** Passed the values of the "checkin" and "filename" request
** properties, this function verifies that they are valid and
** populates:
**
** - *zRevUuid = the fully-expanded value of zRev (owned by the
**    caller). zRevUuid may be NULL.
**
** - *pVid = the RID of zRevUuid. pVid May be NULL. If the vid
**    cannot be resolved or is ambiguous, pVid is not assigned.
**
** - *frid = the RID of zFilename's blob content. May not be NULL
**   unless zFilename is also NULL. If BOTH of zFilename and frid are
**   NULL then no confirmation is done on the filename argument - only
**   zRev is checked.
**
** Returns 0 if the given file is not in the given checkin or if
** fileedit_ajax_check_filename() fails, else returns true.  If it
** returns false, it queues up an error response and the caller must
** return immediately.
*/
static int fileedit_ajax_setup_filerev(const char * zRev,
                                       char ** zRevUuid,
                                       int * pVid,
                                       const char * zFilename,
                                       int * frid){
  char * zFileUuid = 0;             /* file content UUID */
  const int checkFile = zFilename!=0 || frid!=0;
  int vid = 0;
  
  if(checkFile && !fileedit_ajax_check_filename(zFilename)){
    return 0;
  }
  vid = symbolic_name_to_rid(zRev, "ci");
  if(0==vid){
    ajax_route_error(404,"Cannot resolve name as a checkin: %s",
                     zRev);
    return 0;
  }else if(vid<0){
    ajax_route_error(400,"Checkin name is ambiguous: %s",
                     zRev);
    return 0;
  }else if(pVid!=0){
    *pVid = vid;
  }
  if(checkFile){
    zFileUuid = fileedit_file_uuid(zFilename, vid, 0);
    if(zFileUuid==0){
      ajax_route_error(404, "Checkin does not contain file.");
      return 0;
    }
  }
  if(zRevUuid!=0){
    *zRevUuid = rid_to_uuid(vid);
  }
  if(checkFile){
    assert(zFileUuid!=0);
    if(frid!=0){
      *frid = fast_uuid_to_rid(zFileUuid);
    }
    fossil_free(zFileUuid);
1288
1289
1290
1291
1292
1293
1294


















































































1295
1296
1297
1298
1299
1300
1301
  p->zUser = mprintf("%s",g.zLogin);
  return 0;
end_fail:
#undef fail
  fossil_free(zFileUuid);
  return rc ? rc : 500;
}



















































































/*
** AJAX route /fileedit?ajax=filelist
**
** Fetches a JSON-format list of leaves and/or filenames for use in
** creating a file selection list in /fileedit. It has different modes
** of operation depending on its arguments:







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







1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
  p->zUser = mprintf("%s",g.zLogin);
  return 0;
end_fail:
#undef fail
  fossil_free(zFileUuid);
  return rc ? rc : 500;
}

/*
** Renders a list of all open leaves in JSON form:
**
** [
**   {checkin: UUID, branch: branchName, timestamp: string}
** ]
**
** The entries are ordered newest first.
**
** If zFirstUuid is not NULL then *zFirstUuid is set to a copy of the
** full UUID of the first (most recent) leaf, which must be freed by
** the caller. It is set to 0 if there are no leaves.
*/
static void fileedit_render_leaves_list(char ** zFirstUuid){
  Blob sql = empty_blob;
  Stmt q = empty_Stmt;
  int i = 0;

  if(zFirstUuid){
    *zFirstUuid = 0;
  }
  blob_append(&sql, timeline_query_for_tty(), -1);
  blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf "
                  "WHERE NOT EXISTS("
                  "SELECT 1 from tagxref WHERE tagid=%d AND "
                  "tagtype>0 AND rid=leaf.rid"
                  ")) "
                  "ORDER BY mtime DESC", TAG_CLOSED);
  db_prepare_blob(&q, &sql);
  CX("[");
  while( SQLITE_ROW==db_step(&q) ){
    const char * zUuid = db_column_text(&q, 1);
    if(i++){
      CX(",");
    }else if(zFirstUuid){
      *zFirstUuid = fossil_strdup(zUuid);
    }
    CX("{");
    CX("\"checkin\":%!j,", zUuid);
    CX("\"branch\":%!j,", db_column_text(&q, 7));
    CX("\"timestamp\":%!j", db_column_text(&q, 2));
    CX("}");
  }
  CX("]");
  db_finalize(&q);
}

/*
** For the given fully resolved UUID, renders a JSON object containing
** the fileeedit-editable files in that checkin:
**
** {
**   checkin: UUID,
**   editableFiles: [ filename1, ... filenameN ]
** }
**
** They are sorted by name using filename_collation().
*/
static void fileedit_render_checkin_files(const char * zFullUuid){
  Blob sql = empty_blob;
  Stmt q = empty_Stmt;
  int i = 0;

  CX("{\"checkin\":%!j,"
     "\"editableFiles\":[", zFullUuid);
  blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) "
                  "ORDER BY filename %s",
                  zFullUuid, filename_collation());
  db_prepare_blob(&q, &sql);
  while( SQLITE_ROW==db_step(&q) ){
    const char * zFilename = db_column_text(&q, 0);
    if(fileedit_is_editable(zFilename)){
      if(i++){
        CX(",");
      }
      CX("%!j", zFilename);
    }
  }
  db_finalize(&q);
  CX("]}");  
}

/*
** AJAX route /fileedit?ajax=filelist
**
** Fetches a JSON-format list of leaves and/or filenames for use in
** creating a file selection list in /fileedit. It has different modes
** of operation depending on its arguments:
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
**   checkin: UUID,
**   editableFiles: [ filename1, ... filenameN ] // sorted by name
** }
**
** On error it produces a JSON response as documented for
** ajax_route_error().
*/
static void fileedit_ajax_filelist(void){
  const char * zCi = PD("checkin",P("ci"));
  Blob sql = empty_blob;
  Stmt q = empty_Stmt;
  int i = 0;

  if(!ajax_route_bootstrap(1,0)){
    return;
  }
  cgi_set_content_type("application/json");
  if(zCi!=0){
    char * zCiFull = 0;
    int vid = 0;
    if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, &vid, 0, 0)){
      /* Error already reported */
      return;
    }
    CX("{\"checkin\":%!j,"
       "\"editableFiles\":[", zCiFull);
    blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) "
                    "ORDER BY filename %s",
                    zCiFull, filename_collation());
    db_prepare_blob(&q, &sql);
    while( SQLITE_ROW==db_step(&q) ){
      const char * zFilename = db_column_text(&q, 0);
      if(fileedit_is_editable(zFilename)){
        if(i++){
          CX(",");
        }
        CX("%!j", zFilename);
      }
    }
    db_finalize(&q);
    CX("]}");
  }else if(P("leaves")!=0){
    blob_append(&sql, timeline_query_for_tty(), -1);
    blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf "
                    "WHERE NOT EXISTS("
                    "SELECT 1 from tagxref WHERE tagid=%d AND "
                    "tagtype>0 AND rid=leaf.rid"
                    ")) "
                    "ORDER BY mtime DESC", TAG_CLOSED);
    db_prepare_blob(&q, &sql);
    CX("[");
    while( SQLITE_ROW==db_step(&q) ){
      if(i++){
        CX(",");
      }
      CX("{");
      CX("\"checkin\":%!j,", db_column_text(&q, 1));
      CX("\"branch\":%!j,", db_column_text(&q, 7));
      CX("\"timestamp\":%!j", db_column_text(&q, 2));
      CX("}");
    }
    CX("]");
    db_finalize(&q);
  }else{
    ajax_route_error(500, "Unhandled URL argument.");
  }
}

/*
** AJAX route /fileedit?ajax=commit







|

<
<
<







<
|



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

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







1402
1403
1404
1405
1406
1407
1408
1409
1410



1411
1412
1413
1414
1415
1416
1417

1418
1419
1420
1421
1422
1423















1424




1425
















1426
1427
1428
1429
1430
1431
1432
**   checkin: UUID,
**   editableFiles: [ filename1, ... filenameN ] // sorted by name
** }
**
** On error it produces a JSON response as documented for
** ajax_route_error().
*/
static void fileedit_ajax_filelist(){
  const char * zCi = PD("checkin",P("ci"));




  if(!ajax_route_bootstrap(1,0)){
    return;
  }
  cgi_set_content_type("application/json");
  if(zCi!=0){
    char * zCiFull = 0;

    if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, 0, 0, 0)){
      /* Error already reported */
      return;
    }
    fileedit_render_checkin_files(zCiFull);
    fossil_free(zCiFull);















  }else if(P("leaves")!=0){




    fileedit_render_leaves_list(0);
















  }else{
    ajax_route_error(500, "Unhandled URL argument.");
  }
}

/*
** AJAX route /fileedit?ajax=commit
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
**
** Which additional parameters are used by each distinct ajax value is
** an internal implementation detail and may change with any given
** build of this code. An unknown "name" value triggers an error, as
** documented for ajax_route_error().
*/
void fileedit_page(void){
  const char * zFilename = 0;          /* filename. We'll accept 'name'
                                           because that param is handled
                                           specially by the core. */
  const char * zRev = 0;                /* checkin version */
  const char * zFileMime = 0;           /* File mime type guess */
  CheckinMiniInfo cimi;                 /* Checkin state */
  int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */
  Blob err = empty_blob;                /* Error report */
  Blob endScript = empty_blob;          /* Script code to run at the
                                           end. This content will be
                                           combined into a single JS
                                           function call, thus each
                                           entry must end with a
                                           semicolon. */
  const char *zAjax = P("name");        /* Name of AJAX route for
                                           sub-dispatching. */

  /* Allow no access to this page without check-in privilege */
  login_check_credentials();
  if( !g.perm.Write ){
    if(zAjax!=0){







<
<
<
<




<
<
<
<
<
<







1554
1555
1556
1557
1558
1559
1560




1561
1562
1563
1564






1565
1566
1567
1568
1569
1570
1571
**
** Which additional parameters are used by each distinct ajax value is
** an internal implementation detail and may change with any given
** build of this code. An unknown "name" value triggers an error, as
** documented for ajax_route_error().
*/
void fileedit_page(void){




  const char * zFileMime = 0;           /* File mime type guess */
  CheckinMiniInfo cimi;                 /* Checkin state */
  int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */
  Blob err = empty_blob;                /* Error report */






  const char *zAjax = P("name");        /* Name of AJAX route for
                                           sub-dispatching. */

  /* Allow no access to this page without check-in privilege */
  login_check_credentials();
  if( !g.perm.Write ){
    if(zAjax!=0){
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
  ** error in (&err) and goto end_footer instead so that we can be
  ** sure to emit the error message, do any cleanup, and end the
  ** transaction cleanly.
  */
  {
    int isMissingArg = 0;
    if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){
      zFilename = cimi.zFilename;
      zRev = cimi.zParentUuid;
      assert(zRev);
      assert(zFilename);
      zFileMime = mimetype_from_name(cimi.zFilename);
    }else if(isMissingArg!=0){
      /* Squelch these startup warnings - they're non-fatal now but
      ** used to be fatal. */
      blob_reset(&err);
    }
  }







|
<
<
<







1625
1626
1627
1628
1629
1630
1631
1632



1633
1634
1635
1636
1637
1638
1639
  ** error in (&err) and goto end_footer instead so that we can be
  ** sure to emit the error message, do any cleanup, and end the
  ** transaction cleanly.
  */
  {
    int isMissingArg = 0;
    if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){
      assert(cimi.zFilename);



      zFileMime = mimetype_from_name(cimi.zFilename);
    }else if(isMissingArg!=0){
      /* Squelch these startup warnings - they're non-fatal now but
      ** used to be fatal. */
      blob_reset(&err);
    }
  }
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981


1982


1983
1984
1985










































1986
1987
1988
1989
1990
1991
1992


1993
1994
1995
       "is unspecified and may differ across environments. When "
       "committing or force-reloading a file, local edits to that "
       "file/check-in combination are discarded.</li>");
    CX("</ul>");
  }
  CX("</div>"/*#fileedit-tab-help*/);

  {
    /* Dynamically populate the editor, display any error in the err
    ** blob, and/or switch to tab #0, where the file selector
    ** lives... */
    blob_appendf(&endScript, "fossil.config['fileedit-glob'] = ");
    glob_render_as_json(fileedit_glob(), &endScript);
    blob_append(&endScript, ";\n", 2);
    blob_append(&endScript, "fossil.onPageLoad(", -1);
    if(zRev && zFilename){
      assert(0==blob_size(&err));
      blob_appendf(&endScript,
                   "()=>fossil.page.loadFile(%!j,%!j)",
                   zFilename, cimi.zParentUuid);
    }else{
      blob_appendf(&endScript,"function(){\n");
      if(blob_size(&err)>0){
        blob_appendf(&endScript,
                     "fossil.error(%!j);\n",
                     blob_str(&err));
      }
      blob_appendf(&endScript,
                   "fossil.page.tabs.switchToTab(0);\n");
      blob_appendf(&endScript,"}");
    }
    blob_appendf(&endScript,");\n");
  }

  blob_reset(&err);
  CheckinMiniInfo_cleanup(&cimi);

  builtin_request_js("sbsdiff.js");
  style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer",
                            "storage", 0);
  builtin_fulfill_js_requests();
  /*
  ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is
  ** used for dynamically toggling certain UI components on and off.
  ** Must come after window.fossil has been intialized and before
  ** fossil.page.fileedit.js. Potential TODO: move this into the
  ** window.fossil bootstrapping so that we don't have to "fulfill"
  ** the JS multiple times.
  */
  ajax_emit_js_preview_modes(1);
  builtin_request_js("fossil.page.fileedit.js");
  builtin_fulfill_js_requests();


  if(blob_size(&endScript)>0){


    style_emit_script_tag(0,0);
    CX("\n(function(){\n");
    CX("try{\n%b}\n"










































       "catch(e){"
       "fossil.error(e); console.error('Exception:',e);"
       "}\n",
       &endScript);
    CX("})();");
    style_emit_script_tag(1,0);
  }


  db_end_transaction(0);
  style_footer();
}







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















>
>
|
>
>


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

|
<
|


>
>



1964
1965
1966
1967
1968
1969
1970






























1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038

2039
2040
2041
2042
2043
2044
2045
2046
       "is unspecified and may differ across environments. When "
       "committing or force-reloading a file, local edits to that "
       "file/check-in combination are discarded.</li>");
    CX("</ul>");
  }
  CX("</div>"/*#fileedit-tab-help*/);































  builtin_request_js("sbsdiff.js");
  style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer",
                            "storage", 0);
  builtin_fulfill_js_requests();
  /*
  ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is
  ** used for dynamically toggling certain UI components on and off.
  ** Must come after window.fossil has been intialized and before
  ** fossil.page.fileedit.js. Potential TODO: move this into the
  ** window.fossil bootstrapping so that we don't have to "fulfill"
  ** the JS multiple times.
  */
  ajax_emit_js_preview_modes(1);
  builtin_request_js("fossil.page.fileedit.js");
  builtin_fulfill_js_requests();
  {
    /* Dynamically populate the editor, display any error in the err
    ** blob, and/or switch to tab #0, where the file selector
    ** lives. The extra C scopes here correspond to JS-level scopes,
    ** to improve grokability. */
    style_emit_script_tag(0,0);
    CX("\n(function(){\n");
    CX("try{\n");
    {
      char * zFirstLeafUuid = 0;
      CX("fossil.config['fileedit-glob'] = ");
      glob_render_json_to_cgi(fileedit_glob());
      CX(";\n");
      if(blob_size(&err)>0){
        CX("fossil.error(%!j);\n", blob_str(&err));
      }
      /* Populate the page with the current leaves and, if available,
         the selected checkin's file list, to save 1 or 2 XHR requests
         at startup. That makes this page uncacheable, but compressed
         delivery of this page is currently less than 6k. */
      CX("fossil.page.initialLeaves = ");
      fileedit_render_leaves_list(cimi.zParentUuid ? 0 : &zFirstLeafUuid);
      CX(";\n");
      if(zFirstLeafUuid){
        assert(!cimi.zParentUuid);
        cimi.zParentUuid = zFirstLeafUuid;
        zFirstLeafUuid = 0;
      }
      if(cimi.zParentUuid){
        CX("fossil.page.initialFiles = ");
        fileedit_render_checkin_files(cimi.zParentUuid);
        CX(";\n");
      }
      CX("fossil.onPageLoad(function(){\n");
      {
        if(blob_size(&err)>0){
          CX("fossil.error(%!j);\n",
             blob_str(&err));
          CX("fossil.page.tabs.switchToTab(0);\n");
        }
        if(cimi.zParentUuid && cimi.zFilename){
          CX("fossil.page.loadFile(%!j,%!j);\n",
             cimi.zFilename, cimi.zParentUuid)
            /* Reminder we cannot embed the JSON-format
               content of the file here because if it contains
               a SCRIPT tag then it will break the whole page. */;
        }
      }
      CX("});\n")/*fossil.onPageLoad()*/;
    }
    CX("}catch(e){"
       "fossil.error(e); console.error('Exception:',e);"
       "}\n");

    CX("})();")/*anonymous function*/;
    style_emit_script_tag(1,0);
  }
  blob_reset(&err);
  CheckinMiniInfo_cleanup(&cimi);
  db_end_transaction(0);
  style_footer();
}
Changes to src/fossil.page.fileedit.js.
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297

298



299
300
301
302
303
304
305
306



307
308









309

310
311
312
313
314
315
316
      D.append(D.clearElement(
        this.e.ciListLabel,
        this.e.selectCi,
        this.e.selectFiles
      ),"Loading leaves...");
      D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); 
      const self = this;
      F.fetch('fileedit/filelist',{
        urlParams:'leaves',
        responseType: 'json',
        onload: function(list){
          D.append(D.clearElement(self.e.ciListLabel),
                   "Open leaves (newest first):");
          self.cache.checkins = list;
          D.clearElement(D.enable(self.e.selectCi));

          let loadThisOne;



          list.forEach(function(o,n){
            if(!n) loadThisOne = o;
            self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch;
            D.option(self.e.selectCi, o.checkin,
                     o.timestamp+' ['+o.branch+']: '
                     +F.hashDigits(o.checkin));
          });
          F.storage.setJSON(self.cache.branchKey, self.cache.branchNames);



          self.loadFiles(loadThisOne ? loadThisOne.checkin : false);
        }









      });

    },
    /**
       Loads the file list for the given checkin UUID. It uses a
       cached copy on subsequent calls for the same UUID. If passed a
       falsy value, it instead clears and disables the file selection
       list.
    */







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







283
284
285
286
287
288
289



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
      D.append(D.clearElement(
        this.e.ciListLabel,
        this.e.selectCi,
        this.e.selectFiles
      ),"Loading leaves...");
      D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); 
      const self = this;



      const onload = function(list){
        D.append(D.clearElement(self.e.ciListLabel),
                 "Open leaves (newest first):");
        self.cache.checkins = list;
        D.clearElement(D.enable(self.e.selectCi));
        let loadThisOne = P.initialFiles/*possibly injected at page-load time*/;
        if(loadThisOne){
          self.cache.files[loadThisOne.checkin] = loadThisOne;
          delete P.initialFiles;
        }
        list.forEach(function(o,n){
          if(!n && !loadThisOne) loadThisOne = o;
          self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch;
          D.option(self.e.selectCi, o.checkin,
                   o.timestamp+' ['+o.branch+']: '
                   +F.hashDigits(o.checkin));
        });
        F.storage.setJSON(self.cache.branchKey, self.cache.branchNames);
        if(loadThisOne){
          self.e.selectCi.value = loadThisOne.checkin;
        }
        self.loadFiles(loadThisOne ? loadThisOne.checkin : false);
      };
      if(P.initialLeaves/*injected at page-load time.*/){
        const lv = P.initialLeaves;
        delete P.initialLeaves;
        onload(lv);
      }else{
        F.fetch('fileedit/filelist',{
          urlParams:'leaves',
          responseType: 'json',
          onload: onload
        });
      }
    },
    /**
       Loads the file list for the given checkin UUID. It uses a
       cached copy on subsequent calls for the same UUID. If passed a
       falsy value, it instead clears and disables the file selection
       list.
    */
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
          stretch to fill the parent width: */
        D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad)
      );
      if(F.config['fileedit-glob']){
        D.append(
          this.e.container,
          D.append(
            D.div(),
            D.append(D.code(),"fileedit-glob"),
            " config setting = ",
            D.append(D.code(), JSON.stringify(F.config['fileedit-glob']))
          )
        );
      }








|







423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
          stretch to fill the parent width: */
        D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad)
      );
      if(F.config['fileedit-glob']){
        D.append(
          this.e.container,
          D.append(
            D.span(),
            D.append(D.code(),"fileedit-glob"),
            " config setting = ",
            D.append(D.code(), JSON.stringify(F.config['fileedit-glob']))
          )
        );
      }

Changes to src/glob.c.
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185















186
187
188
189
190
191
192

/*
** Appends the given glob to the given buffer in the form of a
** JS/JSON-compatible array. It requires that pDest have been
** initialized. If pGlob is NULL or empty it emits [] (an empty
** array).
*/
void glob_render_as_json(Glob *pGlob, Blob *pDest){
  int i = 0;
  blob_append(pDest, "[", 1);
  for( ; pGlob && i < pGlob->nPattern; ++i ){
    if(i){
      blob_append(pDest, ",", 1);
    }
    blob_appendf(pDest, "%!j", pGlob->azPattern[i]);
  }
  blob_append(pDest, "]", 1);
}
















/*
** COMMAND: test-glob
**
** Usage:  %fossil test-glob PATTERN STRING...
**
** PATTERN is a comma- and whitespace-separated list of optionally







|










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







168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207

/*
** Appends the given glob to the given buffer in the form of a
** JS/JSON-compatible array. It requires that pDest have been
** initialized. If pGlob is NULL or empty it emits [] (an empty
** array).
*/
void glob_render_json_to_blob(Glob *pGlob, Blob *pDest){
  int i = 0;
  blob_append(pDest, "[", 1);
  for( ; pGlob && i < pGlob->nPattern; ++i ){
    if(i){
      blob_append(pDest, ",", 1);
    }
    blob_appendf(pDest, "%!j", pGlob->azPattern[i]);
  }
  blob_append(pDest, "]", 1);
}
/*
** Functionally equivalent to glob_render_json_to_blob()
** but outputs via cgi_print().
*/
void glob_render_json_to_cgi(Glob *pGlob){
  int i = 0;
  CX("[");
  for( ; pGlob && i < pGlob->nPattern; ++i ){
    if(i){
      CX(",");
    }
    CX("%!j", pGlob->azPattern[i]);
  }
  CX("]");
}

/*
** COMMAND: test-glob
**
** Usage:  %fossil test-glob PATTERN STRING...
**
** PATTERN is a comma- and whitespace-separated list of optionally