103 entries, |
103 entries, |
104 backupentries, |
104 backupentries, |
105 unlink=True, |
105 unlink=True, |
106 checkambigfiles=None, |
106 checkambigfiles=None, |
107 ): |
107 ): |
|
108 """rollback a transaction : |
|
109 - truncate files that have been appended to |
|
110 - restore file backups |
|
111 - delete temporary files |
|
112 """ |
108 backupfiles = [] |
113 backupfiles = [] |
109 |
114 |
110 def restore_one_backup(vfs, f, b, checkambig): |
115 def restore_one_backup(vfs, f, b, checkambig): |
111 filepath = vfs.join(f) |
116 filepath = vfs.join(f) |
112 backuppath = vfs.join(b) |
117 backuppath = vfs.join(b) |
116 except IOError as exc: |
121 except IOError as exc: |
117 e_msg = stringutil.forcebytestr(exc) |
122 e_msg = stringutil.forcebytestr(exc) |
118 report(_(b"failed to recover %s (%s)\n") % (f, e_msg)) |
123 report(_(b"failed to recover %s (%s)\n") % (f, e_msg)) |
119 raise |
124 raise |
120 |
125 |
|
126 # gather all backup files that impact the store |
|
127 # (we need this to detect files that are both backed up and truncated) |
|
128 store_backup = {} |
|
129 for entry in backupentries: |
|
130 location, file_path, backup_path, cache = entry |
|
131 vfs = vfsmap[location] |
|
132 is_store = vfs.join(b'') == opener.join(b'') |
|
133 if is_store and file_path and backup_path: |
|
134 store_backup[file_path] = entry |
|
135 copy_done = set() |
|
136 |
|
137 # truncate all file `f` to offset `o` |
121 for f, o in sorted(dict(entries).items()): |
138 for f, o in sorted(dict(entries).items()): |
|
139 # if we have a backup for `f`, we should restore it first and truncate |
|
140 # the restored file |
|
141 bck_entry = store_backup.get(f) |
|
142 if bck_entry is not None: |
|
143 location, file_path, backup_path, cache = bck_entry |
|
144 checkambig = False |
|
145 if checkambigfiles: |
|
146 checkambig = (file_path, location) in checkambigfiles |
|
147 restore_one_backup(opener, file_path, backup_path, checkambig) |
|
148 copy_done.add(bck_entry) |
|
149 # truncate the file to its pre-transaction size |
122 if o or not unlink: |
150 if o or not unlink: |
123 checkambig = checkambigfiles and (f, b'') in checkambigfiles |
151 checkambig = checkambigfiles and (f, b'') in checkambigfiles |
124 try: |
152 try: |
125 fp = opener(f, b'a', checkambig=checkambig) |
153 fp = opener(f, b'a', checkambig=checkambig) |
126 if fp.tell() < o: |
154 if fp.tell() < o: |
135 fp.close() |
163 fp.close() |
136 except IOError: |
164 except IOError: |
137 report(_(b"failed to truncate %s\n") % f) |
165 report(_(b"failed to truncate %s\n") % f) |
138 raise |
166 raise |
139 else: |
167 else: |
|
168 # delete empty file |
140 try: |
169 try: |
141 opener.unlink(f) |
170 opener.unlink(f) |
142 except FileNotFoundError: |
171 except FileNotFoundError: |
143 pass |
172 pass |
144 |
173 # restore backed up files and clean up temporary files |
145 for l, f, b, c in backupentries: |
174 for entry in backupentries: |
|
175 if entry in copy_done: |
|
176 continue |
|
177 l, f, b, c = entry |
146 if l not in vfsmap and c: |
178 if l not in vfsmap and c: |
147 report(b"couldn't handle %s: unknown cache location %s\n" % (b, l)) |
179 report(b"couldn't handle %s: unknown cache location %s\n" % (b, l)) |
148 vfs = vfsmap[l] |
180 vfs = vfsmap[l] |
149 try: |
181 try: |
150 checkambig = checkambigfiles and (f, l) in checkambigfiles |
182 checkambig = checkambigfiles and (f, l) in checkambigfiles |
168 pass |
200 pass |
169 except (IOError, OSError, error.Abort): |
201 except (IOError, OSError, error.Abort): |
170 if not c: |
202 if not c: |
171 raise |
203 raise |
172 |
204 |
|
205 # cleanup transaction state file and the backups file |
173 backuppath = b"%s.backupfiles" % journal |
206 backuppath = b"%s.backupfiles" % journal |
174 if opener.exists(backuppath): |
207 if opener.exists(backuppath): |
175 opener.unlink(backuppath) |
208 opener.unlink(backuppath) |
176 opener.unlink(journal) |
209 opener.unlink(journal) |
177 try: |
210 try: |
344 # add enough data to the journal to do the truncate |
377 # add enough data to the journal to do the truncate |
345 self._file.write(b"%s\0%d\n" % (file, offset)) |
378 self._file.write(b"%s\0%d\n" % (file, offset)) |
346 self._file.flush() |
379 self._file.flush() |
347 |
380 |
348 @active |
381 @active |
349 def addbackup(self, file, hardlink=True, location=b''): |
382 def addbackup(self, file, hardlink=True, location=b'', for_offset=False): |
350 """Adds a backup of the file to the transaction |
383 """Adds a backup of the file to the transaction |
351 |
384 |
352 Calling addbackup() creates a hardlink backup of the specified file |
385 Calling addbackup() creates a hardlink backup of the specified file |
353 that is used to recover the file in the event of the transaction |
386 that is used to recover the file in the event of the transaction |
354 aborting. |
387 aborting. |
355 |
388 |
356 * `file`: the file path, relative to .hg/store |
389 * `file`: the file path, relative to .hg/store |
357 * `hardlink`: use a hardlink to quickly create the backup |
390 * `hardlink`: use a hardlink to quickly create the backup |
|
391 |
|
392 If `for_offset` is set, we expect a offset for this file to have been previously recorded |
358 """ |
393 """ |
359 if self._queue: |
394 if self._queue: |
360 msg = b'cannot use transaction.addbackup inside "group"' |
395 msg = b'cannot use transaction.addbackup inside "group"' |
361 raise error.ProgrammingError(msg) |
396 raise error.ProgrammingError(msg) |
362 |
397 |
363 if ( |
398 if file in self._newfiles or file in self._backupmap: |
364 file in self._newfiles |
|
365 or file in self._offsetmap |
|
366 or file in self._backupmap |
|
367 ): |
|
368 return |
399 return |
|
400 elif file in self._offsetmap and not for_offset: |
|
401 return |
|
402 elif for_offset and file not in self._offsetmap: |
|
403 msg = ( |
|
404 'calling `addbackup` with `for_offmap=True`, ' |
|
405 'but no offset recorded: [%r] %r' |
|
406 ) |
|
407 msg %= (location, file) |
|
408 raise error.ProgrammingError(msg) |
|
409 |
369 vfs = self._vfsmap[location] |
410 vfs = self._vfsmap[location] |
370 dirname, filename = vfs.split(file) |
411 dirname, filename = vfs.split(file) |
371 backupfilename = b"%s.backup.%s" % (self._journal, filename) |
412 backupfilename = b"%s.backup.%s" % (self._journal, filename) |
372 backupfile = vfs.reljoin(dirname, backupfilename) |
413 backupfile = vfs.reljoin(dirname, backupfilename) |
373 if vfs.exists(file): |
414 if vfs.exists(file): |