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