Fossil SCM

Steps to prevent accidental forks during a commit when two users try to commit to the same parent at the same time. The first user to do the autosync pull takes a lock which causes the second user to get a "would fork" error, unless the --allow-fork or --force options are used.

drh 2019-06-29 03:17 trunk merge
Commit 03cc3329384cd0c12eab9969fb77fea2a6ff85ac18a91887abb0f510fe090115
+6 -2
--- src/checkin.c
+++ src/checkin.c
@@ -2161,11 +2161,15 @@
21612161
21622162
/*
21632163
** Autosync if autosync is enabled and this is not a private check-in.
21642164
*/
21652165
if( !g.markPrivate ){
2166
- if( autosync_loop(SYNC_PULL, db_get_int("autosync-tries", 1), 1) ){
2166
+ int syncFlags = SYNC_PULL;
2167
+ if( vid!=0 && !allowFork && !forceFlag ){
2168
+ syncFlags |= SYNC_CKIN_LOCK;
2169
+ }
2170
+ if( autosync_loop(syncFlags, db_get_int("autosync-tries", 1), 1) ){
21672171
fossil_exit(1);
21682172
}
21692173
}
21702174
21712175
/* Require confirmation to continue with the check-in if there is
@@ -2278,11 +2282,11 @@
22782282
"See https://fossil-scm.org/fossil/doc/trunk/www/branching.wiki#branching");
22792283
}
22802284
sCiInfo.zBranch = db_get("main-branch", "trunk");
22812285
}
22822286
}else if( sCiInfo.zBranch==0 && allowFork==0 && forceFlag==0
2283
- && g.markPrivate==0 && !is_a_leaf(vid)
2287
+ && g.markPrivate==0 && (g.ckinLockFail || !is_a_leaf(vid))
22842288
){
22852289
/* Can't avoid duplicating this string because some C compilers
22862290
** refuse to see static const char zErr[] = "... as "constant"
22872291
** enough for a printf() style format string. (e.g. Clang 10)
22882292
*/
22892293
--- src/checkin.c
+++ src/checkin.c
@@ -2161,11 +2161,15 @@
2161
2162 /*
2163 ** Autosync if autosync is enabled and this is not a private check-in.
2164 */
2165 if( !g.markPrivate ){
2166 if( autosync_loop(SYNC_PULL, db_get_int("autosync-tries", 1), 1) ){
 
 
 
 
2167 fossil_exit(1);
2168 }
2169 }
2170
2171 /* Require confirmation to continue with the check-in if there is
@@ -2278,11 +2282,11 @@
2278 "See https://fossil-scm.org/fossil/doc/trunk/www/branching.wiki#branching");
2279 }
2280 sCiInfo.zBranch = db_get("main-branch", "trunk");
2281 }
2282 }else if( sCiInfo.zBranch==0 && allowFork==0 && forceFlag==0
2283 && g.markPrivate==0 && !is_a_leaf(vid)
2284 ){
2285 /* Can't avoid duplicating this string because some C compilers
2286 ** refuse to see static const char zErr[] = "... as "constant"
2287 ** enough for a printf() style format string. (e.g. Clang 10)
2288 */
2289
--- src/checkin.c
+++ src/checkin.c
@@ -2161,11 +2161,15 @@
2161
2162 /*
2163 ** Autosync if autosync is enabled and this is not a private check-in.
2164 */
2165 if( !g.markPrivate ){
2166 int syncFlags = SYNC_PULL;
2167 if( vid!=0 && !allowFork && !forceFlag ){
2168 syncFlags |= SYNC_CKIN_LOCK;
2169 }
2170 if( autosync_loop(syncFlags, db_get_int("autosync-tries", 1), 1) ){
2171 fossil_exit(1);
2172 }
2173 }
2174
2175 /* Require confirmation to continue with the check-in if there is
@@ -2278,11 +2282,11 @@
2282 "See https://fossil-scm.org/fossil/doc/trunk/www/branching.wiki#branching");
2283 }
2284 sCiInfo.zBranch = db_get("main-branch", "trunk");
2285 }
2286 }else if( sCiInfo.zBranch==0 && allowFork==0 && forceFlag==0
2287 && g.markPrivate==0 && (g.ckinLockFail || !is_a_leaf(vid))
2288 ){
2289 /* Can't avoid duplicating this string because some C compilers
2290 ** refuse to see static const char zErr[] = "... as "constant"
2291 ** enough for a printf() style format string. (e.g. Clang 10)
2292 */
2293
+1
--- src/main.c
+++ src/main.c
@@ -190,10 +190,11 @@
190190
FILE *httpOut; /* Send HTTP output here */
191191
int xlinkClusterOnly; /* Set when cloning. Only process clusters */
192192
int fTimeFormat; /* 1 for UTC. 2 for localtime. 0 not yet selected */
193193
int *aCommitFile; /* Array of files to be committed */
194194
int markPrivate; /* All new artifacts are private if true */
195
+ int ckinLockFail; /* Check-in lock failure received from server */
195196
int clockSkewSeen; /* True if clocks on client and server out of sync */
196197
int wikiFlags; /* Wiki conversion flags applied to %W */
197198
char isHTTP; /* True if server/CGI modes, else assume CLI. */
198199
char javascriptHyperlink; /* If true, set href= using script, not HTML */
199200
Blob httpHeader; /* Complete text of the HTTP request header */
200201
--- src/main.c
+++ src/main.c
@@ -190,10 +190,11 @@
190 FILE *httpOut; /* Send HTTP output here */
191 int xlinkClusterOnly; /* Set when cloning. Only process clusters */
192 int fTimeFormat; /* 1 for UTC. 2 for localtime. 0 not yet selected */
193 int *aCommitFile; /* Array of files to be committed */
194 int markPrivate; /* All new artifacts are private if true */
 
195 int clockSkewSeen; /* True if clocks on client and server out of sync */
196 int wikiFlags; /* Wiki conversion flags applied to %W */
197 char isHTTP; /* True if server/CGI modes, else assume CLI. */
198 char javascriptHyperlink; /* If true, set href= using script, not HTML */
199 Blob httpHeader; /* Complete text of the HTTP request header */
200
--- src/main.c
+++ src/main.c
@@ -190,10 +190,11 @@
190 FILE *httpOut; /* Send HTTP output here */
191 int xlinkClusterOnly; /* Set when cloning. Only process clusters */
192 int fTimeFormat; /* 1 for UTC. 2 for localtime. 0 not yet selected */
193 int *aCommitFile; /* Array of files to be committed */
194 int markPrivate; /* All new artifacts are private if true */
195 int ckinLockFail; /* Check-in lock failure received from server */
196 int clockSkewSeen; /* True if clocks on client and server out of sync */
197 int wikiFlags; /* Wiki conversion flags applied to %W */
198 char isHTTP; /* True if server/CGI modes, else assume CLI. */
199 char javascriptHyperlink; /* If true, set href= using script, not HTML */
200 Blob httpHeader; /* Complete text of the HTTP request header */
201
+89
--- src/xfer.c
+++ src/xfer.c
@@ -1545,10 +1545,62 @@
15451545
send_unversioned_catalog(&xfer);
15461546
}
15471547
}
15481548
uvCatalogSent = 1;
15491549
}
1550
+
1551
+ /* pragma ci-lock CHECKIN-HASH CLIENT-ID
1552
+ **
1553
+ ** The client wants to make non-branch commit against the check-in
1554
+ ** identified by CHECKIN-HASH. The server will remember this and
1555
+ ** subsequent ci-lock request from different clients will generate
1556
+ ** a ci-lock-fail pragma in the reply.
1557
+ */
1558
+ if( blob_eq(&xfer.aToken[1], "ci-lock")
1559
+ && xfer.nToken==4
1560
+ && blob_is_hname(&xfer.aToken[2])
1561
+ ){
1562
+ Stmt q;
1563
+ sqlite3_int64 iNow = time(0);
1564
+ int seenFault = 0;
1565
+ db_prepare(&q,
1566
+ "SELECT json_extract(value,'$.login'),"
1567
+ " mtime,"
1568
+ " json_extract(value,'$.clientid'),"
1569
+ " (SELECT rid FROM blob WHERE uuid=substr(name,9)),"
1570
+ " name"
1571
+ " FROM config WHERE name GLOB 'ci-lock-*'"
1572
+ );
1573
+ while( db_step(&q)==SQLITE_ROW ){
1574
+ int x = db_column_int(&q,3);
1575
+ const char *zName = db_column_text(&q,4);
1576
+ if( db_column_int64(&q,1)<iNow-3600*24 || !is_a_leaf(x) ){
1577
+ /* check-in locks expire after 24 hours, or when the check-in
1578
+ ** is no longer a leaf */
1579
+ db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
1580
+ continue;
1581
+ }
1582
+ if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){
1583
+ const char *zClientId = db_column_text(&q, 2);
1584
+ const char *zLogin = db_column_text(&q,0);
1585
+ sqlite3_int64 mtime = db_column_int64(&q, 1);
1586
+ if( fossil_strcmp(zClientId, blob_str(&xfer.aToken[3]))!=0 ){
1587
+ @ pragma ci-lock-fail %F(zLogin) %lld(mtime)
1588
+ }
1589
+ seenFault = 1;
1590
+ }
1591
+ }
1592
+ db_finalize(&q);
1593
+ if( !seenFault ){
1594
+ db_multi_exec(
1595
+ "REPLACE INTO config(name,value,mtime)"
1596
+ "VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())",
1597
+ blob_str(&xfer.aToken[2]), g.zLogin,
1598
+ blob_str(&xfer.aToken[3])
1599
+ );
1600
+ }
1601
+ }
15501602
}else
15511603
15521604
/* Unknown message
15531605
*/
15541606
{
@@ -1655,10 +1707,11 @@
16551707
#define SYNC_UV_REVERT 0x0080 /* Copy server unversioned to client */
16561708
#define SYNC_FROMPARENT 0x0100 /* Pull from the parent project */
16571709
#define SYNC_UV_TRACE 0x0200 /* Describe UV activities */
16581710
#define SYNC_UV_DRYRUN 0x0400 /* Do not actually exchange files */
16591711
#define SYNC_IFABLE 0x0800 /* Inability to sync is not fatal */
1712
+#define SYNC_CKIN_LOCK 0x1000 /* Lock the current check-in */
16601713
#endif
16611714
16621715
/*
16631716
** Floating-point absolute value
16641717
*/
@@ -1708,10 +1761,11 @@
17081761
int uvDoPush = 0; /* Generate uvfile messages to send to server */
17091762
int nUvGimmeSent = 0; /* Number of uvgimme cards sent on this cycle */
17101763
int nUvFileRcvd = 0; /* Number of uvfile cards received on this cycle */
17111764
sqlite3_int64 mtime; /* Modification time on a UV file */
17121765
int autopushFailed = 0; /* Autopush following commit failed if true */
1766
+ const char *zCkinLock; /* Name of check-in to lock. NULL for none */
17131767
17141768
if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH;
17151769
if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED))==0
17161770
&& configRcvMask==0 && configSendMask==0 ) return 0;
17171771
if( syncFlags & SYNC_FROMPARENT ){
@@ -1749,10 +1803,18 @@
17491803
17501804
/* Send the send-private pragma if we are trying to sync private data */
17511805
if( syncFlags & SYNC_PRIVATE ){
17521806
blob_append(&send, "pragma send-private\n", -1);
17531807
}
1808
+
1809
+ /* Figure out which check-in to lock */
1810
+ if( syncFlags & SYNC_CKIN_LOCK ){
1811
+ int vid = db_lget_int("checkout",0);
1812
+ zCkinLock = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid);
1813
+ }else{
1814
+ zCkinLock = 0;
1815
+ }
17541816
17551817
/* When syncing unversioned files, create a TEMP table in which to store
17561818
** the names of files that need to be sent from client to server.
17571819
**
17581820
** The initial assumption is that all unversioned files need to be sent
@@ -1911,10 +1973,22 @@
19111973
}
19121974
db_finalize(&uvq);
19131975
if( rc==SQLITE_DONE ) uvDoPush = 0;
19141976
}
19151977
}
1978
+
1979
+ /* Lock the current check-out */
1980
+ if( zCkinLock ){
1981
+ const char *zClientId;
1982
+ zClientId = db_lget("client-id", 0);
1983
+ if( zClientId==0 ){
1984
+ zClientId = db_text(0, "SELECT lower(hex(randomblob(20)))");
1985
+ db_lset("client-id", zClientId);
1986
+ }
1987
+ blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
1988
+ zCkinLock = 0;
1989
+ }
19161990
19171991
/* Append randomness to the end of the message. This makes all
19181992
** messages unique so that that the login-card nonce will always
19191993
** be unique.
19201994
*/
@@ -2271,10 +2345,25 @@
22712345
if( blob_eq(&xfer.aToken[1], "uv-pull-only") ){
22722346
if( syncFlags & SYNC_UV_REVERT ) uvDoPush = 1;
22732347
}else if( blob_eq(&xfer.aToken[1], "uv-push-ok") ){
22742348
uvDoPush = 1;
22752349
}
2350
+
2351
+ /* pragma ci-lock-fail USER-HOLDING-LOCK LOCK-TIME
2352
+ **
2353
+ ** The server generates this message when a "pragma ci-lock"
2354
+ ** is attempted on a check-in for which there is an existing
2355
+ ** lock. USER-HOLDING-LOCK is the name of the user who originated
2356
+ ** the lock, and LOCK-TIME is the timestamp (seconds since 1970)
2357
+ ** when the lock was taken.
2358
+ */
2359
+ else if( blob_eq(&xfer.aToken[1], "ci-lock-fail") && xfer.nToken==4 ){
2360
+ char *zUser = blob_terminate(&xfer.aToken[2]);
2361
+ defossilize(zUser);
2362
+ fossil_print("\nParent check-in locked by %s\n", zUser);
2363
+ g.ckinLockFail = 1;
2364
+ }
22762365
}else
22772366
22782367
/* error MESSAGE
22792368
**
22802369
** Report an error and abandon the sync session.
22812370
--- src/xfer.c
+++ src/xfer.c
@@ -1545,10 +1545,62 @@
1545 send_unversioned_catalog(&xfer);
1546 }
1547 }
1548 uvCatalogSent = 1;
1549 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1550 }else
1551
1552 /* Unknown message
1553 */
1554 {
@@ -1655,10 +1707,11 @@
1655 #define SYNC_UV_REVERT 0x0080 /* Copy server unversioned to client */
1656 #define SYNC_FROMPARENT 0x0100 /* Pull from the parent project */
1657 #define SYNC_UV_TRACE 0x0200 /* Describe UV activities */
1658 #define SYNC_UV_DRYRUN 0x0400 /* Do not actually exchange files */
1659 #define SYNC_IFABLE 0x0800 /* Inability to sync is not fatal */
 
1660 #endif
1661
1662 /*
1663 ** Floating-point absolute value
1664 */
@@ -1708,10 +1761,11 @@
1708 int uvDoPush = 0; /* Generate uvfile messages to send to server */
1709 int nUvGimmeSent = 0; /* Number of uvgimme cards sent on this cycle */
1710 int nUvFileRcvd = 0; /* Number of uvfile cards received on this cycle */
1711 sqlite3_int64 mtime; /* Modification time on a UV file */
1712 int autopushFailed = 0; /* Autopush following commit failed if true */
 
1713
1714 if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH;
1715 if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED))==0
1716 && configRcvMask==0 && configSendMask==0 ) return 0;
1717 if( syncFlags & SYNC_FROMPARENT ){
@@ -1749,10 +1803,18 @@
1749
1750 /* Send the send-private pragma if we are trying to sync private data */
1751 if( syncFlags & SYNC_PRIVATE ){
1752 blob_append(&send, "pragma send-private\n", -1);
1753 }
 
 
 
 
 
 
 
 
1754
1755 /* When syncing unversioned files, create a TEMP table in which to store
1756 ** the names of files that need to be sent from client to server.
1757 **
1758 ** The initial assumption is that all unversioned files need to be sent
@@ -1911,10 +1973,22 @@
1911 }
1912 db_finalize(&uvq);
1913 if( rc==SQLITE_DONE ) uvDoPush = 0;
1914 }
1915 }
 
 
 
 
 
 
 
 
 
 
 
 
1916
1917 /* Append randomness to the end of the message. This makes all
1918 ** messages unique so that that the login-card nonce will always
1919 ** be unique.
1920 */
@@ -2271,10 +2345,25 @@
2271 if( blob_eq(&xfer.aToken[1], "uv-pull-only") ){
2272 if( syncFlags & SYNC_UV_REVERT ) uvDoPush = 1;
2273 }else if( blob_eq(&xfer.aToken[1], "uv-push-ok") ){
2274 uvDoPush = 1;
2275 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2276 }else
2277
2278 /* error MESSAGE
2279 **
2280 ** Report an error and abandon the sync session.
2281
--- src/xfer.c
+++ src/xfer.c
@@ -1545,10 +1545,62 @@
1545 send_unversioned_catalog(&xfer);
1546 }
1547 }
1548 uvCatalogSent = 1;
1549 }
1550
1551 /* pragma ci-lock CHECKIN-HASH CLIENT-ID
1552 **
1553 ** The client wants to make non-branch commit against the check-in
1554 ** identified by CHECKIN-HASH. The server will remember this and
1555 ** subsequent ci-lock request from different clients will generate
1556 ** a ci-lock-fail pragma in the reply.
1557 */
1558 if( blob_eq(&xfer.aToken[1], "ci-lock")
1559 && xfer.nToken==4
1560 && blob_is_hname(&xfer.aToken[2])
1561 ){
1562 Stmt q;
1563 sqlite3_int64 iNow = time(0);
1564 int seenFault = 0;
1565 db_prepare(&q,
1566 "SELECT json_extract(value,'$.login'),"
1567 " mtime,"
1568 " json_extract(value,'$.clientid'),"
1569 " (SELECT rid FROM blob WHERE uuid=substr(name,9)),"
1570 " name"
1571 " FROM config WHERE name GLOB 'ci-lock-*'"
1572 );
1573 while( db_step(&q)==SQLITE_ROW ){
1574 int x = db_column_int(&q,3);
1575 const char *zName = db_column_text(&q,4);
1576 if( db_column_int64(&q,1)<iNow-3600*24 || !is_a_leaf(x) ){
1577 /* check-in locks expire after 24 hours, or when the check-in
1578 ** is no longer a leaf */
1579 db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
1580 continue;
1581 }
1582 if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){
1583 const char *zClientId = db_column_text(&q, 2);
1584 const char *zLogin = db_column_text(&q,0);
1585 sqlite3_int64 mtime = db_column_int64(&q, 1);
1586 if( fossil_strcmp(zClientId, blob_str(&xfer.aToken[3]))!=0 ){
1587 @ pragma ci-lock-fail %F(zLogin) %lld(mtime)
1588 }
1589 seenFault = 1;
1590 }
1591 }
1592 db_finalize(&q);
1593 if( !seenFault ){
1594 db_multi_exec(
1595 "REPLACE INTO config(name,value,mtime)"
1596 "VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())",
1597 blob_str(&xfer.aToken[2]), g.zLogin,
1598 blob_str(&xfer.aToken[3])
1599 );
1600 }
1601 }
1602 }else
1603
1604 /* Unknown message
1605 */
1606 {
@@ -1655,10 +1707,11 @@
1707 #define SYNC_UV_REVERT 0x0080 /* Copy server unversioned to client */
1708 #define SYNC_FROMPARENT 0x0100 /* Pull from the parent project */
1709 #define SYNC_UV_TRACE 0x0200 /* Describe UV activities */
1710 #define SYNC_UV_DRYRUN 0x0400 /* Do not actually exchange files */
1711 #define SYNC_IFABLE 0x0800 /* Inability to sync is not fatal */
1712 #define SYNC_CKIN_LOCK 0x1000 /* Lock the current check-in */
1713 #endif
1714
1715 /*
1716 ** Floating-point absolute value
1717 */
@@ -1708,10 +1761,11 @@
1761 int uvDoPush = 0; /* Generate uvfile messages to send to server */
1762 int nUvGimmeSent = 0; /* Number of uvgimme cards sent on this cycle */
1763 int nUvFileRcvd = 0; /* Number of uvfile cards received on this cycle */
1764 sqlite3_int64 mtime; /* Modification time on a UV file */
1765 int autopushFailed = 0; /* Autopush following commit failed if true */
1766 const char *zCkinLock; /* Name of check-in to lock. NULL for none */
1767
1768 if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH;
1769 if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED))==0
1770 && configRcvMask==0 && configSendMask==0 ) return 0;
1771 if( syncFlags & SYNC_FROMPARENT ){
@@ -1749,10 +1803,18 @@
1803
1804 /* Send the send-private pragma if we are trying to sync private data */
1805 if( syncFlags & SYNC_PRIVATE ){
1806 blob_append(&send, "pragma send-private\n", -1);
1807 }
1808
1809 /* Figure out which check-in to lock */
1810 if( syncFlags & SYNC_CKIN_LOCK ){
1811 int vid = db_lget_int("checkout",0);
1812 zCkinLock = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid);
1813 }else{
1814 zCkinLock = 0;
1815 }
1816
1817 /* When syncing unversioned files, create a TEMP table in which to store
1818 ** the names of files that need to be sent from client to server.
1819 **
1820 ** The initial assumption is that all unversioned files need to be sent
@@ -1911,10 +1973,22 @@
1973 }
1974 db_finalize(&uvq);
1975 if( rc==SQLITE_DONE ) uvDoPush = 0;
1976 }
1977 }
1978
1979 /* Lock the current check-out */
1980 if( zCkinLock ){
1981 const char *zClientId;
1982 zClientId = db_lget("client-id", 0);
1983 if( zClientId==0 ){
1984 zClientId = db_text(0, "SELECT lower(hex(randomblob(20)))");
1985 db_lset("client-id", zClientId);
1986 }
1987 blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
1988 zCkinLock = 0;
1989 }
1990
1991 /* Append randomness to the end of the message. This makes all
1992 ** messages unique so that that the login-card nonce will always
1993 ** be unique.
1994 */
@@ -2271,10 +2345,25 @@
2345 if( blob_eq(&xfer.aToken[1], "uv-pull-only") ){
2346 if( syncFlags & SYNC_UV_REVERT ) uvDoPush = 1;
2347 }else if( blob_eq(&xfer.aToken[1], "uv-push-ok") ){
2348 uvDoPush = 1;
2349 }
2350
2351 /* pragma ci-lock-fail USER-HOLDING-LOCK LOCK-TIME
2352 **
2353 ** The server generates this message when a "pragma ci-lock"
2354 ** is attempted on a check-in for which there is an existing
2355 ** lock. USER-HOLDING-LOCK is the name of the user who originated
2356 ** the lock, and LOCK-TIME is the timestamp (seconds since 1970)
2357 ** when the lock was taken.
2358 */
2359 else if( blob_eq(&xfer.aToken[1], "ci-lock-fail") && xfer.nToken==4 ){
2360 char *zUser = blob_terminate(&xfer.aToken[2]);
2361 defossilize(zUser);
2362 fossil_print("\nParent check-in locked by %s\n", zUser);
2363 g.ckinLockFail = 1;
2364 }
2365 }else
2366
2367 /* error MESSAGE
2368 **
2369 ** Report an error and abandon the sync session.
2370

Keyboard Shortcuts

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