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