##// END OF EJS Templates
journal: track bookmark deletion...
marmoute -
r51701:e9a2e1c7 default
parent child Browse files
Show More
@@ -1,607 +1,610 b''
1 # journal.py
1 # journal.py
2 #
2 #
3 # Copyright 2014-2016 Facebook, Inc.
3 # Copyright 2014-2016 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """track previous positions of bookmarks (EXPERIMENTAL)
7 """track previous positions of bookmarks (EXPERIMENTAL)
8
8
9 This extension adds a new command: `hg journal`, which shows you where
9 This extension adds a new command: `hg journal`, which shows you where
10 bookmarks were previously located.
10 bookmarks were previously located.
11
11
12 """
12 """
13
13
14
14
15 import collections
15 import collections
16 import os
16 import os
17 import weakref
17 import weakref
18
18
19 from mercurial.i18n import _
19 from mercurial.i18n import _
20 from mercurial.node import (
20 from mercurial.node import (
21 bin,
21 bin,
22 hex,
22 hex,
23 )
23 )
24
24
25 from mercurial import (
25 from mercurial import (
26 bookmarks,
26 bookmarks,
27 cmdutil,
27 cmdutil,
28 dispatch,
28 dispatch,
29 encoding,
29 encoding,
30 error,
30 error,
31 extensions,
31 extensions,
32 hg,
32 hg,
33 localrepo,
33 localrepo,
34 lock,
34 lock,
35 logcmdutil,
35 logcmdutil,
36 pycompat,
36 pycompat,
37 registrar,
37 registrar,
38 util,
38 util,
39 )
39 )
40 from mercurial.utils import (
40 from mercurial.utils import (
41 dateutil,
41 dateutil,
42 procutil,
42 procutil,
43 stringutil,
43 stringutil,
44 )
44 )
45
45
46 cmdtable = {}
46 cmdtable = {}
47 command = registrar.command(cmdtable)
47 command = registrar.command(cmdtable)
48
48
49 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
49 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
50 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
50 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
51 # be specifying the version(s) of Mercurial they are tested with, or
51 # be specifying the version(s) of Mercurial they are tested with, or
52 # leave the attribute unspecified.
52 # leave the attribute unspecified.
53 testedwith = b'ships-with-hg-core'
53 testedwith = b'ships-with-hg-core'
54
54
55 # storage format version; increment when the format changes
55 # storage format version; increment when the format changes
56 storageversion = 0
56 storageversion = 0
57
57
58 # namespaces
58 # namespaces
59 bookmarktype = b'bookmark'
59 bookmarktype = b'bookmark'
60 wdirparenttype = b'wdirparent'
60 wdirparenttype = b'wdirparent'
61 # In a shared repository, what shared feature name is used
61 # In a shared repository, what shared feature name is used
62 # to indicate this namespace is shared with the source?
62 # to indicate this namespace is shared with the source?
63 sharednamespaces = {
63 sharednamespaces = {
64 bookmarktype: hg.sharedbookmarks,
64 bookmarktype: hg.sharedbookmarks,
65 }
65 }
66
66
67 # Journal recording, register hooks and storage object
67 # Journal recording, register hooks and storage object
68 def extsetup(ui):
68 def extsetup(ui):
69 extensions.wrapfunction(dispatch, 'runcommand', runcommand)
69 extensions.wrapfunction(dispatch, 'runcommand', runcommand)
70 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
70 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
71 extensions.wrapfilecache(
71 extensions.wrapfilecache(
72 localrepo.localrepository, b'dirstate', wrapdirstate
72 localrepo.localrepository, b'dirstate', wrapdirstate
73 )
73 )
74 extensions.wrapfunction(hg, 'postshare', wrappostshare)
74 extensions.wrapfunction(hg, 'postshare', wrappostshare)
75 extensions.wrapfunction(hg, 'copystore', unsharejournal)
75 extensions.wrapfunction(hg, 'copystore', unsharejournal)
76
76
77
77
78 def reposetup(ui, repo):
78 def reposetup(ui, repo):
79 if repo.local():
79 if repo.local():
80 repo.journal = journalstorage(repo)
80 repo.journal = journalstorage(repo)
81 repo._wlockfreeprefix.add(b'namejournal')
81 repo._wlockfreeprefix.add(b'namejournal')
82
82
83 dirstate, cached = localrepo.isfilecached(repo, b'dirstate')
83 dirstate, cached = localrepo.isfilecached(repo, b'dirstate')
84 if cached:
84 if cached:
85 # already instantiated dirstate isn't yet marked as
85 # already instantiated dirstate isn't yet marked as
86 # "journal"-ing, even though repo.dirstate() was already
86 # "journal"-ing, even though repo.dirstate() was already
87 # wrapped by own wrapdirstate()
87 # wrapped by own wrapdirstate()
88 _setupdirstate(repo, dirstate)
88 _setupdirstate(repo, dirstate)
89
89
90
90
91 def runcommand(orig, lui, repo, cmd, fullargs, *args):
91 def runcommand(orig, lui, repo, cmd, fullargs, *args):
92 """Track the command line options for recording in the journal"""
92 """Track the command line options for recording in the journal"""
93 journalstorage.recordcommand(*fullargs)
93 journalstorage.recordcommand(*fullargs)
94 return orig(lui, repo, cmd, fullargs, *args)
94 return orig(lui, repo, cmd, fullargs, *args)
95
95
96
96
97 def _setupdirstate(repo, dirstate):
97 def _setupdirstate(repo, dirstate):
98 dirstate.journalstorage = repo.journal
98 dirstate.journalstorage = repo.journal
99 dirstate.addparentchangecallback(b'journal', recorddirstateparents)
99 dirstate.addparentchangecallback(b'journal', recorddirstateparents)
100
100
101
101
102 # hooks to record dirstate changes
102 # hooks to record dirstate changes
103 def wrapdirstate(orig, repo):
103 def wrapdirstate(orig, repo):
104 """Make journal storage available to the dirstate object"""
104 """Make journal storage available to the dirstate object"""
105 dirstate = orig(repo)
105 dirstate = orig(repo)
106 if util.safehasattr(repo, 'journal'):
106 if util.safehasattr(repo, 'journal'):
107 _setupdirstate(repo, dirstate)
107 _setupdirstate(repo, dirstate)
108 return dirstate
108 return dirstate
109
109
110
110
111 def recorddirstateparents(dirstate, old, new):
111 def recorddirstateparents(dirstate, old, new):
112 """Records all dirstate parent changes in the journal."""
112 """Records all dirstate parent changes in the journal."""
113 old = list(old)
113 old = list(old)
114 new = list(new)
114 new = list(new)
115 if util.safehasattr(dirstate, 'journalstorage'):
115 if util.safehasattr(dirstate, 'journalstorage'):
116 # only record two hashes if there was a merge
116 # only record two hashes if there was a merge
117 oldhashes = old[:1] if old[1] == dirstate._nodeconstants.nullid else old
117 oldhashes = old[:1] if old[1] == dirstate._nodeconstants.nullid else old
118 newhashes = new[:1] if new[1] == dirstate._nodeconstants.nullid else new
118 newhashes = new[:1] if new[1] == dirstate._nodeconstants.nullid else new
119 dirstate.journalstorage.record(
119 dirstate.journalstorage.record(
120 wdirparenttype, b'.', oldhashes, newhashes
120 wdirparenttype, b'.', oldhashes, newhashes
121 )
121 )
122
122
123
123
124 # hooks to record bookmark changes (both local and remote)
124 # hooks to record bookmark changes (both local and remote)
125 def recordbookmarks(orig, store, fp):
125 def recordbookmarks(orig, store, fp):
126 """Records all bookmark changes in the journal."""
126 """Records all bookmark changes in the journal."""
127 repo = store._repo
127 repo = store._repo
128 if util.safehasattr(repo, 'journal'):
128 if util.safehasattr(repo, 'journal'):
129 oldmarks = bookmarks.bmstore(repo)
129 oldmarks = bookmarks.bmstore(repo)
130 for mark, value in store.items():
130 all_marks = set(b for b, n in oldmarks.items())
131 all_marks.update(b for b, n in store.items())
132 for mark in sorted(all_marks):
133 value = store.get(mark, repo.nullid)
131 oldvalue = oldmarks.get(mark, repo.nullid)
134 oldvalue = oldmarks.get(mark, repo.nullid)
132 if value != oldvalue:
135 if value != oldvalue:
133 repo.journal.record(bookmarktype, mark, oldvalue, value)
136 repo.journal.record(bookmarktype, mark, oldvalue, value)
134 return orig(store, fp)
137 return orig(store, fp)
135
138
136
139
137 # shared repository support
140 # shared repository support
138 def _readsharedfeatures(repo):
141 def _readsharedfeatures(repo):
139 """A set of shared features for this repository"""
142 """A set of shared features for this repository"""
140 try:
143 try:
141 return set(repo.vfs.read(b'shared').splitlines())
144 return set(repo.vfs.read(b'shared').splitlines())
142 except FileNotFoundError:
145 except FileNotFoundError:
143 return set()
146 return set()
144
147
145
148
146 def _mergeentriesiter(*iterables, **kwargs):
149 def _mergeentriesiter(*iterables, **kwargs):
147 """Given a set of sorted iterables, yield the next entry in merged order
150 """Given a set of sorted iterables, yield the next entry in merged order
148
151
149 Note that by default entries go from most recent to oldest.
152 Note that by default entries go from most recent to oldest.
150 """
153 """
151 order = kwargs.pop('order', max)
154 order = kwargs.pop('order', max)
152 iterables = [iter(it) for it in iterables]
155 iterables = [iter(it) for it in iterables]
153 # this tracks still active iterables; iterables are deleted as they are
156 # this tracks still active iterables; iterables are deleted as they are
154 # exhausted, which is why this is a dictionary and why each entry also
157 # exhausted, which is why this is a dictionary and why each entry also
155 # stores the key. Entries are mutable so we can store the next value each
158 # stores the key. Entries are mutable so we can store the next value each
156 # time.
159 # time.
157 iterable_map = {}
160 iterable_map = {}
158 for key, it in enumerate(iterables):
161 for key, it in enumerate(iterables):
159 try:
162 try:
160 iterable_map[key] = [next(it), key, it]
163 iterable_map[key] = [next(it), key, it]
161 except StopIteration:
164 except StopIteration:
162 # empty entry, can be ignored
165 # empty entry, can be ignored
163 pass
166 pass
164
167
165 while iterable_map:
168 while iterable_map:
166 value, key, it = order(iterable_map.values())
169 value, key, it = order(iterable_map.values())
167 yield value
170 yield value
168 try:
171 try:
169 iterable_map[key][0] = next(it)
172 iterable_map[key][0] = next(it)
170 except StopIteration:
173 except StopIteration:
171 # this iterable is empty, remove it from consideration
174 # this iterable is empty, remove it from consideration
172 del iterable_map[key]
175 del iterable_map[key]
173
176
174
177
175 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
178 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
176 """Mark this shared working copy as sharing journal information"""
179 """Mark this shared working copy as sharing journal information"""
177 with destrepo.wlock():
180 with destrepo.wlock():
178 orig(sourcerepo, destrepo, **kwargs)
181 orig(sourcerepo, destrepo, **kwargs)
179 with destrepo.vfs(b'shared', b'a') as fp:
182 with destrepo.vfs(b'shared', b'a') as fp:
180 fp.write(b'journal\n')
183 fp.write(b'journal\n')
181
184
182
185
183 def unsharejournal(orig, ui, repo, repopath):
186 def unsharejournal(orig, ui, repo, repopath):
184 """Copy shared journal entries into this repo when unsharing"""
187 """Copy shared journal entries into this repo when unsharing"""
185 if (
188 if (
186 repo.path == repopath
189 repo.path == repopath
187 and repo.shared()
190 and repo.shared()
188 and util.safehasattr(repo, 'journal')
191 and util.safehasattr(repo, 'journal')
189 ):
192 ):
190 sharedrepo = hg.sharedreposource(repo)
193 sharedrepo = hg.sharedreposource(repo)
191 sharedfeatures = _readsharedfeatures(repo)
194 sharedfeatures = _readsharedfeatures(repo)
192 if sharedrepo and sharedfeatures > {b'journal'}:
195 if sharedrepo and sharedfeatures > {b'journal'}:
193 # there is a shared repository and there are shared journal entries
196 # there is a shared repository and there are shared journal entries
194 # to copy. move shared date over from source to destination but
197 # to copy. move shared date over from source to destination but
195 # move the local file first
198 # move the local file first
196 if repo.vfs.exists(b'namejournal'):
199 if repo.vfs.exists(b'namejournal'):
197 journalpath = repo.vfs.join(b'namejournal')
200 journalpath = repo.vfs.join(b'namejournal')
198 util.rename(journalpath, journalpath + b'.bak')
201 util.rename(journalpath, journalpath + b'.bak')
199 storage = repo.journal
202 storage = repo.journal
200 local = storage._open(
203 local = storage._open(
201 repo.vfs, filename=b'namejournal.bak', _newestfirst=False
204 repo.vfs, filename=b'namejournal.bak', _newestfirst=False
202 )
205 )
203 shared = (
206 shared = (
204 e
207 e
205 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
208 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
206 if sharednamespaces.get(e.namespace) in sharedfeatures
209 if sharednamespaces.get(e.namespace) in sharedfeatures
207 )
210 )
208 for entry in _mergeentriesiter(local, shared, order=min):
211 for entry in _mergeentriesiter(local, shared, order=min):
209 storage._write(repo.vfs, entry)
212 storage._write(repo.vfs, entry)
210
213
211 return orig(ui, repo, repopath)
214 return orig(ui, repo, repopath)
212
215
213
216
214 class journalentry(
217 class journalentry(
215 collections.namedtuple(
218 collections.namedtuple(
216 'journalentry',
219 'journalentry',
217 'timestamp user command namespace name oldhashes newhashes',
220 'timestamp user command namespace name oldhashes newhashes',
218 )
221 )
219 ):
222 ):
220 """Individual journal entry
223 """Individual journal entry
221
224
222 * timestamp: a mercurial (time, timezone) tuple
225 * timestamp: a mercurial (time, timezone) tuple
223 * user: the username that ran the command
226 * user: the username that ran the command
224 * namespace: the entry namespace, an opaque string
227 * namespace: the entry namespace, an opaque string
225 * name: the name of the changed item, opaque string with meaning in the
228 * name: the name of the changed item, opaque string with meaning in the
226 namespace
229 namespace
227 * command: the hg command that triggered this record
230 * command: the hg command that triggered this record
228 * oldhashes: a tuple of one or more binary hashes for the old location
231 * oldhashes: a tuple of one or more binary hashes for the old location
229 * newhashes: a tuple of one or more binary hashes for the new location
232 * newhashes: a tuple of one or more binary hashes for the new location
230
233
231 Handles serialisation from and to the storage format. Fields are
234 Handles serialisation from and to the storage format. Fields are
232 separated by newlines, hashes are written out in hex separated by commas,
235 separated by newlines, hashes are written out in hex separated by commas,
233 timestamp and timezone are separated by a space.
236 timestamp and timezone are separated by a space.
234
237
235 """
238 """
236
239
237 @classmethod
240 @classmethod
238 def fromstorage(cls, line):
241 def fromstorage(cls, line):
239 (
242 (
240 time,
243 time,
241 user,
244 user,
242 command,
245 command,
243 namespace,
246 namespace,
244 name,
247 name,
245 oldhashes,
248 oldhashes,
246 newhashes,
249 newhashes,
247 ) = line.split(b'\n')
250 ) = line.split(b'\n')
248 timestamp, tz = time.split()
251 timestamp, tz = time.split()
249 timestamp, tz = float(timestamp), int(tz)
252 timestamp, tz = float(timestamp), int(tz)
250 oldhashes = tuple(bin(hash) for hash in oldhashes.split(b','))
253 oldhashes = tuple(bin(hash) for hash in oldhashes.split(b','))
251 newhashes = tuple(bin(hash) for hash in newhashes.split(b','))
254 newhashes = tuple(bin(hash) for hash in newhashes.split(b','))
252 return cls(
255 return cls(
253 (timestamp, tz),
256 (timestamp, tz),
254 user,
257 user,
255 command,
258 command,
256 namespace,
259 namespace,
257 name,
260 name,
258 oldhashes,
261 oldhashes,
259 newhashes,
262 newhashes,
260 )
263 )
261
264
262 def __bytes__(self):
265 def __bytes__(self):
263 """bytes representation for storage"""
266 """bytes representation for storage"""
264 time = b' '.join(map(pycompat.bytestr, self.timestamp))
267 time = b' '.join(map(pycompat.bytestr, self.timestamp))
265 oldhashes = b','.join([hex(hash) for hash in self.oldhashes])
268 oldhashes = b','.join([hex(hash) for hash in self.oldhashes])
266 newhashes = b','.join([hex(hash) for hash in self.newhashes])
269 newhashes = b','.join([hex(hash) for hash in self.newhashes])
267 return b'\n'.join(
270 return b'\n'.join(
268 (
271 (
269 time,
272 time,
270 self.user,
273 self.user,
271 self.command,
274 self.command,
272 self.namespace,
275 self.namespace,
273 self.name,
276 self.name,
274 oldhashes,
277 oldhashes,
275 newhashes,
278 newhashes,
276 )
279 )
277 )
280 )
278
281
279 __str__ = encoding.strmethod(__bytes__)
282 __str__ = encoding.strmethod(__bytes__)
280
283
281
284
282 class journalstorage:
285 class journalstorage:
283 """Storage for journal entries
286 """Storage for journal entries
284
287
285 Entries are divided over two files; one with entries that pertain to the
288 Entries are divided over two files; one with entries that pertain to the
286 local working copy *only*, and one with entries that are shared across
289 local working copy *only*, and one with entries that are shared across
287 multiple working copies when shared using the share extension.
290 multiple working copies when shared using the share extension.
288
291
289 Entries are stored with NUL bytes as separators. See the journalentry
292 Entries are stored with NUL bytes as separators. See the journalentry
290 class for the per-entry structure.
293 class for the per-entry structure.
291
294
292 The file format starts with an integer version, delimited by a NUL.
295 The file format starts with an integer version, delimited by a NUL.
293
296
294 This storage uses a dedicated lock; this makes it easier to avoid issues
297 This storage uses a dedicated lock; this makes it easier to avoid issues
295 with adding entries that added when the regular wlock is unlocked (e.g.
298 with adding entries that added when the regular wlock is unlocked (e.g.
296 the dirstate).
299 the dirstate).
297
300
298 """
301 """
299
302
300 _currentcommand = ()
303 _currentcommand = ()
301 _lockref = None
304 _lockref = None
302
305
303 def __init__(self, repo):
306 def __init__(self, repo):
304 self.user = procutil.getuser()
307 self.user = procutil.getuser()
305 self.ui = repo.ui
308 self.ui = repo.ui
306 self.vfs = repo.vfs
309 self.vfs = repo.vfs
307
310
308 # is this working copy using a shared storage?
311 # is this working copy using a shared storage?
309 self.sharedfeatures = self.sharedvfs = None
312 self.sharedfeatures = self.sharedvfs = None
310 if repo.shared():
313 if repo.shared():
311 features = _readsharedfeatures(repo)
314 features = _readsharedfeatures(repo)
312 sharedrepo = hg.sharedreposource(repo)
315 sharedrepo = hg.sharedreposource(repo)
313 if sharedrepo is not None and b'journal' in features:
316 if sharedrepo is not None and b'journal' in features:
314 self.sharedvfs = sharedrepo.vfs
317 self.sharedvfs = sharedrepo.vfs
315 self.sharedfeatures = features
318 self.sharedfeatures = features
316
319
317 # track the current command for recording in journal entries
320 # track the current command for recording in journal entries
318 @property
321 @property
319 def command(self):
322 def command(self):
320 commandstr = b' '.join(
323 commandstr = b' '.join(
321 map(procutil.shellquote, journalstorage._currentcommand)
324 map(procutil.shellquote, journalstorage._currentcommand)
322 )
325 )
323 if b'\n' in commandstr:
326 if b'\n' in commandstr:
324 # truncate multi-line commands
327 # truncate multi-line commands
325 commandstr = commandstr.partition(b'\n')[0] + b' ...'
328 commandstr = commandstr.partition(b'\n')[0] + b' ...'
326 return commandstr
329 return commandstr
327
330
328 @classmethod
331 @classmethod
329 def recordcommand(cls, *fullargs):
332 def recordcommand(cls, *fullargs):
330 """Set the current hg arguments, stored with recorded entries"""
333 """Set the current hg arguments, stored with recorded entries"""
331 # Set the current command on the class because we may have started
334 # Set the current command on the class because we may have started
332 # with a non-local repo (cloning for example).
335 # with a non-local repo (cloning for example).
333 cls._currentcommand = fullargs
336 cls._currentcommand = fullargs
334
337
335 def _currentlock(self, lockref):
338 def _currentlock(self, lockref):
336 """Returns the lock if it's held, or None if it's not.
339 """Returns the lock if it's held, or None if it's not.
337
340
338 (This is copied from the localrepo class)
341 (This is copied from the localrepo class)
339 """
342 """
340 if lockref is None:
343 if lockref is None:
341 return None
344 return None
342 l = lockref()
345 l = lockref()
343 if l is None or not l.held:
346 if l is None or not l.held:
344 return None
347 return None
345 return l
348 return l
346
349
347 def jlock(self, vfs):
350 def jlock(self, vfs):
348 """Create a lock for the journal file"""
351 """Create a lock for the journal file"""
349 if self._currentlock(self._lockref) is not None:
352 if self._currentlock(self._lockref) is not None:
350 raise error.Abort(_(b'journal lock does not support nesting'))
353 raise error.Abort(_(b'journal lock does not support nesting'))
351 desc = _(b'journal of %s') % vfs.base
354 desc = _(b'journal of %s') % vfs.base
352 try:
355 try:
353 l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc)
356 l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc)
354 except error.LockHeld as inst:
357 except error.LockHeld as inst:
355 self.ui.warn(
358 self.ui.warn(
356 _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker)
359 _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker)
357 )
360 )
358 # default to 600 seconds timeout
361 # default to 600 seconds timeout
359 l = lock.lock(
362 l = lock.lock(
360 vfs,
363 vfs,
361 b'namejournal.lock',
364 b'namejournal.lock',
362 self.ui.configint(b"ui", b"timeout"),
365 self.ui.configint(b"ui", b"timeout"),
363 desc=desc,
366 desc=desc,
364 )
367 )
365 self.ui.warn(_(b"got lock after %s seconds\n") % l.delay)
368 self.ui.warn(_(b"got lock after %s seconds\n") % l.delay)
366 self._lockref = weakref.ref(l)
369 self._lockref = weakref.ref(l)
367 return l
370 return l
368
371
369 def record(self, namespace, name, oldhashes, newhashes):
372 def record(self, namespace, name, oldhashes, newhashes):
370 """Record a new journal entry
373 """Record a new journal entry
371
374
372 * namespace: an opaque string; this can be used to filter on the type
375 * namespace: an opaque string; this can be used to filter on the type
373 of recorded entries.
376 of recorded entries.
374 * name: the name defining this entry; for bookmarks, this is the
377 * name: the name defining this entry; for bookmarks, this is the
375 bookmark name. Can be filtered on when retrieving entries.
378 bookmark name. Can be filtered on when retrieving entries.
376 * oldhashes and newhashes: each a single binary hash, or a list of
379 * oldhashes and newhashes: each a single binary hash, or a list of
377 binary hashes. These represent the old and new position of the named
380 binary hashes. These represent the old and new position of the named
378 item.
381 item.
379
382
380 """
383 """
381 if not isinstance(oldhashes, list):
384 if not isinstance(oldhashes, list):
382 oldhashes = [oldhashes]
385 oldhashes = [oldhashes]
383 if not isinstance(newhashes, list):
386 if not isinstance(newhashes, list):
384 newhashes = [newhashes]
387 newhashes = [newhashes]
385
388
386 entry = journalentry(
389 entry = journalentry(
387 dateutil.makedate(),
390 dateutil.makedate(),
388 self.user,
391 self.user,
389 self.command,
392 self.command,
390 namespace,
393 namespace,
391 name,
394 name,
392 oldhashes,
395 oldhashes,
393 newhashes,
396 newhashes,
394 )
397 )
395
398
396 vfs = self.vfs
399 vfs = self.vfs
397 if self.sharedvfs is not None:
400 if self.sharedvfs is not None:
398 # write to the shared repository if this feature is being
401 # write to the shared repository if this feature is being
399 # shared between working copies.
402 # shared between working copies.
400 if sharednamespaces.get(namespace) in self.sharedfeatures:
403 if sharednamespaces.get(namespace) in self.sharedfeatures:
401 vfs = self.sharedvfs
404 vfs = self.sharedvfs
402
405
403 self._write(vfs, entry)
406 self._write(vfs, entry)
404
407
405 def _write(self, vfs, entry):
408 def _write(self, vfs, entry):
406 with self.jlock(vfs):
409 with self.jlock(vfs):
407 # open file in amend mode to ensure it is created if missing
410 # open file in amend mode to ensure it is created if missing
408 with vfs(b'namejournal', mode=b'a+b') as f:
411 with vfs(b'namejournal', mode=b'a+b') as f:
409 f.seek(0, os.SEEK_SET)
412 f.seek(0, os.SEEK_SET)
410 # Read just enough bytes to get a version number (up to 2
413 # Read just enough bytes to get a version number (up to 2
411 # digits plus separator)
414 # digits plus separator)
412 version = f.read(3).partition(b'\0')[0]
415 version = f.read(3).partition(b'\0')[0]
413 if version and version != b"%d" % storageversion:
416 if version and version != b"%d" % storageversion:
414 # different version of the storage. Exit early (and not
417 # different version of the storage. Exit early (and not
415 # write anything) if this is not a version we can handle or
418 # write anything) if this is not a version we can handle or
416 # the file is corrupt. In future, perhaps rotate the file
419 # the file is corrupt. In future, perhaps rotate the file
417 # instead?
420 # instead?
418 self.ui.warn(
421 self.ui.warn(
419 _(b"unsupported journal file version '%s'\n") % version
422 _(b"unsupported journal file version '%s'\n") % version
420 )
423 )
421 return
424 return
422 if not version:
425 if not version:
423 # empty file, write version first
426 # empty file, write version first
424 f.write((b"%d" % storageversion) + b'\0')
427 f.write((b"%d" % storageversion) + b'\0')
425 f.seek(0, os.SEEK_END)
428 f.seek(0, os.SEEK_END)
426 f.write(bytes(entry) + b'\0')
429 f.write(bytes(entry) + b'\0')
427
430
428 def filtered(self, namespace=None, name=None):
431 def filtered(self, namespace=None, name=None):
429 """Yield all journal entries with the given namespace or name
432 """Yield all journal entries with the given namespace or name
430
433
431 Both the namespace and the name are optional; if neither is given all
434 Both the namespace and the name are optional; if neither is given all
432 entries in the journal are produced.
435 entries in the journal are produced.
433
436
434 Matching supports regular expressions by using the `re:` prefix
437 Matching supports regular expressions by using the `re:` prefix
435 (use `literal:` to match names or namespaces that start with `re:`)
438 (use `literal:` to match names or namespaces that start with `re:`)
436
439
437 """
440 """
438 if namespace is not None:
441 if namespace is not None:
439 namespace = stringutil.stringmatcher(namespace)[-1]
442 namespace = stringutil.stringmatcher(namespace)[-1]
440 if name is not None:
443 if name is not None:
441 name = stringutil.stringmatcher(name)[-1]
444 name = stringutil.stringmatcher(name)[-1]
442 for entry in self:
445 for entry in self:
443 if namespace is not None and not namespace(entry.namespace):
446 if namespace is not None and not namespace(entry.namespace):
444 continue
447 continue
445 if name is not None and not name(entry.name):
448 if name is not None and not name(entry.name):
446 continue
449 continue
447 yield entry
450 yield entry
448
451
449 def __iter__(self):
452 def __iter__(self):
450 """Iterate over the storage
453 """Iterate over the storage
451
454
452 Yields journalentry instances for each contained journal record.
455 Yields journalentry instances for each contained journal record.
453
456
454 """
457 """
455 local = self._open(self.vfs)
458 local = self._open(self.vfs)
456
459
457 if self.sharedvfs is None:
460 if self.sharedvfs is None:
458 return local
461 return local
459
462
460 # iterate over both local and shared entries, but only those
463 # iterate over both local and shared entries, but only those
461 # shared entries that are among the currently shared features
464 # shared entries that are among the currently shared features
462 shared = (
465 shared = (
463 e
466 e
464 for e in self._open(self.sharedvfs)
467 for e in self._open(self.sharedvfs)
465 if sharednamespaces.get(e.namespace) in self.sharedfeatures
468 if sharednamespaces.get(e.namespace) in self.sharedfeatures
466 )
469 )
467 return _mergeentriesiter(local, shared)
470 return _mergeentriesiter(local, shared)
468
471
469 def _open(self, vfs, filename=b'namejournal', _newestfirst=True):
472 def _open(self, vfs, filename=b'namejournal', _newestfirst=True):
470 if not vfs.exists(filename):
473 if not vfs.exists(filename):
471 return
474 return
472
475
473 with vfs(filename) as f:
476 with vfs(filename) as f:
474 raw = f.read()
477 raw = f.read()
475
478
476 lines = raw.split(b'\0')
479 lines = raw.split(b'\0')
477 version = lines and lines[0]
480 version = lines and lines[0]
478 if version != b"%d" % storageversion:
481 if version != b"%d" % storageversion:
479 version = version or _(b'not available')
482 version = version or _(b'not available')
480 raise error.Abort(_(b"unknown journal file version '%s'") % version)
483 raise error.Abort(_(b"unknown journal file version '%s'") % version)
481
484
482 # Skip the first line, it's a version number. Normally we iterate over
485 # Skip the first line, it's a version number. Normally we iterate over
483 # these in reverse order to list newest first; only when copying across
486 # these in reverse order to list newest first; only when copying across
484 # a shared storage do we forgo reversing.
487 # a shared storage do we forgo reversing.
485 lines = lines[1:]
488 lines = lines[1:]
486 if _newestfirst:
489 if _newestfirst:
487 lines = reversed(lines)
490 lines = reversed(lines)
488 for line in lines:
491 for line in lines:
489 if not line:
492 if not line:
490 continue
493 continue
491 yield journalentry.fromstorage(line)
494 yield journalentry.fromstorage(line)
492
495
493
496
494 # journal reading
497 # journal reading
495 # log options that don't make sense for journal
498 # log options that don't make sense for journal
496 _ignoreopts = (b'no-merges', b'graph')
499 _ignoreopts = (b'no-merges', b'graph')
497
500
498
501
499 @command(
502 @command(
500 b'journal',
503 b'journal',
501 [
504 [
502 (b'', b'all', None, b'show history for all names'),
505 (b'', b'all', None, b'show history for all names'),
503 (b'c', b'commits', None, b'show commit metadata'),
506 (b'c', b'commits', None, b'show commit metadata'),
504 ]
507 ]
505 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
508 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
506 b'[OPTION]... [BOOKMARKNAME]',
509 b'[OPTION]... [BOOKMARKNAME]',
507 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
510 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
508 )
511 )
509 def journal(ui, repo, *args, **opts):
512 def journal(ui, repo, *args, **opts):
510 """show the previous position of bookmarks and the working copy
513 """show the previous position of bookmarks and the working copy
511
514
512 The journal is used to see the previous commits that bookmarks and the
515 The journal is used to see the previous commits that bookmarks and the
513 working copy pointed to. By default the previous locations for the working
516 working copy pointed to. By default the previous locations for the working
514 copy. Passing a bookmark name will show all the previous positions of
517 copy. Passing a bookmark name will show all the previous positions of
515 that bookmark. Use the --all switch to show previous locations for all
518 that bookmark. Use the --all switch to show previous locations for all
516 bookmarks and the working copy; each line will then include the bookmark
519 bookmarks and the working copy; each line will then include the bookmark
517 name, or '.' for the working copy, as well.
520 name, or '.' for the working copy, as well.
518
521
519 If `name` starts with `re:`, the remainder of the name is treated as
522 If `name` starts with `re:`, the remainder of the name is treated as
520 a regular expression. To match a name that actually starts with `re:`,
523 a regular expression. To match a name that actually starts with `re:`,
521 use the prefix `literal:`.
524 use the prefix `literal:`.
522
525
523 By default hg journal only shows the commit hash and the command that was
526 By default hg journal only shows the commit hash and the command that was
524 running at that time. -v/--verbose will show the prior hash, the user, and
527 running at that time. -v/--verbose will show the prior hash, the user, and
525 the time at which it happened.
528 the time at which it happened.
526
529
527 Use -c/--commits to output log information on each commit hash; at this
530 Use -c/--commits to output log information on each commit hash; at this
528 point you can use the usual `--patch`, `--git`, `--stat` and `--template`
531 point you can use the usual `--patch`, `--git`, `--stat` and `--template`
529 switches to alter the log output for these.
532 switches to alter the log output for these.
530
533
531 `hg journal -T json` can be used to produce machine readable output.
534 `hg journal -T json` can be used to produce machine readable output.
532
535
533 """
536 """
534 opts = pycompat.byteskwargs(opts)
537 opts = pycompat.byteskwargs(opts)
535 name = b'.'
538 name = b'.'
536 if opts.get(b'all'):
539 if opts.get(b'all'):
537 if args:
540 if args:
538 raise error.Abort(
541 raise error.Abort(
539 _(b"You can't combine --all and filtering on a name")
542 _(b"You can't combine --all and filtering on a name")
540 )
543 )
541 name = None
544 name = None
542 if args:
545 if args:
543 name = args[0]
546 name = args[0]
544
547
545 fm = ui.formatter(b'journal', opts)
548 fm = ui.formatter(b'journal', opts)
546
549
547 def formatnodes(nodes):
550 def formatnodes(nodes):
548 return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',')
551 return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',')
549
552
550 if opts.get(b"template") != b"json":
553 if opts.get(b"template") != b"json":
551 if name is None:
554 if name is None:
552 displayname = _(b'the working copy and bookmarks')
555 displayname = _(b'the working copy and bookmarks')
553 else:
556 else:
554 displayname = b"'%s'" % name
557 displayname = b"'%s'" % name
555 ui.status(_(b"previous locations of %s:\n") % displayname)
558 ui.status(_(b"previous locations of %s:\n") % displayname)
556
559
557 limit = logcmdutil.getlimit(opts)
560 limit = logcmdutil.getlimit(opts)
558 entry = None
561 entry = None
559 ui.pager(b'journal')
562 ui.pager(b'journal')
560 for count, entry in enumerate(repo.journal.filtered(name=name)):
563 for count, entry in enumerate(repo.journal.filtered(name=name)):
561 if count == limit:
564 if count == limit:
562 break
565 break
563
566
564 fm.startitem()
567 fm.startitem()
565 fm.condwrite(
568 fm.condwrite(
566 ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes)
569 ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes)
567 )
570 )
568 fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes))
571 fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes))
569 fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user)
572 fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user)
570
573
571 # ``name`` is bytes, or None only if 'all' was an option.
574 # ``name`` is bytes, or None only if 'all' was an option.
572 fm.condwrite(
575 fm.condwrite(
573 # pytype: disable=attribute-error
576 # pytype: disable=attribute-error
574 opts.get(b'all') or name.startswith(b're:'),
577 opts.get(b'all') or name.startswith(b're:'),
575 # pytype: enable=attribute-error
578 # pytype: enable=attribute-error
576 b'name',
579 b'name',
577 b' %-8s',
580 b' %-8s',
578 entry.name,
581 entry.name,
579 )
582 )
580
583
581 fm.condwrite(
584 fm.condwrite(
582 ui.verbose,
585 ui.verbose,
583 b'date',
586 b'date',
584 b' %s',
587 b' %s',
585 fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'),
588 fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'),
586 )
589 )
587 fm.write(b'command', b' %s\n', entry.command)
590 fm.write(b'command', b' %s\n', entry.command)
588
591
589 if opts.get(b"commits"):
592 if opts.get(b"commits"):
590 if fm.isplain():
593 if fm.isplain():
591 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
594 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
592 else:
595 else:
593 displayer = logcmdutil.changesetformatter(
596 displayer = logcmdutil.changesetformatter(
594 ui, repo, fm.nested(b'changesets'), diffopts=opts
597 ui, repo, fm.nested(b'changesets'), diffopts=opts
595 )
598 )
596 for hash in entry.newhashes:
599 for hash in entry.newhashes:
597 try:
600 try:
598 ctx = repo[hash]
601 ctx = repo[hash]
599 displayer.show(ctx)
602 displayer.show(ctx)
600 except error.RepoLookupError as e:
603 except error.RepoLookupError as e:
601 fm.plain(b"%s\n\n" % pycompat.bytestr(e))
604 fm.plain(b"%s\n\n" % pycompat.bytestr(e))
602 displayer.close()
605 displayer.close()
603
606
604 fm.end()
607 fm.end()
605
608
606 if entry is None:
609 if entry is None:
607 ui.status(_(b"no recorded locations\n"))
610 ui.status(_(b"no recorded locations\n"))
@@ -1,314 +1,318 b''
1 Tests for the journal extension; records bookmark locations.
1 Tests for the journal extension; records bookmark locations.
2
2
3 $ cat >> testmocks.py << EOF
3 $ cat >> testmocks.py << EOF
4 > # mock out procutil.getuser() and util.makedate() to supply testable values
4 > # mock out procutil.getuser() and util.makedate() to supply testable values
5 > import os
5 > import os
6 > from mercurial import pycompat, util
6 > from mercurial import pycompat, util
7 > from mercurial.utils import dateutil, procutil
7 > from mercurial.utils import dateutil, procutil
8 > def mockgetuser():
8 > def mockgetuser():
9 > return b'foobar'
9 > return b'foobar'
10 >
10 >
11 > def mockmakedate():
11 > def mockmakedate():
12 > filename = os.path.join(os.environ['TESTTMP'], 'testtime')
12 > filename = os.path.join(os.environ['TESTTMP'], 'testtime')
13 > try:
13 > try:
14 > with open(filename, 'rb') as timef:
14 > with open(filename, 'rb') as timef:
15 > time = float(timef.read()) + 1
15 > time = float(timef.read()) + 1
16 > except IOError:
16 > except IOError:
17 > time = 0.0
17 > time = 0.0
18 > with open(filename, 'wb') as timef:
18 > with open(filename, 'wb') as timef:
19 > timef.write(pycompat.bytestr(time))
19 > timef.write(pycompat.bytestr(time))
20 > return (time, 0)
20 > return (time, 0)
21 >
21 >
22 > procutil.getuser = mockgetuser
22 > procutil.getuser = mockgetuser
23 > dateutil.makedate = mockmakedate
23 > dateutil.makedate = mockmakedate
24 > EOF
24 > EOF
25
25
26 $ cat >> $HGRCPATH << EOF
26 $ cat >> $HGRCPATH << EOF
27 > [extensions]
27 > [extensions]
28 > journal=
28 > journal=
29 > testmocks=`pwd`/testmocks.py
29 > testmocks=`pwd`/testmocks.py
30 > EOF
30 > EOF
31
31
32 Setup repo
32 Setup repo
33
33
34 $ hg init repo
34 $ hg init repo
35 $ cd repo
35 $ cd repo
36
36
37 Test empty journal
37 Test empty journal
38
38
39 $ hg journal
39 $ hg journal
40 previous locations of '.':
40 previous locations of '.':
41 no recorded locations
41 no recorded locations
42 $ hg journal foo
42 $ hg journal foo
43 previous locations of 'foo':
43 previous locations of 'foo':
44 no recorded locations
44 no recorded locations
45
45
46 Test that working copy changes are tracked
46 Test that working copy changes are tracked
47
47
48 $ echo a > a
48 $ echo a > a
49 $ hg commit -Aqm a
49 $ hg commit -Aqm a
50 $ hg journal
50 $ hg journal
51 previous locations of '.':
51 previous locations of '.':
52 cb9a9f314b8b commit -Aqm a
52 cb9a9f314b8b commit -Aqm a
53 $ echo b > a
53 $ echo b > a
54 $ hg commit -Aqm b
54 $ hg commit -Aqm b
55 $ hg journal
55 $ hg journal
56 previous locations of '.':
56 previous locations of '.':
57 1e6c11564562 commit -Aqm b
57 1e6c11564562 commit -Aqm b
58 cb9a9f314b8b commit -Aqm a
58 cb9a9f314b8b commit -Aqm a
59 $ hg up 0
59 $ hg up 0
60 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 $ hg journal
61 $ hg journal
62 previous locations of '.':
62 previous locations of '.':
63 cb9a9f314b8b up 0
63 cb9a9f314b8b up 0
64 1e6c11564562 commit -Aqm b
64 1e6c11564562 commit -Aqm b
65 cb9a9f314b8b commit -Aqm a
65 cb9a9f314b8b commit -Aqm a
66
66
67 Test that bookmarks are tracked
67 Test that bookmarks are tracked
68
68
69 $ hg book -r tip bar
69 $ hg book -r tip bar
70 $ hg journal bar
70 $ hg journal bar
71 previous locations of 'bar':
71 previous locations of 'bar':
72 1e6c11564562 book -r tip bar
72 1e6c11564562 book -r tip bar
73 $ hg book -f bar
73 $ hg book -f bar
74 $ hg journal bar
74 $ hg journal bar
75 previous locations of 'bar':
75 previous locations of 'bar':
76 cb9a9f314b8b book -f bar
76 cb9a9f314b8b book -f bar
77 1e6c11564562 book -r tip bar
77 1e6c11564562 book -r tip bar
78 $ hg up
78 $ hg up
79 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
80 updating bookmark bar
80 updating bookmark bar
81 $ hg journal bar
81 $ hg journal bar
82 previous locations of 'bar':
82 previous locations of 'bar':
83 1e6c11564562 up
83 1e6c11564562 up
84 cb9a9f314b8b book -f bar
84 cb9a9f314b8b book -f bar
85 1e6c11564562 book -r tip bar
85 1e6c11564562 book -r tip bar
86
86
87 Test that we tracks bookmark deletion
87 Test that we tracks bookmark deletion
88
88
89 $ hg book -r . babar
89 $ hg book -r . babar
90 $ hg book -f -r .~1 babar
90 $ hg book -f -r .~1 babar
91 $ hg book -d babar
91 $ hg book -d babar
92 $ hg journal babar
92 $ hg journal babar
93 previous locations of 'babar':
93 previous locations of 'babar':
94 000000000000 book -d babar
94 cb9a9f314b8b book -f -r '.~1' babar
95 cb9a9f314b8b book -f -r '.~1' babar
95 1e6c11564562 book -r . babar
96 1e6c11564562 book -r . babar
96
97
97 Test that bookmarks and working copy tracking is not mixed
98 Test that bookmarks and working copy tracking is not mixed
98
99
99 $ hg journal
100 $ hg journal
100 previous locations of '.':
101 previous locations of '.':
101 1e6c11564562 up
102 1e6c11564562 up
102 cb9a9f314b8b up 0
103 cb9a9f314b8b up 0
103 1e6c11564562 commit -Aqm b
104 1e6c11564562 commit -Aqm b
104 cb9a9f314b8b commit -Aqm a
105 cb9a9f314b8b commit -Aqm a
105
106
106 Test that you can list all entries as well as limit the list or filter on them
107 Test that you can list all entries as well as limit the list or filter on them
107
108
108 $ hg book -r tip baz
109 $ hg book -r tip baz
109 $ hg journal --all
110 $ hg journal --all
110 previous locations of the working copy and bookmarks:
111 previous locations of the working copy and bookmarks:
111 1e6c11564562 baz book -r tip baz
112 1e6c11564562 baz book -r tip baz
113 000000000000 babar book -d babar
112 cb9a9f314b8b babar book -f -r '.~1' babar
114 cb9a9f314b8b babar book -f -r '.~1' babar
113 1e6c11564562 babar book -r . babar
115 1e6c11564562 babar book -r . babar
114 1e6c11564562 bar up
116 1e6c11564562 bar up
115 1e6c11564562 . up
117 1e6c11564562 . up
116 cb9a9f314b8b bar book -f bar
118 cb9a9f314b8b bar book -f bar
117 1e6c11564562 bar book -r tip bar
119 1e6c11564562 bar book -r tip bar
118 cb9a9f314b8b . up 0
120 cb9a9f314b8b . up 0
119 1e6c11564562 . commit -Aqm b
121 1e6c11564562 . commit -Aqm b
120 cb9a9f314b8b . commit -Aqm a
122 cb9a9f314b8b . commit -Aqm a
121 $ hg journal --limit 2
123 $ hg journal --limit 2
122 previous locations of '.':
124 previous locations of '.':
123 1e6c11564562 up
125 1e6c11564562 up
124 cb9a9f314b8b up 0
126 cb9a9f314b8b up 0
125 $ hg journal bar
127 $ hg journal bar
126 previous locations of 'bar':
128 previous locations of 'bar':
127 1e6c11564562 up
129 1e6c11564562 up
128 cb9a9f314b8b book -f bar
130 cb9a9f314b8b book -f bar
129 1e6c11564562 book -r tip bar
131 1e6c11564562 book -r tip bar
130 $ hg journal foo
132 $ hg journal foo
131 previous locations of 'foo':
133 previous locations of 'foo':
132 no recorded locations
134 no recorded locations
133 $ hg journal .
135 $ hg journal .
134 previous locations of '.':
136 previous locations of '.':
135 1e6c11564562 up
137 1e6c11564562 up
136 cb9a9f314b8b up 0
138 cb9a9f314b8b up 0
137 1e6c11564562 commit -Aqm b
139 1e6c11564562 commit -Aqm b
138 cb9a9f314b8b commit -Aqm a
140 cb9a9f314b8b commit -Aqm a
139 $ hg journal "re:ba."
141 $ hg journal "re:ba."
140 previous locations of 're:ba.':
142 previous locations of 're:ba.':
141 1e6c11564562 baz book -r tip baz
143 1e6c11564562 baz book -r tip baz
144 000000000000 babar book -d babar
142 cb9a9f314b8b babar book -f -r '.~1' babar
145 cb9a9f314b8b babar book -f -r '.~1' babar
143 1e6c11564562 babar book -r . babar
146 1e6c11564562 babar book -r . babar
144 1e6c11564562 bar up
147 1e6c11564562 bar up
145 cb9a9f314b8b bar book -f bar
148 cb9a9f314b8b bar book -f bar
146 1e6c11564562 bar book -r tip bar
149 1e6c11564562 bar book -r tip bar
147
150
148 Test that verbose, JSON, template and commit output work
151 Test that verbose, JSON, template and commit output work
149
152
150 $ hg journal --verbose --all
153 $ hg journal --verbose --all
151 previous locations of the working copy and bookmarks:
154 previous locations of the working copy and bookmarks:
152 000000000000 -> 1e6c11564562 foobar baz 1970-01-01 00:00 +0000 book -r tip baz
155 000000000000 -> 1e6c11564562 foobar baz 1970-01-01 00:00 +0000 book -r tip baz
156 cb9a9f314b8b -> 000000000000 foobar babar 1970-01-01 00:00 +0000 book -d babar
153 1e6c11564562 -> cb9a9f314b8b foobar babar 1970-01-01 00:00 +0000 book -f -r '.~1' babar
157 1e6c11564562 -> cb9a9f314b8b foobar babar 1970-01-01 00:00 +0000 book -f -r '.~1' babar
154 000000000000 -> 1e6c11564562 foobar babar 1970-01-01 00:00 +0000 book -r . babar
158 000000000000 -> 1e6c11564562 foobar babar 1970-01-01 00:00 +0000 book -r . babar
155 cb9a9f314b8b -> 1e6c11564562 foobar bar 1970-01-01 00:00 +0000 up
159 cb9a9f314b8b -> 1e6c11564562 foobar bar 1970-01-01 00:00 +0000 up
156 cb9a9f314b8b -> 1e6c11564562 foobar . 1970-01-01 00:00 +0000 up
160 cb9a9f314b8b -> 1e6c11564562 foobar . 1970-01-01 00:00 +0000 up
157 1e6c11564562 -> cb9a9f314b8b foobar bar 1970-01-01 00:00 +0000 book -f bar
161 1e6c11564562 -> cb9a9f314b8b foobar bar 1970-01-01 00:00 +0000 book -f bar
158 000000000000 -> 1e6c11564562 foobar bar 1970-01-01 00:00 +0000 book -r tip bar
162 000000000000 -> 1e6c11564562 foobar bar 1970-01-01 00:00 +0000 book -r tip bar
159 1e6c11564562 -> cb9a9f314b8b foobar . 1970-01-01 00:00 +0000 up 0
163 1e6c11564562 -> cb9a9f314b8b foobar . 1970-01-01 00:00 +0000 up 0
160 cb9a9f314b8b -> 1e6c11564562 foobar . 1970-01-01 00:00 +0000 commit -Aqm b
164 cb9a9f314b8b -> 1e6c11564562 foobar . 1970-01-01 00:00 +0000 commit -Aqm b
161 000000000000 -> cb9a9f314b8b foobar . 1970-01-01 00:00 +0000 commit -Aqm a
165 000000000000 -> cb9a9f314b8b foobar . 1970-01-01 00:00 +0000 commit -Aqm a
162 $ hg journal --verbose -Tjson
166 $ hg journal --verbose -Tjson
163 [
167 [
164 {
168 {
165 "command": "up",
169 "command": "up",
166 "date": [5, 0],
170 "date": [5, 0],
167 "name": ".",
171 "name": ".",
168 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
172 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
169 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
173 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
170 "user": "foobar"
174 "user": "foobar"
171 },
175 },
172 {
176 {
173 "command": "up 0",
177 "command": "up 0",
174 "date": [2, 0],
178 "date": [2, 0],
175 "name": ".",
179 "name": ".",
176 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
180 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
177 "oldnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
181 "oldnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
178 "user": "foobar"
182 "user": "foobar"
179 },
183 },
180 {
184 {
181 "command": "commit -Aqm b",
185 "command": "commit -Aqm b",
182 "date": [1, 0],
186 "date": [1, 0],
183 "name": ".",
187 "name": ".",
184 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
188 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
185 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
189 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
186 "user": "foobar"
190 "user": "foobar"
187 },
191 },
188 {
192 {
189 "command": "commit -Aqm a",
193 "command": "commit -Aqm a",
190 "date": [0, 0],
194 "date": [0, 0],
191 "name": ".",
195 "name": ".",
192 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
196 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
193 "oldnodes": ["0000000000000000000000000000000000000000"],
197 "oldnodes": ["0000000000000000000000000000000000000000"],
194 "user": "foobar"
198 "user": "foobar"
195 }
199 }
196 ]
200 ]
197
201
198 $ cat <<EOF >> $HGRCPATH
202 $ cat <<EOF >> $HGRCPATH
199 > [templates]
203 > [templates]
200 > j = "{oldnodes % '{node|upper}'} -> {newnodes % '{node|upper}'}
204 > j = "{oldnodes % '{node|upper}'} -> {newnodes % '{node|upper}'}
201 > - user: {user}
205 > - user: {user}
202 > - command: {command}
206 > - command: {command}
203 > - date: {date|rfc3339date}
207 > - date: {date|rfc3339date}
204 > - newnodes: {newnodes}
208 > - newnodes: {newnodes}
205 > - oldnodes: {oldnodes}
209 > - oldnodes: {oldnodes}
206 > "
210 > "
207 > EOF
211 > EOF
208 $ hg journal -Tj -l1
212 $ hg journal -Tj -l1
209 previous locations of '.':
213 previous locations of '.':
210 CB9A9F314B8B07BA71012FCDBC544B5A4D82FF5B -> 1E6C11564562B4ED919BACA798BC4338BD299D6A
214 CB9A9F314B8B07BA71012FCDBC544B5A4D82FF5B -> 1E6C11564562B4ED919BACA798BC4338BD299D6A
211 - user: foobar
215 - user: foobar
212 - command: up
216 - command: up
213 - date: 1970-01-01T00:00:05+00:00
217 - date: 1970-01-01T00:00:05+00:00
214 - newnodes: 1e6c11564562b4ed919baca798bc4338bd299d6a
218 - newnodes: 1e6c11564562b4ed919baca798bc4338bd299d6a
215 - oldnodes: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
219 - oldnodes: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
216
220
217 $ hg journal --commit
221 $ hg journal --commit
218 previous locations of '.':
222 previous locations of '.':
219 1e6c11564562 up
223 1e6c11564562 up
220 changeset: 1:1e6c11564562
224 changeset: 1:1e6c11564562
221 bookmark: bar
225 bookmark: bar
222 bookmark: baz
226 bookmark: baz
223 tag: tip
227 tag: tip
224 user: test
228 user: test
225 date: Thu Jan 01 00:00:00 1970 +0000
229 date: Thu Jan 01 00:00:00 1970 +0000
226 summary: b
230 summary: b
227
231
228 cb9a9f314b8b up 0
232 cb9a9f314b8b up 0
229 changeset: 0:cb9a9f314b8b
233 changeset: 0:cb9a9f314b8b
230 user: test
234 user: test
231 date: Thu Jan 01 00:00:00 1970 +0000
235 date: Thu Jan 01 00:00:00 1970 +0000
232 summary: a
236 summary: a
233
237
234 1e6c11564562 commit -Aqm b
238 1e6c11564562 commit -Aqm b
235 changeset: 1:1e6c11564562
239 changeset: 1:1e6c11564562
236 bookmark: bar
240 bookmark: bar
237 bookmark: baz
241 bookmark: baz
238 tag: tip
242 tag: tip
239 user: test
243 user: test
240 date: Thu Jan 01 00:00:00 1970 +0000
244 date: Thu Jan 01 00:00:00 1970 +0000
241 summary: b
245 summary: b
242
246
243 cb9a9f314b8b commit -Aqm a
247 cb9a9f314b8b commit -Aqm a
244 changeset: 0:cb9a9f314b8b
248 changeset: 0:cb9a9f314b8b
245 user: test
249 user: test
246 date: Thu Jan 01 00:00:00 1970 +0000
250 date: Thu Jan 01 00:00:00 1970 +0000
247 summary: a
251 summary: a
248
252
249
253
250 $ hg journal --commit -Tjson
254 $ hg journal --commit -Tjson
251 [
255 [
252 {
256 {
253 "changesets": [{"bookmarks": ["bar", "baz"], "branch": "default", "date": [0, 0], "desc": "b", "node": "1e6c11564562b4ed919baca798bc4338bd299d6a", "parents": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"], "phase": "draft", "rev": 1, "tags": ["tip"], "user": "test"}],
257 "changesets": [{"bookmarks": ["bar", "baz"], "branch": "default", "date": [0, 0], "desc": "b", "node": "1e6c11564562b4ed919baca798bc4338bd299d6a", "parents": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"], "phase": "draft", "rev": 1, "tags": ["tip"], "user": "test"}],
254 "command": "up",
258 "command": "up",
255 "date": [5, 0],
259 "date": [5, 0],
256 "name": ".",
260 "name": ".",
257 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
261 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
258 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
262 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
259 "user": "foobar"
263 "user": "foobar"
260 },
264 },
261 {
265 {
262 "changesets": [{"bookmarks": [], "branch": "default", "date": [0, 0], "desc": "a", "node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "parents": ["0000000000000000000000000000000000000000"], "phase": "draft", "rev": 0, "tags": [], "user": "test"}],
266 "changesets": [{"bookmarks": [], "branch": "default", "date": [0, 0], "desc": "a", "node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "parents": ["0000000000000000000000000000000000000000"], "phase": "draft", "rev": 0, "tags": [], "user": "test"}],
263 "command": "up 0",
267 "command": "up 0",
264 "date": [2, 0],
268 "date": [2, 0],
265 "name": ".",
269 "name": ".",
266 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
270 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
267 "oldnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
271 "oldnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
268 "user": "foobar"
272 "user": "foobar"
269 },
273 },
270 {
274 {
271 "changesets": [{"bookmarks": ["bar", "baz"], "branch": "default", "date": [0, 0], "desc": "b", "node": "1e6c11564562b4ed919baca798bc4338bd299d6a", "parents": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"], "phase": "draft", "rev": 1, "tags": ["tip"], "user": "test"}],
275 "changesets": [{"bookmarks": ["bar", "baz"], "branch": "default", "date": [0, 0], "desc": "b", "node": "1e6c11564562b4ed919baca798bc4338bd299d6a", "parents": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"], "phase": "draft", "rev": 1, "tags": ["tip"], "user": "test"}],
272 "command": "commit -Aqm b",
276 "command": "commit -Aqm b",
273 "date": [1, 0],
277 "date": [1, 0],
274 "name": ".",
278 "name": ".",
275 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
279 "newnodes": ["1e6c11564562b4ed919baca798bc4338bd299d6a"],
276 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
280 "oldnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
277 "user": "foobar"
281 "user": "foobar"
278 },
282 },
279 {
283 {
280 "changesets": [{"bookmarks": [], "branch": "default", "date": [0, 0], "desc": "a", "node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "parents": ["0000000000000000000000000000000000000000"], "phase": "draft", "rev": 0, "tags": [], "user": "test"}],
284 "changesets": [{"bookmarks": [], "branch": "default", "date": [0, 0], "desc": "a", "node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "parents": ["0000000000000000000000000000000000000000"], "phase": "draft", "rev": 0, "tags": [], "user": "test"}],
281 "command": "commit -Aqm a",
285 "command": "commit -Aqm a",
282 "date": [0, 0],
286 "date": [0, 0],
283 "name": ".",
287 "name": ".",
284 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
288 "newnodes": ["cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b"],
285 "oldnodes": ["0000000000000000000000000000000000000000"],
289 "oldnodes": ["0000000000000000000000000000000000000000"],
286 "user": "foobar"
290 "user": "foobar"
287 }
291 }
288 ]
292 ]
289
293
290 $ hg journal --commit \
294 $ hg journal --commit \
291 > -T'command: {command}\n{changesets % " rev: {rev}\n children: {children}\n"}'
295 > -T'command: {command}\n{changesets % " rev: {rev}\n children: {children}\n"}'
292 previous locations of '.':
296 previous locations of '.':
293 command: up
297 command: up
294 rev: 1
298 rev: 1
295 children:
299 children:
296 command: up 0
300 command: up 0
297 rev: 0
301 rev: 0
298 children: 1:1e6c11564562
302 children: 1:1e6c11564562
299 command: commit -Aqm b
303 command: commit -Aqm b
300 rev: 1
304 rev: 1
301 children:
305 children:
302 command: commit -Aqm a
306 command: commit -Aqm a
303 rev: 0
307 rev: 0
304 children: 1:1e6c11564562
308 children: 1:1e6c11564562
305
309
306 Test for behaviour on unexpected storage version information
310 Test for behaviour on unexpected storage version information
307
311
308 $ printf '42\0' > .hg/namejournal
312 $ printf '42\0' > .hg/namejournal
309 $ hg journal
313 $ hg journal
310 previous locations of '.':
314 previous locations of '.':
311 abort: unknown journal file version '42'
315 abort: unknown journal file version '42'
312 [255]
316 [255]
313 $ hg book -r tip doomed
317 $ hg book -r tip doomed
314 unsupported journal file version '42'
318 unsupported journal file version '42'
General Comments 0
You need to be logged in to leave comments. Login now