##// END OF EJS Templates
journal: add share extension support...
Martijn Pieters -
r29503:0103b673 default
parent child Browse files
Show More
@@ -0,0 +1,153 b''
1 Journal extension test: tests the share extension support
2
3 $ cat >> testmocks.py << EOF
4 > # mock out util.getuser() and util.makedate() to supply testable values
5 > import os
6 > from mercurial import util
7 > def mockgetuser():
8 > return 'foobar'
9 >
10 > def mockmakedate():
11 > filename = os.path.join(os.environ['TESTTMP'], 'testtime')
12 > try:
13 > with open(filename, 'rb') as timef:
14 > time = float(timef.read()) + 1
15 > except IOError:
16 > time = 0.0
17 > with open(filename, 'wb') as timef:
18 > timef.write(str(time))
19 > return (time, 0)
20 >
21 > util.getuser = mockgetuser
22 > util.makedate = mockmakedate
23 > EOF
24
25 $ cat >> $HGRCPATH << EOF
26 > [extensions]
27 > journal=
28 > share=
29 > testmocks=`pwd`/testmocks.py
30 > [remotenames]
31 > rename.default=remote
32 > EOF
33
34 $ hg init repo
35 $ cd repo
36 $ hg bookmark bm
37 $ touch file0
38 $ hg commit -Am 'file0 added'
39 adding file0
40 $ hg journal --all
41 previous locations of the working copy and bookmarks:
42 5640b525682e . commit -Am 'file0 added'
43 5640b525682e bm commit -Am 'file0 added'
44
45 A shared working copy initially receives the same bookmarks and working copy
46
47 $ cd ..
48 $ hg share repo shared1
49 updating working directory
50 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
51 $ cd shared1
52 $ hg journal --all
53 previous locations of the working copy and bookmarks:
54 5640b525682e . share repo shared1
55
56 unless you explicitly share bookmarks
57
58 $ cd ..
59 $ hg share --bookmarks repo shared2
60 updating working directory
61 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 $ cd shared2
63 $ hg journal --all
64 previous locations of the working copy and bookmarks:
65 5640b525682e . share --bookmarks repo shared2
66 5640b525682e bm commit -Am 'file0 added'
67
68 Moving the bookmark in the original repository is only shown in the repository
69 that shares bookmarks
70
71 $ cd ../repo
72 $ touch file1
73 $ hg commit -Am "file1 added"
74 adding file1
75 $ cd ../shared1
76 $ hg journal --all
77 previous locations of the working copy and bookmarks:
78 5640b525682e . share repo shared1
79 $ cd ../shared2
80 $ hg journal --all
81 previous locations of the working copy and bookmarks:
82 6432d239ac5d bm commit -Am 'file1 added'
83 5640b525682e . share --bookmarks repo shared2
84 5640b525682e bm commit -Am 'file0 added'
85
86 But working copy changes are always 'local'
87
88 $ cd ../repo
89 $ hg up 0
90 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
91 (leaving bookmark bm)
92 $ hg journal --all
93 previous locations of the working copy and bookmarks:
94 5640b525682e . up 0
95 6432d239ac5d . commit -Am 'file1 added'
96 6432d239ac5d bm commit -Am 'file1 added'
97 5640b525682e . commit -Am 'file0 added'
98 5640b525682e bm commit -Am 'file0 added'
99 $ cd ../shared2
100 $ hg journal --all
101 previous locations of the working copy and bookmarks:
102 6432d239ac5d bm commit -Am 'file1 added'
103 5640b525682e . share --bookmarks repo shared2
104 5640b525682e bm commit -Am 'file0 added'
105 $ hg up tip
106 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
107 $ hg up 0
108 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
109 $ hg journal
110 previous locations of '.':
111 5640b525682e up 0
112 6432d239ac5d up tip
113 5640b525682e share --bookmarks repo shared2
114
115 Unsharing works as expected; the journal remains consistent
116
117 $ cd ../shared1
118 $ hg unshare
119 $ hg journal --all
120 previous locations of the working copy and bookmarks:
121 5640b525682e . share repo shared1
122 $ cd ../shared2
123 $ hg unshare
124 $ hg journal --all
125 previous locations of the working copy and bookmarks:
126 5640b525682e . up 0
127 6432d239ac5d . up tip
128 6432d239ac5d bm commit -Am 'file1 added'
129 5640b525682e . share --bookmarks repo shared2
130 5640b525682e bm commit -Am 'file0 added'
131
132 New journal entries in the source repo no longer show up in the other working copies
133
134 $ cd ../repo
135 $ hg bookmark newbm -r tip
136 $ hg journal newbm
137 previous locations of 'newbm':
138 6432d239ac5d bookmark newbm -r tip
139 $ cd ../shared2
140 $ hg journal newbm
141 previous locations of 'newbm':
142 no recorded locations
143
144 This applies for both directions
145
146 $ hg bookmark shared2bm -r tip
147 $ hg journal shared2bm
148 previous locations of 'shared2bm':
149 6432d239ac5d bookmark shared2bm -r tip
150 $ cd ../repo
151 $ hg journal shared2bm
152 previous locations of 'shared2bm':
153 no recorded locations
@@ -14,6 +14,7 b' bookmarks were previously located.'
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 import collections
16 import collections
17 import errno
17 import os
18 import os
18 import weakref
19 import weakref
19
20
@@ -27,12 +28,15 b' from mercurial import ('
27 dispatch,
28 dispatch,
28 error,
29 error,
29 extensions,
30 extensions,
31 hg,
30 localrepo,
32 localrepo,
31 lock,
33 lock,
32 node,
34 node,
33 util,
35 util,
34 )
36 )
35
37
38 from . import share
39
36 cmdtable = {}
40 cmdtable = {}
37 command = cmdutil.command(cmdtable)
41 command = cmdutil.command(cmdtable)
38
42
@@ -48,6 +52,11 b' storageversion = 0'
48 # namespaces
52 # namespaces
49 bookmarktype = 'bookmark'
53 bookmarktype = 'bookmark'
50 wdirparenttype = 'wdirparent'
54 wdirparenttype = 'wdirparent'
55 # In a shared repository, what shared feature name is used
56 # to indicate this namespace is shared with the source?
57 sharednamespaces = {
58 bookmarktype: hg.sharedbookmarks,
59 }
51
60
52 # Journal recording, register hooks and storage object
61 # Journal recording, register hooks and storage object
53 def extsetup(ui):
62 def extsetup(ui):
@@ -57,6 +66,8 b' def extsetup(ui):'
57 dirstate.dirstate, '_writedirstate', recorddirstateparents)
66 dirstate.dirstate, '_writedirstate', recorddirstateparents)
58 extensions.wrapfunction(
67 extensions.wrapfunction(
59 localrepo.localrepository.dirstate, 'func', wrapdirstate)
68 localrepo.localrepository.dirstate, 'func', wrapdirstate)
69 extensions.wrapfunction(hg, 'postshare', wrappostshare)
70 extensions.wrapfunction(hg, 'copystore', unsharejournal)
60
71
61 def reposetup(ui, repo):
72 def reposetup(ui, repo):
62 if repo.local():
73 if repo.local():
@@ -114,6 +125,74 b' def recordbookmarks(orig, store, fp):'
114 repo.journal.record(bookmarktype, mark, oldvalue, value)
125 repo.journal.record(bookmarktype, mark, oldvalue, value)
115 return orig(store, fp)
126 return orig(store, fp)
116
127
128 # shared repository support
129 def _readsharedfeatures(repo):
130 """A set of shared features for this repository"""
131 try:
132 return set(repo.vfs.read('shared').splitlines())
133 except IOError as inst:
134 if inst.errno != errno.ENOENT:
135 raise
136 return set()
137
138 def _mergeentriesiter(*iterables, **kwargs):
139 """Given a set of sorted iterables, yield the next entry in merged order
140
141 Note that by default entries go from most recent to oldest.
142 """
143 order = kwargs.pop('order', max)
144 iterables = [iter(it) for it in iterables]
145 # this tracks still active iterables; iterables are deleted as they are
146 # exhausted, which is why this is a dictionary and why each entry also
147 # stores the key. Entries are mutable so we can store the next value each
148 # time.
149 iterable_map = {}
150 for key, it in enumerate(iterables):
151 try:
152 iterable_map[key] = [next(it), key, it]
153 except StopIteration:
154 # empty entry, can be ignored
155 pass
156
157 while iterable_map:
158 value, key, it = order(iterable_map.itervalues())
159 yield value
160 try:
161 iterable_map[key][0] = next(it)
162 except StopIteration:
163 # this iterable is empty, remove it from consideration
164 del iterable_map[key]
165
166 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
167 """Mark this shared working copy as sharing journal information"""
168 orig(sourcerepo, destrepo, **kwargs)
169 with destrepo.vfs('shared', 'a') as fp:
170 fp.write('journal\n')
171
172 def unsharejournal(orig, ui, repo, repopath):
173 """Copy shared journal entries into this repo when unsharing"""
174 if (repo.path == repopath and repo.shared() and
175 util.safehasattr(repo, 'journal')):
176 sharedrepo = share._getsrcrepo(repo)
177 sharedfeatures = _readsharedfeatures(repo)
178 if sharedrepo and sharedfeatures > set(['journal']):
179 # there is a shared repository and there are shared journal entries
180 # to copy. move shared date over from source to destination but
181 # move the local file first
182 if repo.vfs.exists('journal'):
183 journalpath = repo.join('journal')
184 util.rename(journalpath, journalpath + '.bak')
185 storage = repo.journal
186 local = storage._open(
187 repo.vfs, filename='journal.bak', _newestfirst=False)
188 shared = (
189 e for e in storage._open(sharedrepo.vfs, _newestfirst=False)
190 if sharednamespaces.get(e.namespace) in sharedfeatures)
191 for entry in _mergeentriesiter(local, shared, order=min):
192 storage._write(repo.vfs, entry)
193
194 return orig(ui, repo, repopath)
195
117 class journalentry(collections.namedtuple(
196 class journalentry(collections.namedtuple(
118 'journalentry',
197 'journalentry',
119 'timestamp user command namespace name oldhashes newhashes')):
198 'timestamp user command namespace name oldhashes newhashes')):
@@ -157,6 +236,10 b' class journalentry(collections.namedtupl'
157 class journalstorage(object):
236 class journalstorage(object):
158 """Storage for journal entries
237 """Storage for journal entries
159
238
239 Entries are divided over two files; one with entries that pertain to the
240 local working copy *only*, and one with entries that are shared across
241 multiple working copies when shared using the share extension.
242
160 Entries are stored with NUL bytes as separators. See the journalentry
243 Entries are stored with NUL bytes as separators. See the journalentry
161 class for the per-entry structure.
244 class for the per-entry structure.
162
245
@@ -175,6 +258,15 b' class journalstorage(object):'
175 self.ui = repo.ui
258 self.ui = repo.ui
176 self.vfs = repo.vfs
259 self.vfs = repo.vfs
177
260
261 # is this working copy using a shared storage?
262 self.sharedfeatures = self.sharedvfs = None
263 if repo.shared():
264 features = _readsharedfeatures(repo)
265 sharedrepo = share._getsrcrepo(repo)
266 if sharedrepo is not None and 'journal' in features:
267 self.sharedvfs = sharedrepo.vfs
268 self.sharedfeatures = features
269
178 # track the current command for recording in journal entries
270 # track the current command for recording in journal entries
179 @property
271 @property
180 def command(self):
272 def command(self):
@@ -192,19 +284,19 b' class journalstorage(object):'
192 # with a non-local repo (cloning for example).
284 # with a non-local repo (cloning for example).
193 cls._currentcommand = fullargs
285 cls._currentcommand = fullargs
194
286
195 def jlock(self):
287 def jlock(self, vfs):
196 """Create a lock for the journal file"""
288 """Create a lock for the journal file"""
197 if self._lockref and self._lockref():
289 if self._lockref and self._lockref():
198 raise error.Abort(_('journal lock does not support nesting'))
290 raise error.Abort(_('journal lock does not support nesting'))
199 desc = _('journal of %s') % self.vfs.base
291 desc = _('journal of %s') % vfs.base
200 try:
292 try:
201 l = lock.lock(self.vfs, 'journal.lock', 0, desc=desc)
293 l = lock.lock(vfs, 'journal.lock', 0, desc=desc)
202 except error.LockHeld as inst:
294 except error.LockHeld as inst:
203 self.ui.warn(
295 self.ui.warn(
204 _("waiting for lock on %s held by %r\n") % (desc, inst.locker))
296 _("waiting for lock on %s held by %r\n") % (desc, inst.locker))
205 # default to 600 seconds timeout
297 # default to 600 seconds timeout
206 l = lock.lock(
298 l = lock.lock(
207 self.vfs, 'journal.lock',
299 vfs, 'journal.lock',
208 int(self.ui.config("ui", "timeout", "600")), desc=desc)
300 int(self.ui.config("ui", "timeout", "600")), desc=desc)
209 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
301 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
210 self._lockref = weakref.ref(l)
302 self._lockref = weakref.ref(l)
@@ -231,10 +323,20 b' class journalstorage(object):'
231 util.makedate(), self.user, self.command, namespace, name,
323 util.makedate(), self.user, self.command, namespace, name,
232 oldhashes, newhashes)
324 oldhashes, newhashes)
233
325
234 with self.jlock():
326 vfs = self.vfs
327 if self.sharedvfs is not None:
328 # write to the shared repository if this feature is being
329 # shared between working copies.
330 if sharednamespaces.get(namespace) in self.sharedfeatures:
331 vfs = self.sharedvfs
332
333 self._write(vfs, entry)
334
335 def _write(self, vfs, entry):
336 with self.jlock(vfs):
235 version = None
337 version = None
236 # open file in amend mode to ensure it is created if missing
338 # open file in amend mode to ensure it is created if missing
237 with self.vfs('journal', mode='a+b', atomictemp=True) as f:
339 with vfs('journal', mode='a+b', atomictemp=True) as f:
238 f.seek(0, os.SEEK_SET)
340 f.seek(0, os.SEEK_SET)
239 # Read just enough bytes to get a version number (up to 2
341 # Read just enough bytes to get a version number (up to 2
240 # digits plus separator)
342 # digits plus separator)
@@ -273,10 +375,23 b' class journalstorage(object):'
273 Yields journalentry instances for each contained journal record.
375 Yields journalentry instances for each contained journal record.
274
376
275 """
377 """
276 if not self.vfs.exists('journal'):
378 local = self._open(self.vfs)
379
380 if self.sharedvfs is None:
381 return local
382
383 # iterate over both local and shared entries, but only those
384 # shared entries that are among the currently shared features
385 shared = (
386 e for e in self._open(self.sharedvfs)
387 if sharednamespaces.get(e.namespace) in self.sharedfeatures)
388 return _mergeentriesiter(local, shared)
389
390 def _open(self, vfs, filename='journal', _newestfirst=True):
391 if not vfs.exists(filename):
277 return
392 return
278
393
279 with self.vfs('journal') as f:
394 with vfs(filename) as f:
280 raw = f.read()
395 raw = f.read()
281
396
282 lines = raw.split('\0')
397 lines = raw.split('\0')
@@ -285,8 +400,12 b' class journalstorage(object):'
285 version = version or _('not available')
400 version = version or _('not available')
286 raise error.Abort(_("unknown journal file version '%s'") % version)
401 raise error.Abort(_("unknown journal file version '%s'") % version)
287
402
288 # Skip the first line, it's a version number. Reverse the rest.
403 # Skip the first line, it's a version number. Normally we iterate over
289 lines = reversed(lines[1:])
404 # these in reverse order to list newest first; only when copying across
405 # a shared storage do we forgo reversing.
406 lines = lines[1:]
407 if _newestfirst:
408 lines = reversed(lines)
290 for line in lines:
409 for line in lines:
291 if not line:
410 if not line:
292 continue
411 continue
General Comments 0
You need to be logged in to leave comments. Login now