commands: make commit acquire locks before processing (issue4368)
Before this patch, "hg commit" (process A) executes steps below:
1. get current branch heads via 'repo.branchheads()'
- cache 'repo.changelog'
2. invoke 'repo.commit()'
3. acquire wlock
- invalidate 'repo.dirstate'
4. access 'repo.dirstate'
- re-read '.hg/dirstate'
- check validity of parent revisions with 'repo.changelog'
5. invoke 'repo.commitctx()'
6. acquire store lock (slock)
- invalidate 'repo.changelog'
7. do committing
8. release slock
9. release wlock
10. check new branch head (via 'cmdutil.commitstatus()')
If acquisition of wlock at (3) above waits for another "hg commit"
(process B) or so running parallelly to release wlock, process A
causes creating orphan revision, because:
- '.hg/dirstate' refers the revision, which is newly added by
process B, as its parent
- but already cached 'repo.changelog' doesn't contain such revision
- therefore, validating parents of '.hg/dirstate' at (4) above
replaces such revision with 'nullid'
Then, process A creates "orphan" revision, of which parent is "null"
revision.
In addition to it, "created new head" may be shown at the end of
process A unintentionally, if store is updated parallelly, because
both getting branch heads (1) and checking new branch head (10) are
executed outside slock scope.
To avoid this issue, this patch makes "hg commit" acquire wlock and
slock before processing.
This patch resolves the issue between "hg commit" processes, but not
one between "hg commit" and other commands. Subsequent patches resolve
the latter.
Even after this patch, there are still corner case problems below:
- filecache may overlook changes of '.hg/dirstate', and it causes
similar issue (see below for detail)
https://bz.mercurial-scm.org/show_bug.cgi?id=4368#c10
- 3rd party extension may cause similar issue, if it directly uses
'repo.commit()' without acquisition of wlock and slock
This can be fixed by acquisition of slock at the beginning of
'repo.commit()', but it seems suitable for "default" branch
In fact, acquisition of slock itself is already introduced at
"default" branch by 4414d500604f, but acquisition is not at the
beginning of 'repo.commit()'.
This patch also changes some tests:
- test-fncache.t needs this tricky wrapping, to release (= forced
failure of) wlock certainly
- order of "hg commit" output is changed by widening scope of locks,
because some hooks are fired after releasing wlock
#require killdaemons
hide outer repo
$ hg init
$ echo "[extensions]" >> $HGRCPATH
$ echo "mq=" >> $HGRCPATH
$ mkdir webdir
$ cd webdir
$ hg init a
$ hg --cwd a qinit -c
$ echo a > a/a
$ hg --cwd a ci -A -m a
adding a
$ echo b > a/b
$ hg --cwd a addremove
adding b
$ hg --cwd a qnew -f b.patch
$ hg --cwd a qcommit -m b.patch
$ hg --cwd a log --template "{desc}\n"
[mq]: b.patch
a
$ hg --cwd a/.hg/patches log --template "{desc}\n"
b.patch
$ root=`pwd`
$ cd ..
test with recursive collection
$ cat > collections.conf <<EOF
> [paths]
> /=$root/**
> EOF
$ hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf collections.conf \
> -A access-paths.log -E error-paths-1.log
$ cat hg.pid >> $DAEMON_PIDS
$ get-with-headers.py localhost:$HGPORT '?style=raw'
200 Script output follows
/a/
/a/.hg/patches/
$ hg qclone http://localhost:$HGPORT/a b
requesting all changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 2 changes to 2 files
requesting all changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 3 changes to 3 files
updating to branch default
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg --cwd b log --template "{desc}\n"
a
$ hg --cwd b qpush -a
applying b.patch
now at: b.patch
$ hg --cwd b log --template "{desc}\n"
imported patch b.patch
a
test with normal collection
$ cat > collections1.conf <<EOF
> [paths]
> /=$root/*
> EOF
$ hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf collections1.conf \
> -A access-paths.log -E error-paths-1.log
$ cat hg.pid >> $DAEMON_PIDS
$ get-with-headers.py localhost:$HGPORT1 '?style=raw'
200 Script output follows
/a/
/a/.hg/patches/
$ hg qclone http://localhost:$HGPORT1/a c
requesting all changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 2 changes to 2 files
requesting all changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 3 changes to 3 files
updating to branch default
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg --cwd c log --template "{desc}\n"
a
$ hg --cwd c qpush -a
applying b.patch
now at: b.patch
$ hg --cwd c log --template "{desc}\n"
imported patch b.patch
a
test with old-style collection
$ cat > collections2.conf <<EOF
> [collections]
> $root=$root
> EOF
$ hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections2.conf \
> -A access-paths.log -E error-paths-1.log
$ cat hg.pid >> $DAEMON_PIDS
$ get-with-headers.py localhost:$HGPORT2 '?style=raw'
200 Script output follows
/a/
/a/.hg/patches/
$ hg qclone http://localhost:$HGPORT2/a d
requesting all changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 2 changes to 2 files
requesting all changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 3 changes to 3 files
updating to branch default
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg --cwd d log --template "{desc}\n"
a
$ hg --cwd d qpush -a
applying b.patch
now at: b.patch
$ hg --cwd d log --template "{desc}\n"
imported patch b.patch
a
test --mq works and uses correct repository config
$ hg --cwd d outgoing --mq
comparing with http://localhost:$HGPORT2/a/.hg/patches
searching for changes
no changes found
[1]
$ hg --cwd d log --mq --template '{rev} {desc|firstline}\n'
0 b.patch
$ killdaemons.py