comparison hgext/journal.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents 7f63ec6969f3
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
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 )
72 extensions.wrapfunction(hg, 'postshare', wrappostshare) 73 extensions.wrapfunction(hg, 'postshare', wrappostshare)
73 extensions.wrapfunction(hg, 'copystore', unsharejournal) 74 extensions.wrapfunction(hg, 'copystore', unsharejournal)
75
74 76
75 def reposetup(ui, repo): 77 def reposetup(ui, repo):
76 if repo.local(): 78 if repo.local():
77 repo.journal = journalstorage(repo) 79 repo.journal = journalstorage(repo)
78 repo._wlockfreeprefix.add('namejournal') 80 repo._wlockfreeprefix.add('namejournal')
82 # already instantiated dirstate isn't yet marked as 84 # already instantiated dirstate isn't yet marked as
83 # "journal"-ing, even though repo.dirstate() was already 85 # "journal"-ing, even though repo.dirstate() was already
84 # wrapped by own wrapdirstate() 86 # wrapped by own wrapdirstate()
85 _setupdirstate(repo, dirstate) 87 _setupdirstate(repo, dirstate)
86 88
89
87 def runcommand(orig, lui, repo, cmd, fullargs, *args): 90 def runcommand(orig, lui, repo, cmd, fullargs, *args):
88 """Track the command line options for recording in the journal""" 91 """Track the command line options for recording in the journal"""
89 journalstorage.recordcommand(*fullargs) 92 journalstorage.recordcommand(*fullargs)
90 return orig(lui, repo, cmd, fullargs, *args) 93 return orig(lui, repo, cmd, fullargs, *args)
91 94
95
92 def _setupdirstate(repo, dirstate): 96 def _setupdirstate(repo, dirstate):
93 dirstate.journalstorage = repo.journal 97 dirstate.journalstorage = repo.journal
94 dirstate.addparentchangecallback('journal', recorddirstateparents) 98 dirstate.addparentchangecallback('journal', recorddirstateparents)
99
95 100
96 # hooks to record dirstate changes 101 # hooks to record dirstate changes
97 def wrapdirstate(orig, repo): 102 def wrapdirstate(orig, repo):
98 """Make journal storage available to the dirstate object""" 103 """Make journal storage available to the dirstate object"""
99 dirstate = orig(repo) 104 dirstate = orig(repo)
100 if util.safehasattr(repo, 'journal'): 105 if util.safehasattr(repo, 'journal'):
101 _setupdirstate(repo, dirstate) 106 _setupdirstate(repo, dirstate)
102 return dirstate 107 return dirstate
108
103 109
104 def recorddirstateparents(dirstate, old, new): 110 def recorddirstateparents(dirstate, old, new):
105 """Records all dirstate parent changes in the journal.""" 111 """Records all dirstate parent changes in the journal."""
106 old = list(old) 112 old = list(old)
107 new = list(new) 113 new = list(new)
108 if util.safehasattr(dirstate, 'journalstorage'): 114 if util.safehasattr(dirstate, 'journalstorage'):
109 # only record two hashes if there was a merge 115 # only record two hashes if there was a merge
110 oldhashes = old[:1] if old[1] == node.nullid else old 116 oldhashes = old[:1] if old[1] == node.nullid else old
111 newhashes = new[:1] if new[1] == node.nullid else new 117 newhashes = new[:1] if new[1] == node.nullid else new
112 dirstate.journalstorage.record( 118 dirstate.journalstorage.record(
113 wdirparenttype, '.', oldhashes, newhashes) 119 wdirparenttype, '.', oldhashes, newhashes
120 )
121
114 122
115 # hooks to record bookmark changes (both local and remote) 123 # hooks to record bookmark changes (both local and remote)
116 def recordbookmarks(orig, store, fp): 124 def recordbookmarks(orig, store, fp):
117 """Records all bookmark changes in the journal.""" 125 """Records all bookmark changes in the journal."""
118 repo = store._repo 126 repo = store._repo
122 oldvalue = oldmarks.get(mark, node.nullid) 130 oldvalue = oldmarks.get(mark, node.nullid)
123 if value != oldvalue: 131 if value != oldvalue:
124 repo.journal.record(bookmarktype, mark, oldvalue, value) 132 repo.journal.record(bookmarktype, mark, oldvalue, value)
125 return orig(store, fp) 133 return orig(store, fp)
126 134
135
127 # shared repository support 136 # shared repository support
128 def _readsharedfeatures(repo): 137 def _readsharedfeatures(repo):
129 """A set of shared features for this repository""" 138 """A set of shared features for this repository"""
130 try: 139 try:
131 return set(repo.vfs.read('shared').splitlines()) 140 return set(repo.vfs.read('shared').splitlines())
132 except IOError as inst: 141 except IOError as inst:
133 if inst.errno != errno.ENOENT: 142 if inst.errno != errno.ENOENT:
134 raise 143 raise
135 return set() 144 return set()
145
136 146
137 def _mergeentriesiter(*iterables, **kwargs): 147 def _mergeentriesiter(*iterables, **kwargs):
138 """Given a set of sorted iterables, yield the next entry in merged order 148 """Given a set of sorted iterables, yield the next entry in merged order
139 149
140 Note that by default entries go from most recent to oldest. 150 Note that by default entries go from most recent to oldest.
160 iterable_map[key][0] = next(it) 170 iterable_map[key][0] = next(it)
161 except StopIteration: 171 except StopIteration:
162 # this iterable is empty, remove it from consideration 172 # this iterable is empty, remove it from consideration
163 del iterable_map[key] 173 del iterable_map[key]
164 174
175
165 def wrappostshare(orig, sourcerepo, destrepo, **kwargs): 176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
166 """Mark this shared working copy as sharing journal information""" 177 """Mark this shared working copy as sharing journal information"""
167 with destrepo.wlock(): 178 with destrepo.wlock():
168 orig(sourcerepo, destrepo, **kwargs) 179 orig(sourcerepo, destrepo, **kwargs)
169 with destrepo.vfs('shared', 'a') as fp: 180 with destrepo.vfs('shared', 'a') as fp:
170 fp.write('journal\n') 181 fp.write('journal\n')
171 182
183
172 def unsharejournal(orig, ui, repo, repopath): 184 def unsharejournal(orig, ui, repo, repopath):
173 """Copy shared journal entries into this repo when unsharing""" 185 """Copy shared journal entries into this repo when unsharing"""
174 if (repo.path == repopath and repo.shared() and 186 if (
175 util.safehasattr(repo, 'journal')): 187 repo.path == repopath
188 and repo.shared()
189 and util.safehasattr(repo, 'journal')
190 ):
176 sharedrepo = hg.sharedreposource(repo) 191 sharedrepo = hg.sharedreposource(repo)
177 sharedfeatures = _readsharedfeatures(repo) 192 sharedfeatures = _readsharedfeatures(repo)
178 if sharedrepo and sharedfeatures > {'journal'}: 193 if sharedrepo and sharedfeatures > {'journal'}:
179 # there is a shared repository and there are shared journal entries 194 # there is a shared repository and there are shared journal entries
180 # to copy. move shared date over from source to destination but 195 # to copy. move shared date over from source to destination but
182 if repo.vfs.exists('namejournal'): 197 if repo.vfs.exists('namejournal'):
183 journalpath = repo.vfs.join('namejournal') 198 journalpath = repo.vfs.join('namejournal')
184 util.rename(journalpath, journalpath + '.bak') 199 util.rename(journalpath, journalpath + '.bak')
185 storage = repo.journal 200 storage = repo.journal
186 local = storage._open( 201 local = storage._open(
187 repo.vfs, filename='namejournal.bak', _newestfirst=False) 202 repo.vfs, filename='namejournal.bak', _newestfirst=False
203 )
188 shared = ( 204 shared = (
189 e for e in storage._open(sharedrepo.vfs, _newestfirst=False) 205 e
190 if sharednamespaces.get(e.namespace) in sharedfeatures) 206 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
207 if sharednamespaces.get(e.namespace) in sharedfeatures
208 )
191 for entry in _mergeentriesiter(local, shared, order=min): 209 for entry in _mergeentriesiter(local, shared, order=min):
192 storage._write(repo.vfs, entry) 210 storage._write(repo.vfs, entry)
193 211
194 return orig(ui, repo, repopath) 212 return orig(ui, repo, repopath)
195 213
196 class journalentry(collections.namedtuple( 214
215 class journalentry(
216 collections.namedtuple(
197 r'journalentry', 217 r'journalentry',
198 r'timestamp user command namespace name oldhashes newhashes')): 218 r'timestamp user command namespace name oldhashes newhashes',
219 )
220 ):
199 """Individual journal entry 221 """Individual journal entry
200 222
201 * timestamp: a mercurial (time, timezone) tuple 223 * timestamp: a mercurial (time, timezone) tuple
202 * user: the username that ran the command 224 * user: the username that ran the command
203 * namespace: the entry namespace, an opaque string 225 * namespace: the entry namespace, an opaque string
210 Handles serialisation from and to the storage format. Fields are 232 Handles serialisation from and to the storage format. Fields are
211 separated by newlines, hashes are written out in hex separated by commas, 233 separated by newlines, hashes are written out in hex separated by commas,
212 timestamp and timezone are separated by a space. 234 timestamp and timezone are separated by a space.
213 235
214 """ 236 """
237
215 @classmethod 238 @classmethod
216 def fromstorage(cls, line): 239 def fromstorage(cls, line):
217 (time, user, command, namespace, name, 240 (
218 oldhashes, newhashes) = line.split('\n') 241 time,
242 user,
243 command,
244 namespace,
245 name,
246 oldhashes,
247 newhashes,
248 ) = line.split('\n')
219 timestamp, tz = time.split() 249 timestamp, tz = time.split()
220 timestamp, tz = float(timestamp), int(tz) 250 timestamp, tz = float(timestamp), int(tz)
221 oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(',')) 251 oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(','))
222 newhashes = tuple(node.bin(hash) for hash in newhashes.split(',')) 252 newhashes = tuple(node.bin(hash) for hash in newhashes.split(','))
223 return cls( 253 return cls(
224 (timestamp, tz), user, command, namespace, name, 254 (timestamp, tz),
225 oldhashes, newhashes) 255 user,
256 command,
257 namespace,
258 name,
259 oldhashes,
260 newhashes,
261 )
226 262
227 def __bytes__(self): 263 def __bytes__(self):
228 """bytes representation for storage""" 264 """bytes representation for storage"""
229 time = ' '.join(map(pycompat.bytestr, self.timestamp)) 265 time = ' '.join(map(pycompat.bytestr, self.timestamp))
230 oldhashes = ','.join([node.hex(hash) for hash in self.oldhashes]) 266 oldhashes = ','.join([node.hex(hash) for hash in self.oldhashes])
231 newhashes = ','.join([node.hex(hash) for hash in self.newhashes]) 267 newhashes = ','.join([node.hex(hash) for hash in self.newhashes])
232 return '\n'.join(( 268 return '\n'.join(
233 time, self.user, self.command, self.namespace, self.name, 269 (
234 oldhashes, newhashes)) 270 time,
271 self.user,
272 self.command,
273 self.namespace,
274 self.name,
275 oldhashes,
276 newhashes,
277 )
278 )
235 279
236 __str__ = encoding.strmethod(__bytes__) 280 __str__ = encoding.strmethod(__bytes__)
281
237 282
238 class journalstorage(object): 283 class journalstorage(object):
239 """Storage for journal entries 284 """Storage for journal entries
240 285
241 Entries are divided over two files; one with entries that pertain to the 286 Entries are divided over two files; one with entries that pertain to the
250 This storage uses a dedicated lock; this makes it easier to avoid issues 295 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. 296 with adding entries that added when the regular wlock is unlocked (e.g.
252 the dirstate). 297 the dirstate).
253 298
254 """ 299 """
300
255 _currentcommand = () 301 _currentcommand = ()
256 _lockref = None 302 _lockref = None
257 303
258 def __init__(self, repo): 304 def __init__(self, repo):
259 self.user = procutil.getuser() 305 self.user = procutil.getuser()
271 317
272 # track the current command for recording in journal entries 318 # track the current command for recording in journal entries
273 @property 319 @property
274 def command(self): 320 def command(self):
275 commandstr = ' '.join( 321 commandstr = ' '.join(
276 map(procutil.shellquote, journalstorage._currentcommand)) 322 map(procutil.shellquote, journalstorage._currentcommand)
323 )
277 if '\n' in commandstr: 324 if '\n' in commandstr:
278 # truncate multi-line commands 325 # truncate multi-line commands
279 commandstr = commandstr.partition('\n')[0] + ' ...' 326 commandstr = commandstr.partition('\n')[0] + ' ...'
280 return commandstr 327 return commandstr
281 328
305 desc = _('journal of %s') % vfs.base 352 desc = _('journal of %s') % vfs.base
306 try: 353 try:
307 l = lock.lock(vfs, 'namejournal.lock', 0, desc=desc) 354 l = lock.lock(vfs, 'namejournal.lock', 0, desc=desc)
308 except error.LockHeld as inst: 355 except error.LockHeld as inst:
309 self.ui.warn( 356 self.ui.warn(
310 _("waiting for lock on %s held by %r\n") % (desc, inst.locker)) 357 _("waiting for lock on %s held by %r\n") % (desc, inst.locker)
358 )
311 # default to 600 seconds timeout 359 # default to 600 seconds timeout
312 l = lock.lock( 360 l = lock.lock(
313 vfs, 'namejournal.lock', 361 vfs,
314 self.ui.configint("ui", "timeout"), desc=desc) 362 'namejournal.lock',
363 self.ui.configint("ui", "timeout"),
364 desc=desc,
365 )
315 self.ui.warn(_("got lock after %s seconds\n") % l.delay) 366 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
316 self._lockref = weakref.ref(l) 367 self._lockref = weakref.ref(l)
317 return l 368 return l
318 369
319 def record(self, namespace, name, oldhashes, newhashes): 370 def record(self, namespace, name, oldhashes, newhashes):
332 oldhashes = [oldhashes] 383 oldhashes = [oldhashes]
333 if not isinstance(newhashes, list): 384 if not isinstance(newhashes, list):
334 newhashes = [newhashes] 385 newhashes = [newhashes]
335 386
336 entry = journalentry( 387 entry = journalentry(
337 dateutil.makedate(), self.user, self.command, namespace, name, 388 dateutil.makedate(),
338 oldhashes, newhashes) 389 self.user,
390 self.command,
391 namespace,
392 name,
393 oldhashes,
394 newhashes,
395 )
339 396
340 vfs = self.vfs 397 vfs = self.vfs
341 if self.sharedvfs is not None: 398 if self.sharedvfs is not None:
342 # write to the shared repository if this feature is being 399 # write to the shared repository if this feature is being
343 # shared between working copies. 400 # shared between working copies.
358 # different version of the storage. Exit early (and not 415 # different version of the storage. Exit early (and not
359 # write anything) if this is not a version we can handle or 416 # write anything) if this is not a version we can handle or
360 # the file is corrupt. In future, perhaps rotate the file 417 # the file is corrupt. In future, perhaps rotate the file
361 # instead? 418 # instead?
362 self.ui.warn( 419 self.ui.warn(
363 _("unsupported journal file version '%s'\n") % version) 420 _("unsupported journal file version '%s'\n") % version
421 )
364 return 422 return
365 if not version: 423 if not version:
366 # empty file, write version first 424 # empty file, write version first
367 f.write(("%d" % storageversion) + '\0') 425 f.write(("%d" % storageversion) + '\0')
368 f.seek(0, os.SEEK_END) 426 f.seek(0, os.SEEK_END)
401 return local 459 return local
402 460
403 # iterate over both local and shared entries, but only those 461 # iterate over both local and shared entries, but only those
404 # shared entries that are among the currently shared features 462 # shared entries that are among the currently shared features
405 shared = ( 463 shared = (
406 e for e in self._open(self.sharedvfs) 464 e
407 if sharednamespaces.get(e.namespace) in self.sharedfeatures) 465 for e in self._open(self.sharedvfs)
466 if sharednamespaces.get(e.namespace) in self.sharedfeatures
467 )
408 return _mergeentriesiter(local, shared) 468 return _mergeentriesiter(local, shared)
409 469
410 def _open(self, vfs, filename='namejournal', _newestfirst=True): 470 def _open(self, vfs, filename='namejournal', _newestfirst=True):
411 if not vfs.exists(filename): 471 if not vfs.exists(filename):
412 return 472 return
429 for line in lines: 489 for line in lines:
430 if not line: 490 if not line:
431 continue 491 continue
432 yield journalentry.fromstorage(line) 492 yield journalentry.fromstorage(line)
433 493
494
434 # journal reading 495 # journal reading
435 # log options that don't make sense for journal 496 # log options that don't make sense for journal
436 _ignoreopts = ('no-merges', 'graph') 497 _ignoreopts = ('no-merges', 'graph')
498
499
437 @command( 500 @command(
438 'journal', [ 501 'journal',
502 [
439 ('', 'all', None, 'show history for all names'), 503 ('', 'all', None, 'show history for all names'),
440 ('c', 'commits', None, 'show commit metadata'), 504 ('c', 'commits', None, 'show commit metadata'),
441 ] + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts], 505 ]
506 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
442 '[OPTION]... [BOOKMARKNAME]', 507 '[OPTION]... [BOOKMARKNAME]',
443 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION) 508 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
509 )
444 def journal(ui, repo, *args, **opts): 510 def journal(ui, repo, *args, **opts):
445 """show the previous position of bookmarks and the working copy 511 """show the previous position of bookmarks and the working copy
446 512
447 The journal is used to see the previous commits that bookmarks and the 513 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 514 working copy pointed to. By default the previous locations for the working
469 opts = pycompat.byteskwargs(opts) 535 opts = pycompat.byteskwargs(opts)
470 name = '.' 536 name = '.'
471 if opts.get('all'): 537 if opts.get('all'):
472 if args: 538 if args:
473 raise error.Abort( 539 raise error.Abort(
474 _("You can't combine --all and filtering on a name")) 540 _("You can't combine --all and filtering on a name")
541 )
475 name = None 542 name = None
476 if args: 543 if args:
477 name = args[0] 544 name = args[0]
478 545
479 fm = ui.formatter('journal', opts) 546 fm = ui.formatter('journal', opts)
547
480 def formatnodes(nodes): 548 def formatnodes(nodes):
481 return fm.formatlist(map(fm.hexfunc, nodes), name='node', sep=',') 549 return fm.formatlist(map(fm.hexfunc, nodes), name='node', sep=',')
482 550
483 if opts.get("template") != "json": 551 if opts.get("template") != "json":
484 if name is None: 552 if name is None:
493 for count, entry in enumerate(repo.journal.filtered(name=name)): 561 for count, entry in enumerate(repo.journal.filtered(name=name)):
494 if count == limit: 562 if count == limit:
495 break 563 break
496 564
497 fm.startitem() 565 fm.startitem()
498 fm.condwrite(ui.verbose, 'oldnodes', '%s -> ', 566 fm.condwrite(
499 formatnodes(entry.oldhashes)) 567 ui.verbose, 'oldnodes', '%s -> ', formatnodes(entry.oldhashes)
568 )
500 fm.write('newnodes', '%s', formatnodes(entry.newhashes)) 569 fm.write('newnodes', '%s', formatnodes(entry.newhashes))
501 fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user) 570 fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user)
502 fm.condwrite( 571 fm.condwrite(
503 opts.get('all') or name.startswith('re:'), 572 opts.get('all') or name.startswith('re:'),
504 'name', ' %-8s', entry.name) 573 'name',
505 574 ' %-8s',
506 fm.condwrite(ui.verbose, 'date', ' %s', 575 entry.name,
507 fm.formatdate(entry.timestamp, '%Y-%m-%d %H:%M %1%2')) 576 )
577
578 fm.condwrite(
579 ui.verbose,
580 'date',
581 ' %s',
582 fm.formatdate(entry.timestamp, '%Y-%m-%d %H:%M %1%2'),
583 )
508 fm.write('command', ' %s\n', entry.command) 584 fm.write('command', ' %s\n', entry.command)
509 585
510 if opts.get("commits"): 586 if opts.get("commits"):
511 if fm.isplain(): 587 if fm.isplain():
512 displayer = logcmdutil.changesetdisplayer(ui, repo, opts) 588 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
513 else: 589 else:
514 displayer = logcmdutil.changesetformatter( 590 displayer = logcmdutil.changesetformatter(
515 ui, repo, fm.nested('changesets'), diffopts=opts) 591 ui, repo, fm.nested('changesets'), diffopts=opts
592 )
516 for hash in entry.newhashes: 593 for hash in entry.newhashes:
517 try: 594 try:
518 ctx = repo[hash] 595 ctx = repo[hash]
519 displayer.show(ctx) 596 displayer.show(ctx)
520 except error.RepoLookupError as e: 597 except error.RepoLookupError as e: