Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Several minor internal cleanups in the /json bits, most notably how g.json.isJsonMode gets initialized (based strictly on the path/command, not guessing based on POST input). |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
39bef9274530c454c2368c442f7b6a54 |
| User & Date: | stephan 2020-01-29 12:34:07.334 |
Context
|
2020-01-29
| ||
| 13:52 | Added the 'files' CSS class to the /dir column view element, per discussion at [https://fossil-scm.org/forum/forumpost/092ec8a4d0|/forumpost/092ec8a4d0]. ... (check-in: 374ca0c007 user: stephan tags: trunk) | |
| 12:34 | Several minor internal cleanups in the /json bits, most notably how g.json.isJsonMode gets initialized (based strictly on the path/command, not guessing based on POST input). ... (check-in: 39bef92745 user: stephan tags: trunk) | |
| 09:47 | Removed .column,.columns {float:left} from Ardoise skin because it can break display of README.md in /dir view by causing the README to display in the same row as the dir columns. ... (check-in: da76d728b4 user: stephan tags: trunk) | |
Changes
Changes to src/cgi.c.
| ︙ | ︙ | |||
1033 1034 1035 1036 1037 1038 1039 |
zRequestUri = mprintf("%s/%s", zScriptName, z);
cgi_set_parameter("REQUEST_URI", zRequestUri);
}
if( zPathInfo==0 ){
int i, j;
for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){}
for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){}
| > | > > > > > > > > > > > > | > | 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 |
zRequestUri = mprintf("%s/%s", zScriptName, z);
cgi_set_parameter("REQUEST_URI", zRequestUri);
}
if( zPathInfo==0 ){
int i, j;
for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){}
for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){}
zPathInfo = mprintf("%.*s", j-i, zRequestUri+i);
cgi_set_parameter("PATH_INFO", zPathInfo);
}
#ifdef FOSSIL_ENABLE_JSON
if(strncmp("/json",zPathInfo,5)==0
&& (zPathInfo[5]==0 || zPathInfo[5]=='/')){
/* We need to change some following behaviour depending on whether
** we are operating in JSON mode or not. We cannot, however, be
** certain whether we should/need to be in JSON mode until the
** PATH_INFO is set up.
*/
g.json.isJsonMode = 1;
}else{
assert(!g.json.isJsonMode &&
"Internal misconfiguration of g.json.isJsonMode");
}
#endif
z = (char*)P("HTTP_COOKIE");
if( z ){
z = mprintf("%s",z);
add_param_list(z, ';');
}
z = (char*)P("QUERY_STRING");
|
| ︙ | ︙ | |||
1069 1070 1071 1072 1073 1074 1075 |
blob_zero(&g.cgiIn);
if( len>0 && zType ){
if( fossil_strcmp(zType, "application/x-fossil")==0 ){
blob_read_from_channel(&g.cgiIn, g.httpIn, len);
blob_uncompress(&g.cgiIn, &g.cgiIn);
}
#ifdef FOSSIL_ENABLE_JSON
| | < | < | | < < < | < < | < < < < | 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 |
blob_zero(&g.cgiIn);
if( len>0 && zType ){
if( fossil_strcmp(zType, "application/x-fossil")==0 ){
blob_read_from_channel(&g.cgiIn, g.httpIn, len);
blob_uncompress(&g.cgiIn, &g.cgiIn);
}
#ifdef FOSSIL_ENABLE_JSON
else if( noJson==0 && g.json.isJsonMode!=0
&& json_can_consume_content_type(zType)!=0 ){
cgi_parse_POST_JSON(g.httpIn, (unsigned int)len);
/*
Potential TODOs:
1) If parsing fails, immediately return an error response
without dispatching the ostensibly-upcoming JSON API.
*/
cgi_set_content_type(json_guess_content_type());
}
#endif /* FOSSIL_ENABLE_JSON */
else{
blob_read_from_channel(&g.cgiIn, g.httpIn, len);
}
|
| ︙ | ︙ | |||
1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 |
if( zIpAddr==0 ){
zIpAddr = cgi_remote_ip(fileno(g.httpIn));
}
if( zIpAddr ){
cgi_setenv("REMOTE_ADDR", zIpAddr);
g.zIpAddr = mprintf("%s", zIpAddr);
}
/* Get all the optional fields that follow the first line.
*/
while( fgets(zLine,sizeof(zLine),g.httpIn) ){
char *zFieldName;
char *zVal;
| > | 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 |
if( zIpAddr==0 ){
zIpAddr = cgi_remote_ip(fileno(g.httpIn));
}
if( zIpAddr ){
cgi_setenv("REMOTE_ADDR", zIpAddr);
g.zIpAddr = mprintf("%s", zIpAddr);
}
/* Get all the optional fields that follow the first line.
*/
while( fgets(zLine,sizeof(zLine),g.httpIn) ){
char *zFieldName;
char *zVal;
|
| ︙ | ︙ |
Changes to src/json.c.
| ︙ | ︙ | |||
50 51 52 53 54 55 56 | "payload" /* payload */, "requestId" /*requestId*/, "resultCode" /*resultCode*/, "resultText" /*resultText*/, "timestamp" /*timestamp*/ }; | < < > > > | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
"payload" /* payload */,
"requestId" /*requestId*/,
"resultCode" /*resultCode*/,
"resultText" /*resultText*/,
"timestamp" /*timestamp*/
};
/*
** Returns true (non-0) if fossil appears to be running in JSON mode.
** and either has JSON POSTed input awaiting consumption or fossil is
** running in HTTP mode (in which case certain JSON data *might* be
** available via GET parameters).
*/
int fossil_has_json(){
return g.json.isJsonMode && (g.isHTTP || g.json.post.o);
}
/*
** Placeholder /json/XXX page impl for NYI (Not Yet Implemented)
|
| ︙ | ︙ | |||
574 575 576 577 578 579 580 581 582 583 584 585 586 587 |
: "application/json";
}else{
return "text/plain";
}
}
}
}
/*
** Sends pResponse to the output stream as the response object. This
** function does no validation of pResponse except to assert() that it
** is not NULL. The caller is responsible for ensuring that it meets
** API response envelope conventions.
**
| > > > > > > > > > > > > > > > | 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 |
: "application/json";
}else{
return "text/plain";
}
}
}
}
/*
** Given a request CONTENT_TYPE value, this function returns true
** if it is of a type which the JSON API can ostensibly read.
**
** It accepts any of application/json, text/plain, or
** application/javascript. The former is preferred, but was not
** widespread when this API was initially built, so the latter forms
** are permitted as fallbacks.
*/
int json_can_consume_content_type(const char * zType){
return fossil_strcmp(zType, "application/json")==0
|| fossil_strcmp(zType,"text/plain")==0/*assume this MIGHT be JSON*/
|| fossil_strcmp(zType,"application/javascript")==0;
}
/*
** Sends pResponse to the output stream as the response object. This
** function does no validation of pResponse except to assert() that it
** is not NULL. The caller is responsible for ensuring that it meets
** API response envelope conventions.
**
|
| ︙ | ︙ | |||
923 924 925 926 927 928 929 |
assert(g.json.gc.a && "json_main_bootstrap() was not called!");
assert( (0==once) && "json_mode_bootstrap() called too many times!");
if( once ){
return;
}else{
once = 1;
}
| | > | 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 |
assert(g.json.gc.a && "json_main_bootstrap() was not called!");
assert( (0==once) && "json_mode_bootstrap() called too many times!");
if( once ){
return;
}else{
once = 1;
}
assert(g.json.isJsonMode
&& "g.json.isJsonMode should have been set up by now.");
g.json.resultCode = 0;
g.json.cmd.offset = -1;
g.json.jsonp = PD("jsonp",NULL)
/* FIXME: do some sanity checking on g.json.jsonp and ignore it
if it is not halfway reasonable.
*/
;
|
| ︙ | ︙ | |||
1003 1004 1005 1006 1007 1008 1009 |
break;
}
inFile = (0==strcmp("-",jfile))
? stdin
: fossil_fopen(jfile,"rb");
if(!inFile){
g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED;
| | < | < | 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 |
break;
}
inFile = (0==strcmp("-",jfile))
? stdin
: fossil_fopen(jfile,"rb");
if(!inFile){
g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED;
fossil_fatal("Could not open JSON file [%s].",jfile)
/* Does not return. */
;
}
cgi_parse_POST_JSON(inFile, 0);
fossil_fclose(inFile);
break;
}
/* g.json.reqPayload exists only to simplify some of our access to
the request payload. We currently only use this in the context of
Object payloads, not Arrays, strings, etc.
*/
|
| ︙ | ︙ | |||
1108 1109 1110 1111 1112 1113 1114 |
short i = 0;
#define NEXT cson_string_cstr( \
cson_value_get_string( \
cson_array_get(ar,i) \
))
char const * tok = NEXT;
while( tok ){
| | < | > | 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 |
short i = 0;
#define NEXT cson_string_cstr( \
cson_value_get_string( \
cson_array_get(ar,i) \
))
char const * tok = NEXT;
while( tok ){
if( g.isHTTP/*workaround for "abbreviated name" in CLI mode*/
? (0==strncmp("json",tok,4))
: (0==strcmp(g.argv[1],tok))
){
g.json.cmd.offset = i;
break;
}
++i;
tok = NEXT;
}
|
| ︙ | ︙ | |||
1381 1382 1383 1384 1385 1386 1387 | cson_object * o = NULL; int rc; resultCode = json_dumbdown_rc(resultCode ? resultCode : g.json.resultCode); o = cson_new_object(); v = cson_object_value(o); if( ! o ) return NULL; #define SET(K) if(!tmp) goto cleanup; \ | > | < | > | | < | 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 |
cson_object * o = NULL;
int rc;
resultCode = json_dumbdown_rc(resultCode ? resultCode : g.json.resultCode);
o = cson_new_object();
v = cson_object_value(o);
if( ! o ) return NULL;
#define SET(K) if(!tmp) goto cleanup; \
cson_value_add_reference(tmp); \
rc = cson_object_set( o, K, tmp ); \
cson_value_free(tmp); \
if(rc) do{ \
tmp = NULL; \
goto cleanup; \
}while(0)
tmp = json_new_string(MANIFEST_UUID);
SET("fossil");
tmp = json_new_timestamp(-1);
SET(FossilJsonKeys.timestamp);
|
| ︙ | ︙ | |||
1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 |
}
if(g.json.cmd.commandStr){
tmp = json_new_string(g.json.cmd.commandStr);
}else{
tmp = json_response_command_path();
}
SET("command");
tmp = json_getenv(FossilJsonKeys.requestId);
if( tmp ) cson_object_set( o, FossilJsonKeys.requestId, tmp );
if(0){/* these are only intended for my own testing...*/
if(g.json.cmd.v){
tmp = g.json.cmd.v;
SET("$commandPath");
}
if(g.json.param.v){
tmp = g.json.param.v;
SET("$params");
}
if(0){/*Only for debugging, add some info to the response.*/
tmp = cson_value_new_integer( g.json.cmd.offset );
| > > > | | > < > | < > | 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 |
}
if(g.json.cmd.commandStr){
tmp = json_new_string(g.json.cmd.commandStr);
}else{
tmp = json_response_command_path();
}
if(!tmp){
tmp = json_new_string("???");
}
SET("command");
tmp = json_getenv(FossilJsonKeys.requestId);
if( tmp ) cson_object_set( o, FossilJsonKeys.requestId, tmp );
if(0){/* these are only intended for my own testing...*/
if(g.json.cmd.v){
tmp = g.json.cmd.v;
SET("$commandPath");
}
if(g.json.param.v){
tmp = g.json.param.v;
SET("$params");
}
if(0){/*Only for debugging, add some info to the response.*/
tmp = cson_value_new_integer( g.json.cmd.offset );
SET("cmd.offset");
tmp = cson_value_new_bool( g.isHTTP );
SET("isCGI");
}
}
if(fossil_timer_is_active(g.json.timerId)){
/* This is, philosophically speaking, not quite the right place
for ending the timer, but this is the one function which all of
the JSON exit paths use (and they call it after processing,
just before they end).
*/
sqlite3_uint64 span = fossil_timer_stop(g.json.timerId);
/* I'm actually seeing sub-uSec runtimes in some tests, but a time of
0 is "just kinda wrong".
*/
cson_object_set(o,"procTimeUs", cson_value_new_integer((cson_int_t)span));
span /= 1000/*for milliseconds */;
cson_object_set(o,"procTimeMs", cson_value_new_integer((cson_int_t)span));
assert(!fossil_timer_is_active(g.json.timerId));
g.json.timerId = -1;
}
if(g.json.warnings){
tmp = cson_array_value(g.json.warnings);
SET("warnings");
}
/* Only add the payload to SUCCESS responses. Else delete it. */
if( NULL != payload ){
if( resultCode ){
cson_value_free(payload);
payload = NULL;
}else{
tmp = payload;
SET(FossilJsonKeys.payload);
}
}
if((g.perm.Admin||g.perm.Setup)
&& json_find_option_bool("debugFossilG","json-debug-g",NULL,0)
){
tmp = json_g_to_json();
SET("g");
}
#undef SET
goto ok;
cleanup:
|
| ︙ | ︙ | |||
1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 |
** is NULL then json_err_cstr(code) is used.
*/
void json_err( int code, char const * msg, int alsoOutput ){
int rc = code ? code : (g.json.resultCode
? g.json.resultCode
: FSL_JSON_E_UNKNOWN);
cson_value * resp = NULL;
rc = json_dumbdown_rc(rc);
if( rc && !msg ){
msg = g.zErrMsg;
if(!msg){
msg = json_err_cstr(rc);
}
}
resp = json_create_response(rc, msg, NULL);
if(!resp){
/* about the only error case here is out-of-memory. DO NOT
| > | > | 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 |
** is NULL then json_err_cstr(code) is used.
*/
void json_err( int code, char const * msg, int alsoOutput ){
int rc = code ? code : (g.json.resultCode
? g.json.resultCode
: FSL_JSON_E_UNKNOWN);
cson_value * resp = NULL;
if(g.json.isJsonMode==0) return;
rc = json_dumbdown_rc(rc);
if( rc && !msg ){
msg = g.zErrMsg;
if(!msg){
msg = json_err_cstr(rc);
}
}
resp = json_create_response(rc, msg, NULL);
if(!resp){
/* about the only error case here is out-of-memory. DO NOT
call fossil_panic() or fossil_fatal() here because those
allocate.
*/
fprintf(stderr, "%s: Fatal error: could not allocate "
"response object.\n", g.argv[0]);
fossil_exit(1);
}
if( g.isHTTP ){
if(alsoOutput){
|
| ︙ | ︙ |
Changes to src/json_branch.c.
| ︙ | ︙ | |||
77 78 79 80 81 82 83 |
return NULL;
}
payV = cson_value_new_object();
pay = cson_value_get_object(payV);
listV = cson_value_new_array();
list = cson_value_get_array(listV);
if(fossil_has_json()){
| | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
return NULL;
}
payV = cson_value_new_object();
pay = cson_value_get_object(payV);
listV = cson_value_new_array();
list = cson_value_get_array(listV);
if(fossil_has_json()){
range = json_getenv_cstr("range");
}
range = json_find_option_cstr("range",NULL,"r");
if((!range||!*range) && !g.isHTTP){
range = find_option("all","a",0);
if(range && *range){
range = "a";
|
| ︙ | ︙ |
Changes to src/main.c.
| ︙ | ︙ | |||
265 266 267 268 269 270 271 |
int nRequest; /* Total # of HTTP request */
#ifdef FOSSIL_ENABLE_JSON
struct FossilJsonBits {
int isJsonMode; /* True if running in JSON mode, else
false. This changes how errors are
reported. In JSON mode we try to
always output JSON-form error
| | | > | 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
int nRequest; /* Total # of HTTP request */
#ifdef FOSSIL_ENABLE_JSON
struct FossilJsonBits {
int isJsonMode; /* True if running in JSON mode, else
false. This changes how errors are
reported. In JSON mode we try to
always output JSON-form error
responses and always (in CGI mode)
exit() with code 0 to avoid an HTTP
500 error.
*/
int resultCode; /* used for passing back specific codes
** from /json callbacks. */
int errorDetailParanoia; /* 0=full error codes, 1=%10, 2=%100, 3=%1000 */
cson_output_opt outOpt; /* formatting options for JSON mode. */
cson_value *authToken; /* authentication token */
const char *jsonp; /* Name of JSONP function wrapper. */
|
| ︙ | ︙ | |||
841 842 843 844 845 846 847 848 849 850 851 852 853 854 |
dispatch_matching_names(zCmdName, &couldbe);
fossil_print("%s: ambiguous command prefix: %s\n"
"%s: could be any of:%s\n"
"%s: use \"help\" for more information\n",
g.argv[0], zCmdName, g.argv[0], blob_str(&couldbe), g.argv[0]);
fossil_exit(1);
}
atexit( fossil_atexit );
#ifdef FOSSIL_ENABLE_TH1_HOOKS
/*
** The TH1 return codes from the hook will be handled as follows:
**
** TH_OK: The xFunc() and the TH1 notification will both be executed.
**
| > > > > > > > | 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 |
dispatch_matching_names(zCmdName, &couldbe);
fossil_print("%s: ambiguous command prefix: %s\n"
"%s: could be any of:%s\n"
"%s: use \"help\" for more information\n",
g.argv[0], zCmdName, g.argv[0], blob_str(&couldbe), g.argv[0]);
fossil_exit(1);
}
#ifdef FOSSIL_ENABLE_JSON
else if( rc==0 && strcmp("json",pCmd->zName)==0 ){
g.json.isJsonMode = 1;
}else{
assert(!g.json.isJsonMode && "JSON-mode misconfiguration.");
}
#endif
atexit( fossil_atexit );
#ifdef FOSSIL_ENABLE_TH1_HOOKS
/*
** The TH1 return codes from the hook will be handled as follows:
**
** TH_OK: The xFunc() and the TH1 notification will both be executed.
**
|
| ︙ | ︙ | |||
1508 1509 1510 1511 1512 1513 1514 |
/* Handle universal query parameters */
if( PB("utc") ){
g.fTimeFormat = 1;
}else if( PB("localtime") ){
g.fTimeFormat = 2;
}
| > > > > > > > > > > > > > | > | 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 |
/* Handle universal query parameters */
if( PB("utc") ){
g.fTimeFormat = 1;
}else if( PB("localtime") ){
g.fTimeFormat = 2;
}
#ifdef FOSSIL_ENABLE_JSON
/*
** Ensure that JSON mode is set up if we're visiting /json, to allow
** us to customize some following behaviour (error handling and only
** process JSON-mode POST data if we're actually in a /json
** page). This is normally set up before this routine is called, but
** it looks like the ssh_request_loop() approach to dispatching
** might bypass that.
*/
if( g.json.isJsonMode==0 && zPathInfo!=0
&& 0==strncmp("/json",zPathInfo,5)
&& (zPathInfo[5]==0 || zPathInfo[5]=='/')){
g.json.isJsonMode = 1;
}
#endif
/* If the repository has not been opened already, then find the
** repository based on the first element of PATH_INFO and open it.
*/
if( !g.repositoryOpen ){
char *zRepo; /* Candidate repository name */
char *zToFree = 0; /* Malloced memory that needs to be freed */
const char *zCleanRepo; /* zRepo with surplus leading "/" removed */
|
| ︙ | ︙ | |||
1639 1640 1641 1642 1643 1644 1645 |
return;
}
zRepo[j] = '.';
}
/* If we reach this point, it means that the search of the PATH_INFO
** string is finished. Either zRepo contains the name of the
| | | 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 |
return;
}
zRepo[j] = '.';
}
/* If we reach this point, it means that the search of the PATH_INFO
** string is finished. Either zRepo contains the name of the
** repository to be used, or else no repository could be found and
** some kind of error response is required.
*/
if( szFile<1024 ){
set_base_url(0);
if( (zPathInfo[0]==0 || strcmp(zPathInfo,"/")==0)
&& allowRepoList
&& repo_list_page() ){
|
| ︙ | ︙ | |||
1770 1771 1772 1773 1774 1775 1776 |
zPath[i] = 0;
g.zExtra = &zPath[i+1];
}else{
g.zExtra = 0;
}
break;
}
| < < < < < < < < < < < | 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 |
zPath[i] = 0;
g.zExtra = &zPath[i+1];
}else{
g.zExtra = 0;
}
break;
}
if( g.zExtra ){
/* CGI parameters get this treatment elsewhere, but places like getfile
** will use g.zExtra directly.
** Reminder: the login mechanism uses 'name' differently, and may
** eventually have a problem/collision with this.
**
** Disabled by stephan when running in JSON mode because this
|
| ︙ | ︙ |
Changes to www/json-api/conventions.md.
| ︙ | ︙ | |||
255 256 257 258 259 260 261 | it's needed. - `warnings`: Reserved for future use as a standard place to put non-fatal warnings in responses. Will be an array but the warning structure/type is not yet specified. Intended primarily as a debugging tool, and will "probably not" become part of the public client interface. - `g`: Fossil administrators (those with the "a" or "s" permissions) | | | | 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | it's needed. - `warnings`: Reserved for future use as a standard place to put non-fatal warnings in responses. Will be an array but the warning structure/type is not yet specified. Intended primarily as a debugging tool, and will "probably not" become part of the public client interface. - `g`: Fossil administrators (those with the "a" or "s" permissions) may set the `debugFossilG` boolean request parameter (CLI: `--json-debug-g`) to enable this property for any given response. It contains a good deal of the server-side internal state at the time the response was generated, which is often useful in debuggering problems. Trivia: it is called "g" because that's the name of fossil's internal global state object. - `procTimeMs`: For debugging only - generic clients must not rely on this property. Contains the number of milliseconds the JSON command processor needed to dispatch and process the command. TODO: move the |
| ︙ | ︙ |