Fossil

Check-in [3a5caa86]
Login

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

Overview
Comment:Improved error messages when a commit fails due to a lock conflict: Show the age of the lock in addition to the user. Suggest using the new --override-lock option instead of --allow-fork. Expire locks after any successful check-in from the same check-out.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 3a5caa86dbaebba1fd4a956042ebdab2b031f3c26be0870bca7829b4ec2ccea3
User & Date: drh 2019-07-12 13:58:43
Context
2019-07-12
14:08
Add documentation on the "pragma ci-unlock" message in the sync protocol. check-in: 1eefb8db user: drh tags: trunk
13:58
Improved error messages when a commit fails due to a lock conflict: Show the age of the lock in addition to the user. Suggest using the new --override-lock option instead of --allow-fork. Expire locks after any successful check-in from the same check-out. check-in: 3a5caa86 user: drh tags: trunk
13:24
Clarified the scale of the Linux vs SQLite projects in the same-named section of the Git vs Fossil doc, tightened up the discussion a bit, and added hard SLOC stats to ground it all. Most controversially, added the rattle wrench analogy. check-in: 69e7827d user: wyoung tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to src/checkin.c.

2010
2011
2012
2013
2014
2015
2016

2017
2018
2019
2020
2021
2022
2023
....
2086
2087
2088
2089
2090
2091
2092

2093
2094
2095
2096
2097
2098
2099
....
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
**    --mimetype MIMETYPE        mimetype of check-in comment
**    -n|--dry-run               If given, display instead of run actions
**    --no-prompt                This option disables prompting the user for
**                               input and assumes an answer of 'No' for every
**                               question.
**    --no-warnings              omit all warnings about file contents
**    --nosign                   do not attempt to sign this commit with gpg

**    --private                  do not sync changes and their descendants
**    --hash                     verify file status using hashing rather
**                               than relying on file mtimes
**    --tag TAG-NAME             assign given tag TAG-NAME to the check-in
**    --date-override DATETIME   DATE to use instead of 'now'
**    --user-override USER       USER to use instead of the current default
**
................................................................................
    dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
  }
  zComment = find_option("comment","m",1);
  forceFlag = find_option("force", "f", 0)!=0;
  allowConflict = find_option("allow-conflict",0,0)!=0;
  allowEmpty = find_option("allow-empty",0,0)!=0;
  allowFork = find_option("allow-fork",0,0)!=0;

  allowOlder = find_option("allow-older",0,0)!=0;
  noPrompt = find_option("no-prompt", 0, 0)!=0;
  noWarningFlag = find_option("no-warnings", 0, 0)!=0;
  sCiInfo.zBranch = find_option("branch","b",1);
  sCiInfo.zColor = find_option("bgcolor",0,1);
  sCiInfo.zBrClr = find_option("branchcolor",0,1);
  sCiInfo.closeFlag = find_option("close",0,0)!=0;
................................................................................
  }

  /*
  ** Do not allow a commit that will cause a fork unless the --allow-fork
  ** or --force flags is used, or unless this is a private check-in.
  ** The initial commit MUST have tags "trunk" and "sym-trunk".
  */
  if( !vid ){
    if( sCiInfo.zBranch==0 ){
      if( allowFork==0 && forceFlag==0 && g.markPrivate==0
        && db_exists("SELECT 1 from event where type='ci'") ){
        fossil_fatal("Would fork.  \"update\" first, use --branch, or --allow-fork.\n"
          "See https://fossil-scm.org/fossil/doc/trunk/www/branching.wiki#branching");
      }
      sCiInfo.zBranch = db_get("main-branch", "trunk");
    }
  }else if( sCiInfo.zBranch==0 && allowFork==0 && forceFlag==0
    && g.markPrivate==0 && (g.ckinLockFail || !is_a_leaf(vid))
  ){
    /* Can't avoid duplicating this string because some C compilers
    ** refuse to see static const char zErr[] = "... as "constant"
    ** enough for a printf() style format string.  (e.g. Clang 10)
    */
    fossil_fatal("Would fork.  \"update\" first, use --branch or, --allow-fork.\n"
      "See https://fossil-scm.org/fossil/doc/trunk/www/branching.wiki#branching");
  }

  /*
  ** Do not allow a commit against a closed leaf unless the commit
  ** ends up on a different branch.
  */
  if(







>







 







>







 







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







2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
....
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
....
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292




2293
2294
2295
2296
2297
2298
2299
**    --mimetype MIMETYPE        mimetype of check-in comment
**    -n|--dry-run               If given, display instead of run actions
**    --no-prompt                This option disables prompting the user for
**                               input and assumes an answer of 'No' for every
**                               question.
**    --no-warnings              omit all warnings about file contents
**    --nosign                   do not attempt to sign this commit with gpg
**    --override-lock            allow a check-in even though parent is locked
**    --private                  do not sync changes and their descendants
**    --hash                     verify file status using hashing rather
**                               than relying on file mtimes
**    --tag TAG-NAME             assign given tag TAG-NAME to the check-in
**    --date-override DATETIME   DATE to use instead of 'now'
**    --user-override USER       USER to use instead of the current default
**
................................................................................
    dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
  }
  zComment = find_option("comment","m",1);
  forceFlag = find_option("force", "f", 0)!=0;
  allowConflict = find_option("allow-conflict",0,0)!=0;
  allowEmpty = find_option("allow-empty",0,0)!=0;
  allowFork = find_option("allow-fork",0,0)!=0;
  if( find_option("override-lock",0,0)!=0 ) allowFork = 1;
  allowOlder = find_option("allow-older",0,0)!=0;
  noPrompt = find_option("no-prompt", 0, 0)!=0;
  noWarningFlag = find_option("no-warnings", 0, 0)!=0;
  sCiInfo.zBranch = find_option("branch","b",1);
  sCiInfo.zColor = find_option("bgcolor",0,1);
  sCiInfo.zBrClr = find_option("branchcolor",0,1);
  sCiInfo.closeFlag = find_option("close",0,0)!=0;
................................................................................
  }

  /*
  ** Do not allow a commit that will cause a fork unless the --allow-fork
  ** or --force flags is used, or unless this is a private check-in.
  ** The initial commit MUST have tags "trunk" and "sym-trunk".
  */
  if( sCiInfo.zBranch==0
   && allowFork==0
   && forceFlag==0
   && g.markPrivate==0
   && (vid==0 || !is_a_leaf(vid) || g.ckinLockFail)
  ){
    if( g.ckinLockFail ){
      fossil_fatal("Might fork due to a check-in race with user \"%s\"\n"
                   "Try \"update\" first, or --branch, or use --override-lock",
                   g.ckinLockFail);
    }else{
      fossil_fatal("Would fork.  \"update\" first or use --branch or "
                   "--allow-fork.");
    }




  }

  /*
  ** Do not allow a commit against a closed leaf unless the commit
  ** ends up on a different branch.
  */
  if(

Changes to src/main.c.

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
  int th1Flags;           /* The TH1 integration state flags */
  FILE *httpIn;           /* Accept HTTP input from here */
  FILE *httpOut;          /* Send HTTP output here */
  int xlinkClusterOnly;   /* Set when cloning.  Only process clusters */
  int fTimeFormat;        /* 1 for UTC.  2 for localtime.  0 not yet selected */
  int *aCommitFile;       /* Array of files to be committed */
  int markPrivate;        /* All new artifacts are private if true */
  int ckinLockFail;       /* Check-in lock failure received from server */
  int clockSkewSeen;      /* True if clocks on client and server out of sync */
  int wikiFlags;          /* Wiki conversion flags applied to %W */
  char isHTTP;            /* True if server/CGI modes, else assume CLI. */
  char javascriptHyperlink; /* If true, set href= using script, not HTML */
  Blob httpHeader;        /* Complete text of the HTTP request header */
  UrlData url;            /* Information about current URL */
  const char *zLogin;     /* Login name.  NULL or "" if not logged in. */







|







188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
  int th1Flags;           /* The TH1 integration state flags */
  FILE *httpIn;           /* Accept HTTP input from here */
  FILE *httpOut;          /* Send HTTP output here */
  int xlinkClusterOnly;   /* Set when cloning.  Only process clusters */
  int fTimeFormat;        /* 1 for UTC.  2 for localtime.  0 not yet selected */
  int *aCommitFile;       /* Array of files to be committed */
  int markPrivate;        /* All new artifacts are private if true */
  char *ckinLockFail;     /* Check-in lock failure received from server */
  int clockSkewSeen;      /* True if clocks on client and server out of sync */
  int wikiFlags;          /* Wiki conversion flags applied to %W */
  char isHTTP;            /* True if server/CGI modes, else assume CLI. */
  char javascriptHyperlink; /* If true, set href= using script, not HTML */
  Blob httpHeader;        /* Complete text of the HTTP request header */
  UrlData url;            /* Information about current URL */
  const char *zLogin;     /* Login name.  NULL or "" if not logged in. */

Changes to src/xfer.c.

1557
1558
1559
1560
1561
1562
1563

1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
....
1595
1596
1597
1598
1599
1600
1601



















1602
1603
1604
1605
1606
1607
1608
....
1760
1761
1762
1763
1764
1765
1766

1767
1768
1769
1770
1771
1772
1773
....
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
....
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988


1989
1990
1991
1992
1993
1994
1995
....
2354
2355
2356
2357
2358
2359
2360

2361






2362

2363
2364
2365
2366
2367
2368
2369
2370
      */
      if( blob_eq(&xfer.aToken[1], "ci-lock")
       && xfer.nToken==4
       && blob_is_hname(&xfer.aToken[2])
      ){
        Stmt q;
        sqlite3_int64 iNow = time(0);

        int seenFault = 0;
        db_prepare(&q,
          "SELECT json_extract(value,'$.login'),"
          "       mtime,"
          "       json_extract(value,'$.clientid'),"
          "       (SELECT rid FROM blob WHERE uuid=substr(name,9)),"
          "       name"
          " FROM config WHERE name GLOB 'ci-lock-*'"
        );
        while( db_step(&q)==SQLITE_ROW ){
          int x = db_column_int(&q,3);
          const char *zName = db_column_text(&q,4);
          if( db_column_int64(&q,1)<iNow-3600*24 || !is_a_leaf(x) ){
            /* check-in locks expire after 24 hours, or when the check-in
            ** is no longer a leaf */
            db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
            continue;
          }
          if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){
            const char *zClientId = db_column_text(&q, 2);
            const char *zLogin = db_column_text(&q,0);
            sqlite3_int64 mtime = db_column_int64(&q, 1);
................................................................................
            "REPLACE INTO config(name,value,mtime)"
            "VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())",
            blob_str(&xfer.aToken[2]), g.zLogin,
            blob_str(&xfer.aToken[3])
          );
        }
      }



















    }else

    /* Unknown message
    */
    {
      cgi_reset_content();
      @ error bad\scommand:\s%F(blob_str(&xfer.line))
................................................................................
  int uvHashSent = 0;     /* The "pragma uv-hash" message has been sent */
  int uvDoPush = 0;       /* Generate uvfile messages to send to server */
  int nUvGimmeSent = 0;   /* Number of uvgimme cards sent on this cycle */
  int nUvFileRcvd = 0;    /* Number of uvfile cards received on this cycle */
  sqlite3_int64 mtime;    /* Modification time on a UV file */
  int autopushFailed = 0; /* Autopush following commit failed if true */
  const char *zCkinLock;  /* Name of check-in to lock.  NULL for none */


  if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH;
  if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED))==0
     && configRcvMask==0 && configSendMask==0 ) return 0;
  if( syncFlags & SYNC_FROMPARENT ){
    configRcvMask = 0;
    configSendMask = 0;
................................................................................

  blobarray_zero(xfer.aToken, count(xfer.aToken));
  blob_zero(&send);
  blob_zero(&recv);
  blob_zero(&xfer.err);
  blob_zero(&xfer.line);
  origConfigRcvMask = 0;


  /* Send the send-private pragma if we are trying to sync private data */
  if( syncFlags & SYNC_PRIVATE ){
    blob_append(&send, "pragma send-private\n", -1);
  }

  /* Figure out which check-in to lock */
................................................................................
        db_finalize(&uvq);
        if( rc==SQLITE_DONE ) uvDoPush = 0;
      }
    }

    /* Lock the current check-out */
    if( zCkinLock ){
      const char *zClientId;
      zClientId = db_lget("client-id", 0);
      if( zClientId==0 ){
        zClientId = db_text(0, "SELECT lower(hex(randomblob(20)))");
        db_lset("client-id", zClientId);
      }
      blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
      zCkinLock = 0;


    }

    /* Append randomness to the end of the message.  This makes all
    ** messages unique so that that the login-card nonce will always
    ** be unique.
    */
    zRandomness = db_text(0, "SELECT hex(randomblob(20))");
................................................................................
        ** is attempted on a check-in for which there is an existing
        ** lock.  USER-HOLDING-LOCK is the name of the user who originated
        ** the lock, and LOCK-TIME is the timestamp (seconds since 1970)
        ** when the lock was taken.
        */
        else if( blob_eq(&xfer.aToken[1], "ci-lock-fail") && xfer.nToken==4 ){
          char *zUser = blob_terminate(&xfer.aToken[2]);

          defossilize(zUser);






          fossil_print("\nParent check-in locked by %s\n", zUser);

          g.ckinLockFail = 1;
        }
      }else

      /*   error MESSAGE
      **
      ** Report an error and abandon the sync session.
      **







>












|
|
|







 







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







 







>







 







|







 







<
<






>
>







 







>

>
>
>
>
>
>
|
>
|







1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
....
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
....
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
....
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
....
1995
1996
1997
1998
1999
2000
2001


2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
....
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
      */
      if( blob_eq(&xfer.aToken[1], "ci-lock")
       && xfer.nToken==4
       && blob_is_hname(&xfer.aToken[2])
      ){
        Stmt q;
        sqlite3_int64 iNow = time(0);
        const sqlite3_int64 maxAge = 3600*24; /* Locks expire after 24 hours */
        int seenFault = 0;
        db_prepare(&q,
          "SELECT json_extract(value,'$.login'),"
          "       mtime,"
          "       json_extract(value,'$.clientid'),"
          "       (SELECT rid FROM blob WHERE uuid=substr(name,9)),"
          "       name"
          " FROM config WHERE name GLOB 'ci-lock-*'"
        );
        while( db_step(&q)==SQLITE_ROW ){
          int x = db_column_int(&q,3);
          const char *zName = db_column_text(&q,4);
          if( db_column_int64(&q,1)<iNow-maxAge || !is_a_leaf(x) ){
            /* check-in locks expire after maxAge seconds, or when the
            ** check-in is no longer a leaf */
            db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
            continue;
          }
          if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){
            const char *zClientId = db_column_text(&q, 2);
            const char *zLogin = db_column_text(&q,0);
            sqlite3_int64 mtime = db_column_int64(&q, 1);
................................................................................
            "REPLACE INTO config(name,value,mtime)"
            "VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())",
            blob_str(&xfer.aToken[2]), g.zLogin,
            blob_str(&xfer.aToken[3])
          );
        }
      }

      /*   pragma ci-unlock CLIENT-ID
      **
      ** Remove any locks previously held by CLIENT-ID.  Clients send this
      ** pragma with their own ID whenever they know that they no longer
      ** have any commits pending.
      */
      if( blob_eq(&xfer.aToken[1], "ci-unlock")
       && xfer.nToken==3
       && blob_is_hname(&xfer.aToken[2])
      ){
        db_multi_exec(
          "DELETE FROM config"
          " WHERE name GLOB 'ci-lock-*'"
          "   AND json_extract(value,'$.clientid')=%Q",
          blob_str(&xfer.aToken[2])
        );
      }

    }else

    /* Unknown message
    */
    {
      cgi_reset_content();
      @ error bad\scommand:\s%F(blob_str(&xfer.line))
................................................................................
  int uvHashSent = 0;     /* The "pragma uv-hash" message has been sent */
  int uvDoPush = 0;       /* Generate uvfile messages to send to server */
  int nUvGimmeSent = 0;   /* Number of uvgimme cards sent on this cycle */
  int nUvFileRcvd = 0;    /* Number of uvfile cards received on this cycle */
  sqlite3_int64 mtime;    /* Modification time on a UV file */
  int autopushFailed = 0; /* Autopush following commit failed if true */
  const char *zCkinLock;  /* Name of check-in to lock.  NULL for none */
  const char *zClientId;  /* A unique identifier for this check-out */

  if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH;
  if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED))==0
     && configRcvMask==0 && configSendMask==0 ) return 0;
  if( syncFlags & SYNC_FROMPARENT ){
    configRcvMask = 0;
    configSendMask = 0;
................................................................................

  blobarray_zero(xfer.aToken, count(xfer.aToken));
  blob_zero(&send);
  blob_zero(&recv);
  blob_zero(&xfer.err);
  blob_zero(&xfer.line);
  origConfigRcvMask = 0;
  zClientId = db_lget("client-id", 0);

  /* Send the send-private pragma if we are trying to sync private data */
  if( syncFlags & SYNC_PRIVATE ){
    blob_append(&send, "pragma send-private\n", -1);
  }

  /* Figure out which check-in to lock */
................................................................................
        db_finalize(&uvq);
        if( rc==SQLITE_DONE ) uvDoPush = 0;
      }
    }

    /* Lock the current check-out */
    if( zCkinLock ){


      if( zClientId==0 ){
        zClientId = db_text(0, "SELECT lower(hex(randomblob(20)))");
        db_lset("client-id", zClientId);
      }
      blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
      zCkinLock = 0;
    }else if( zClientId ){
      blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
    }

    /* Append randomness to the end of the message.  This makes all
    ** messages unique so that that the login-card nonce will always
    ** be unique.
    */
    zRandomness = db_text(0, "SELECT hex(randomblob(20))");
................................................................................
        ** is attempted on a check-in for which there is an existing
        ** lock.  USER-HOLDING-LOCK is the name of the user who originated
        ** the lock, and LOCK-TIME is the timestamp (seconds since 1970)
        ** when the lock was taken.
        */
        else if( blob_eq(&xfer.aToken[1], "ci-lock-fail") && xfer.nToken==4 ){
          char *zUser = blob_terminate(&xfer.aToken[2]);
          sqlite3_int64 mtime, iNow;
          defossilize(zUser);
          iNow = time(NULL);
          if( blob_is_int64(&xfer.aToken[3], &mtime) && iNow>mtime ){
            iNow = time(NULL);
            fossil_print("\nParent check-in lock by %s %s ago\n",
               zUser, human_readable_age((iNow+1-mtime)/86400.0));
          }else{
            fossil_print("\nParent check-in locked by %s\n", zUser);
          }
          g.ckinLockFail = fossil_strdup(zUser);
        }
      }else

      /*   error MESSAGE
      **
      ** Report an error and abandon the sync session.
      **