41 # storage format version; increment when the format changes |
45 # storage format version; increment when the format changes |
42 storageversion = 0 |
46 storageversion = 0 |
43 |
47 |
44 # namespaces |
48 # namespaces |
45 bookmarktype = 'bookmark' |
49 bookmarktype = 'bookmark' |
|
50 wdirparenttype = 'wdirparent' |
46 |
51 |
47 # Journal recording, register hooks and storage object |
52 # Journal recording, register hooks and storage object |
48 def extsetup(ui): |
53 def extsetup(ui): |
49 extensions.wrapfunction(dispatch, 'runcommand', runcommand) |
54 extensions.wrapfunction(dispatch, 'runcommand', runcommand) |
50 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks) |
55 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks) |
|
56 extensions.wrapfunction( |
|
57 dirstate.dirstate, '_writedirstate', recorddirstateparents) |
|
58 extensions.wrapfunction( |
|
59 localrepo.localrepository.dirstate, 'func', wrapdirstate) |
51 |
60 |
52 def reposetup(ui, repo): |
61 def reposetup(ui, repo): |
53 if repo.local(): |
62 if repo.local(): |
54 repo.journal = journalstorage(repo) |
63 repo.journal = journalstorage(repo) |
55 |
64 |
56 def runcommand(orig, lui, repo, cmd, fullargs, *args): |
65 def runcommand(orig, lui, repo, cmd, fullargs, *args): |
57 """Track the command line options for recording in the journal""" |
66 """Track the command line options for recording in the journal""" |
58 journalstorage.recordcommand(*fullargs) |
67 journalstorage.recordcommand(*fullargs) |
59 return orig(lui, repo, cmd, fullargs, *args) |
68 return orig(lui, repo, cmd, fullargs, *args) |
60 |
69 |
|
70 # hooks to record dirstate changes |
|
71 def wrapdirstate(orig, repo): |
|
72 """Make journal storage available to the dirstate object""" |
|
73 dirstate = orig(repo) |
|
74 if util.safehasattr(repo, 'journal'): |
|
75 dirstate.journalstorage = repo.journal |
|
76 return dirstate |
|
77 |
|
78 def recorddirstateparents(orig, dirstate, dirstatefp): |
|
79 """Records all dirstate parent changes in the journal.""" |
|
80 if util.safehasattr(dirstate, 'journalstorage'): |
|
81 old = [node.nullid, node.nullid] |
|
82 nodesize = len(node.nullid) |
|
83 try: |
|
84 # The only source for the old state is in the dirstate file still |
|
85 # on disk; the in-memory dirstate object only contains the new |
|
86 # state. dirstate._opendirstatefile() switches beteen .hg/dirstate |
|
87 # and .hg/dirstate.pending depending on the transaction state. |
|
88 with dirstate._opendirstatefile() as fp: |
|
89 state = fp.read(2 * nodesize) |
|
90 if len(state) == 2 * nodesize: |
|
91 old = [state[:nodesize], state[nodesize:]] |
|
92 except IOError: |
|
93 pass |
|
94 |
|
95 new = dirstate.parents() |
|
96 if old != new: |
|
97 # only record two hashes if there was a merge |
|
98 oldhashes = old[:1] if old[1] == node.nullid else old |
|
99 newhashes = new[:1] if new[1] == node.nullid else new |
|
100 dirstate.journalstorage.record( |
|
101 wdirparenttype, '.', oldhashes, newhashes) |
|
102 |
|
103 return orig(dirstate, dirstatefp) |
|
104 |
|
105 # hooks to record bookmark changes (both local and remote) |
61 def recordbookmarks(orig, store, fp): |
106 def recordbookmarks(orig, store, fp): |
62 """Records all bookmark changes in the journal.""" |
107 """Records all bookmark changes in the journal.""" |
63 repo = store._repo |
108 repo = store._repo |
64 if util.safehasattr(repo, 'journal'): |
109 if util.safehasattr(repo, 'journal'): |
65 oldmarks = bookmarks.bmstore(repo) |
110 oldmarks = bookmarks.bmstore(repo) |
140 """Set the current hg arguments, stored with recorded entries""" |
190 """Set the current hg arguments, stored with recorded entries""" |
141 # Set the current command on the class because we may have started |
191 # Set the current command on the class because we may have started |
142 # with a non-local repo (cloning for example). |
192 # with a non-local repo (cloning for example). |
143 cls._currentcommand = fullargs |
193 cls._currentcommand = fullargs |
144 |
194 |
|
195 def jlock(self): |
|
196 """Create a lock for the journal file""" |
|
197 if self._lockref and self._lockref(): |
|
198 raise error.Abort(_('journal lock does not support nesting')) |
|
199 desc = _('journal of %s') % self.vfs.base |
|
200 try: |
|
201 l = lock.lock(self.vfs, 'journal.lock', 0, desc=desc) |
|
202 except error.LockHeld as inst: |
|
203 self.ui.warn( |
|
204 _("waiting for lock on %s held by %r\n") % (desc, inst.locker)) |
|
205 # default to 600 seconds timeout |
|
206 l = lock.lock( |
|
207 self.vfs, 'journal.lock', |
|
208 int(self.ui.config("ui", "timeout", "600")), desc=desc) |
|
209 self.ui.warn(_("got lock after %s seconds\n") % l.delay) |
|
210 self._lockref = weakref.ref(l) |
|
211 return l |
|
212 |
145 def record(self, namespace, name, oldhashes, newhashes): |
213 def record(self, namespace, name, oldhashes, newhashes): |
146 """Record a new journal entry |
214 """Record a new journal entry |
147 |
215 |
148 * namespace: an opaque string; this can be used to filter on the type |
216 * namespace: an opaque string; this can be used to filter on the type |
149 of recorded entries. |
217 of recorded entries. |
161 |
229 |
162 entry = journalentry( |
230 entry = journalentry( |
163 util.makedate(), self.user, self.command, namespace, name, |
231 util.makedate(), self.user, self.command, namespace, name, |
164 oldhashes, newhashes) |
232 oldhashes, newhashes) |
165 |
233 |
166 with self.repo.wlock(): |
234 with self.jlock(): |
167 version = None |
235 version = None |
168 # open file in amend mode to ensure it is created if missing |
236 # open file in amend mode to ensure it is created if missing |
169 with self.vfs('journal', mode='a+b', atomictemp=True) as f: |
237 with self.vfs('journal', mode='a+b', atomictemp=True) as f: |
170 f.seek(0, os.SEEK_SET) |
238 f.seek(0, os.SEEK_SET) |
171 # Read just enough bytes to get a version number (up to 2 |
239 # Read just enough bytes to get a version number (up to 2 |
227 # journal reading |
295 # journal reading |
228 # log options that don't make sense for journal |
296 # log options that don't make sense for journal |
229 _ignoreopts = ('no-merges', 'graph') |
297 _ignoreopts = ('no-merges', 'graph') |
230 @command( |
298 @command( |
231 'journal', [ |
299 'journal', [ |
|
300 ('', 'all', None, 'show history for all names'), |
232 ('c', 'commits', None, 'show commit metadata'), |
301 ('c', 'commits', None, 'show commit metadata'), |
233 ] + [opt for opt in commands.logopts if opt[1] not in _ignoreopts], |
302 ] + [opt for opt in commands.logopts if opt[1] not in _ignoreopts], |
234 '[OPTION]... [BOOKMARKNAME]') |
303 '[OPTION]... [BOOKMARKNAME]') |
235 def journal(ui, repo, *args, **opts): |
304 def journal(ui, repo, *args, **opts): |
236 """show the previous position of bookmarks |
305 """show the previous position of bookmarks and the working copy |
237 |
306 |
238 The journal is used to see the previous commits of bookmarks. By default |
307 The journal is used to see the previous commits that bookmarks and the |
239 the previous locations for all bookmarks are shown. Passing a bookmark |
308 working copy pointed to. By default the previous locations for the working |
240 name will show all the previous positions of that bookmark. |
309 copy. Passing a bookmark name will show all the previous positions of |
|
310 that bookmark. Use the --all switch to show previous locations for all |
|
311 bookmarks and the working copy; each line will then include the bookmark |
|
312 name, or '.' for the working copy, as well. |
241 |
313 |
242 By default hg journal only shows the commit hash and the command that was |
314 By default hg journal only shows the commit hash and the command that was |
243 running at that time. -v/--verbose will show the prior hash, the user, and |
315 running at that time. -v/--verbose will show the prior hash, the user, and |
244 the time at which it happened. |
316 the time at which it happened. |
245 |
317 |
248 switches to alter the log output for these. |
320 switches to alter the log output for these. |
249 |
321 |
250 `hg journal -T json` can be used to produce machine readable output. |
322 `hg journal -T json` can be used to produce machine readable output. |
251 |
323 |
252 """ |
324 """ |
253 bookmarkname = None |
325 name = '.' |
|
326 if opts.get('all'): |
|
327 if args: |
|
328 raise error.Abort( |
|
329 _("You can't combine --all and filtering on a name")) |
|
330 name = None |
254 if args: |
331 if args: |
255 bookmarkname = args[0] |
332 name = args[0] |
256 |
333 |
257 fm = ui.formatter('journal', opts) |
334 fm = ui.formatter('journal', opts) |
258 |
335 |
259 if opts.get("template") != "json": |
336 if opts.get("template") != "json": |
260 if bookmarkname is None: |
337 if name is None: |
261 name = _('all bookmarks') |
338 displayname = _('the working copy and bookmarks') |
262 else: |
339 else: |
263 name = "'%s'" % bookmarkname |
340 displayname = "'%s'" % name |
264 ui.status(_("previous locations of %s:\n") % name) |
341 ui.status(_("previous locations of %s:\n") % displayname) |
265 |
342 |
266 limit = cmdutil.loglimit(opts) |
343 limit = cmdutil.loglimit(opts) |
267 entry = None |
344 entry = None |
268 for count, entry in enumerate(repo.journal.filtered(name=bookmarkname)): |
345 for count, entry in enumerate(repo.journal.filtered(name=name)): |
269 if count == limit: |
346 if count == limit: |
270 break |
347 break |
271 newhashesstr = ','.join([node.short(hash) for hash in entry.newhashes]) |
348 newhashesstr = ','.join([node.short(hash) for hash in entry.newhashes]) |
272 oldhashesstr = ','.join([node.short(hash) for hash in entry.oldhashes]) |
349 oldhashesstr = ','.join([node.short(hash) for hash in entry.oldhashes]) |
273 |
350 |
274 fm.startitem() |
351 fm.startitem() |
275 fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr) |
352 fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr) |
276 fm.write('newhashes', '%s', newhashesstr) |
353 fm.write('newhashes', '%s', newhashesstr) |
277 fm.condwrite(ui.verbose, 'user', ' %s', entry.user.ljust(8)) |
354 fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user) |
|
355 fm.condwrite(opts.get('all'), 'name', ' %-8s', entry.name) |
278 |
356 |
279 timestring = util.datestr(entry.timestamp, '%Y-%m-%d %H:%M %1%2') |
357 timestring = util.datestr(entry.timestamp, '%Y-%m-%d %H:%M %1%2') |
280 fm.condwrite(ui.verbose, 'date', ' %s', timestring) |
358 fm.condwrite(ui.verbose, 'date', ' %s', timestring) |
281 fm.write('command', ' %s\n', entry.command) |
359 fm.write('command', ' %s\n', entry.command) |
282 |
360 |