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') % |
|
291 | desc = _('journal of %s') % vfs.base | |
200 | try: |
|
292 | try: | |
201 |
l = lock.lock( |
|
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 |
|
|
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 |
|
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 |
|
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. |
|
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