##// END OF EJS Templates
commit: refactor salvage calculation to a different function...
Pulkit Goyal -
r46297:61454026 default
parent child Browse files
Show More
@@ -1,471 +1,480 b''
1 1 # commit.py - fonction to perform commit
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 import errno
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 nullid,
14 14 nullrev,
15 15 )
16 16
17 17 from . import (
18 18 context,
19 19 mergestate,
20 20 metadata,
21 21 phases,
22 22 scmutil,
23 23 subrepoutil,
24 24 )
25 25
26 26
27 27 def _write_copy_meta(repo):
28 28 """return a (changelog, filelog) boolean tuple
29 29
30 30 changelog: copy related information should be stored in the changeset
31 31 filelof: copy related information should be written in the file revision
32 32 """
33 33 if repo.filecopiesmode == b'changeset-sidedata':
34 34 writechangesetcopy = True
35 35 writefilecopymeta = True
36 36 else:
37 37 writecopiesto = repo.ui.config(b'experimental', b'copies.write-to')
38 38 writefilecopymeta = writecopiesto != b'changeset-only'
39 39 writechangesetcopy = writecopiesto in (
40 40 b'changeset-only',
41 41 b'compatibility',
42 42 )
43 43 return writechangesetcopy, writefilecopymeta
44 44
45 45
46 46 def commitctx(repo, ctx, error=False, origctx=None):
47 47 """Add a new revision to the target repository.
48 48 Revision information is passed via the context argument.
49 49
50 50 ctx.files() should list all files involved in this commit, i.e.
51 51 modified/added/removed files. On merge, it may be wider than the
52 52 ctx.files() to be committed, since any file nodes derived directly
53 53 from p1 or p2 are excluded from the committed ctx.files().
54 54
55 55 origctx is for convert to work around the problem that bug
56 56 fixes to the files list in changesets change hashes. For
57 57 convert to be the identity, it can pass an origctx and this
58 58 function will use the same files list when it makes sense to
59 59 do so.
60 60 """
61 61 repo = repo.unfiltered()
62 62
63 63 p1, p2 = ctx.p1(), ctx.p2()
64 64 user = ctx.user()
65 65
66 66 with repo.lock(), repo.transaction(b"commit") as tr:
67 67 mn, files = _prepare_files(tr, ctx, error=error, origctx=origctx)
68 68
69 69 extra = ctx.extra().copy()
70 70
71 71 if extra is not None:
72 72 for name in (
73 73 b'p1copies',
74 74 b'p2copies',
75 75 b'filesadded',
76 76 b'filesremoved',
77 77 ):
78 78 extra.pop(name, None)
79 79 if repo.changelog._copiesstorage == b'extra':
80 80 extra = _extra_with_copies(repo, extra, files)
81 81
82 82 # update changelog
83 83 repo.ui.note(_(b"committing changelog\n"))
84 84 repo.changelog.delayupdate(tr)
85 85 n = repo.changelog.add(
86 86 mn,
87 87 files,
88 88 ctx.description(),
89 89 tr,
90 90 p1.node(),
91 91 p2.node(),
92 92 user,
93 93 ctx.date(),
94 94 extra,
95 95 )
96 96 xp1, xp2 = p1.hex(), p2 and p2.hex() or b''
97 97 repo.hook(
98 98 b'pretxncommit', throw=True, node=hex(n), parent1=xp1, parent2=xp2,
99 99 )
100 100 # set the new commit is proper phase
101 101 targetphase = subrepoutil.newcommitphase(repo.ui, ctx)
102 102 if targetphase:
103 103 # retract boundary do not alter parent changeset.
104 104 # if a parent have higher the resulting phase will
105 105 # be compliant anyway
106 106 #
107 107 # if minimal phase was 0 we don't need to retract anything
108 108 phases.registernew(repo, tr, targetphase, [n])
109 109 return n
110 110
111 111
112 112 def _prepare_files(tr, ctx, error=False, origctx=None):
113 113 repo = ctx.repo()
114 114 p1 = ctx.p1()
115 115
116 116 writechangesetcopy, writefilecopymeta = _write_copy_meta(repo)
117 117
118 118 if ctx.manifestnode():
119 119 # reuse an existing manifest revision
120 120 repo.ui.debug(b'reusing known manifest\n')
121 121 mn = ctx.manifestnode()
122 122 files = metadata.ChangingFiles()
123 123 files.update_touched(ctx.files())
124 124 if writechangesetcopy:
125 125 files.update_added(ctx.filesadded())
126 126 files.update_removed(ctx.filesremoved())
127 127 elif not ctx.files():
128 128 repo.ui.debug(b'reusing manifest from p1 (no file change)\n')
129 129 mn = p1.manifestnode()
130 130 files = metadata.ChangingFiles()
131 131 else:
132 132 mn, files = _process_files(tr, ctx, error=error)
133 133
134 134 if origctx and origctx.manifestnode() == mn:
135 135 origfiles = origctx.files()
136 136 assert files.touched.issubset(origfiles)
137 137 files.update_touched(origfiles)
138 138
139 139 if writechangesetcopy:
140 140 files.update_copies_from_p1(ctx.p1copies())
141 141 files.update_copies_from_p2(ctx.p2copies())
142 142
143 copy_sd = ctx.repo().filecopiesmode == b'changeset-sidedata'
143 ms = mergestate.mergestate.read(repo)
144 salvaged = _get_salvaged(ctx.repo(), ms, ctx)
145 for s in salvaged:
146 files.mark_salvaged(s)
147
148 return mn, files
149
150
151 def _get_salvaged(repo, ms, ctx):
152 """ returns a list of salvaged files
153
154 returns empty list if config option which process salvaged files are
155 not enabled """
156 salvaged = []
157 copy_sd = repo.filecopiesmode == b'changeset-sidedata'
144 158 if copy_sd and len(ctx.parents()) > 1:
145 # XXX this `mergestate.read` could be duplicated with a the merge state
146 # reading in _process_files So we could refactor further to reuse it in
147 # some cases.
148 ms = mergestate.mergestate.read(repo)
149 159 if ms.active():
150 160 for fname in sorted(ms._stateextras.keys()):
151 161 might_removed = ms.extras(fname).get(b'merge-removal-candidate')
152 162 if might_removed == b'yes':
153 163 if fname in ctx:
154 files.mark_salvaged(fname)
155
156 return mn, files
164 salvaged.append(fname)
165 return salvaged
157 166
158 167
159 168 def _process_files(tr, ctx, error=False):
160 169 repo = ctx.repo()
161 170 p1 = ctx.p1()
162 171 p2 = ctx.p2()
163 172
164 173 writechangesetcopy, writefilecopymeta = _write_copy_meta(repo)
165 174
166 175 m1ctx = p1.manifestctx()
167 176 m2ctx = p2.manifestctx()
168 177 mctx = m1ctx.copy()
169 178
170 179 m = mctx.read()
171 180 m1 = m1ctx.read()
172 181 m2 = m2ctx.read()
173 182 ms = mergestate.mergestate.read(repo)
174 183
175 184 files = metadata.ChangingFiles()
176 185
177 186 # check in files
178 187 added = []
179 188 removed = list(ctx.removed())
180 189 linkrev = len(repo)
181 190 repo.ui.note(_(b"committing files:\n"))
182 191 uipathfn = scmutil.getuipathfn(repo)
183 192 for f in sorted(ctx.modified() + ctx.added()):
184 193 repo.ui.note(uipathfn(f) + b"\n")
185 194 try:
186 195 fctx = ctx[f]
187 196 if fctx is None:
188 197 removed.append(f)
189 198 else:
190 199 added.append(f)
191 200 m[f], is_touched = _filecommit(
192 201 repo, fctx, m1, m2, linkrev, tr, writefilecopymeta, ms
193 202 )
194 203 if is_touched:
195 204 if is_touched == 'added':
196 205 files.mark_added(f)
197 206 elif is_touched == 'merged':
198 207 files.mark_merged(f)
199 208 else:
200 209 files.mark_touched(f)
201 210 m.setflag(f, fctx.flags())
202 211 except OSError:
203 212 repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
204 213 raise
205 214 except IOError as inst:
206 215 errcode = getattr(inst, 'errno', errno.ENOENT)
207 216 if error or errcode and errcode != errno.ENOENT:
208 217 repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
209 218 raise
210 219
211 220 # update manifest
212 221 removed = [f for f in removed if f in m1 or f in m2]
213 222 drop = sorted([f for f in removed if f in m])
214 223 for f in drop:
215 224 del m[f]
216 225 if p2.rev() == nullrev:
217 226 files.update_removed(removed)
218 227 else:
219 228 rf = metadata.get_removal_filter(ctx, (p1, p2, m1, m2))
220 229 for f in removed:
221 230 if not rf(f):
222 231 files.mark_removed(f)
223 232
224 233 mn = _commit_manifest(tr, linkrev, ctx, mctx, m, files.touched, added, drop)
225 234
226 235 return mn, files
227 236
228 237
229 238 def _filecommit(
230 239 repo, fctx, manifest1, manifest2, linkrev, tr, includecopymeta, ms,
231 240 ):
232 241 """
233 242 commit an individual file as part of a larger transaction
234 243
235 244 input:
236 245
237 246 fctx: a file context with the content we are trying to commit
238 247 manifest1: manifest of changeset first parent
239 248 manifest2: manifest of changeset second parent
240 249 linkrev: revision number of the changeset being created
241 250 tr: current transation
242 251 includecopymeta: boolean, set to False to skip storing the copy data
243 252 (only used by the Google specific feature of using
244 253 changeset extra as copy source of truth).
245 254 ms: mergestate object
246 255
247 256 output: (filenode, touched)
248 257
249 258 filenode: the filenode that should be used by this changeset
250 259 touched: one of: None (mean untouched), 'added' or 'modified'
251 260 """
252 261
253 262 fname = fctx.path()
254 263 fparent1 = manifest1.get(fname, nullid)
255 264 fparent2 = manifest2.get(fname, nullid)
256 265 touched = None
257 266 if fparent1 == fparent2 == nullid:
258 267 touched = 'added'
259 268
260 269 if isinstance(fctx, context.filectx):
261 270 # This block fast path most comparisons which are usually done. It
262 271 # assumes that bare filectx is used and no merge happened, hence no
263 272 # need to create a new file revision in this case.
264 273 node = fctx.filenode()
265 274 if node in [fparent1, fparent2]:
266 275 repo.ui.debug(b'reusing %s filelog entry\n' % fname)
267 276 if (
268 277 fparent1 != nullid and manifest1.flags(fname) != fctx.flags()
269 278 ) or (
270 279 fparent2 != nullid and manifest2.flags(fname) != fctx.flags()
271 280 ):
272 281 touched = 'modified'
273 282 return node, touched
274 283
275 284 flog = repo.file(fname)
276 285 meta = {}
277 286 cfname = fctx.copysource()
278 287 fnode = None
279 288
280 289 if cfname and cfname != fname:
281 290 # Mark the new revision of this file as a copy of another
282 291 # file. This copy data will effectively act as a parent
283 292 # of this new revision. If this is a merge, the first
284 293 # parent will be the nullid (meaning "look up the copy data")
285 294 # and the second one will be the other parent. For example:
286 295 #
287 296 # 0 --- 1 --- 3 rev1 changes file foo
288 297 # \ / rev2 renames foo to bar and changes it
289 298 # \- 2 -/ rev3 should have bar with all changes and
290 299 # should record that bar descends from
291 300 # bar in rev2 and foo in rev1
292 301 #
293 302 # this allows this merge to succeed:
294 303 #
295 304 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
296 305 # \ / merging rev3 and rev4 should use bar@rev2
297 306 # \- 2 --- 4 as the merge base
298 307 #
299 308
300 309 cnode = manifest1.get(cfname)
301 310 newfparent = fparent2
302 311
303 312 if manifest2: # branch merge
304 313 if fparent2 == nullid or cnode is None: # copied on remote side
305 314 if cfname in manifest2:
306 315 cnode = manifest2[cfname]
307 316 newfparent = fparent1
308 317
309 318 # Here, we used to search backwards through history to try to find
310 319 # where the file copy came from if the source of a copy was not in
311 320 # the parent directory. However, this doesn't actually make sense to
312 321 # do (what does a copy from something not in your working copy even
313 322 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
314 323 # the user that copy information was dropped, so if they didn't
315 324 # expect this outcome it can be fixed, but this is the correct
316 325 # behavior in this circumstance.
317 326
318 327 if cnode:
319 328 repo.ui.debug(b" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
320 329 if includecopymeta:
321 330 meta[b"copy"] = cfname
322 331 meta[b"copyrev"] = hex(cnode)
323 332 fparent1, fparent2 = nullid, newfparent
324 333 else:
325 334 repo.ui.warn(
326 335 _(
327 336 b"warning: can't find ancestor for '%s' "
328 337 b"copied from '%s'!\n"
329 338 )
330 339 % (fname, cfname)
331 340 )
332 341
333 342 elif fparent1 == nullid:
334 343 fparent1, fparent2 = fparent2, nullid
335 344 elif fparent2 != nullid:
336 345 # is one parent an ancestor of the other?
337 346 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
338 347 if fparent1 in fparentancestors:
339 348 fparent1, fparent2 = fparent2, nullid
340 349 elif fparent2 in fparentancestors:
341 350 fparent2 = nullid
342 351 elif not fparentancestors:
343 352 # TODO: this whole if-else might be simplified much more
344 353 if (
345 354 ms.active()
346 355 and ms.extras(fname).get(b'filenode-source') == b'other'
347 356 ):
348 357 fparent1, fparent2 = fparent2, nullid
349 358
350 359 force_new_node = False
351 360 # The file might have been deleted by merge code and user explicitly choose
352 361 # to revert the file and keep it. The other case can be where there is
353 362 # change-delete or delete-change conflict and user explicitly choose to keep
354 363 # the file. The goal is to create a new filenode for users explicit choices
355 364 if (
356 365 repo.ui.configbool(b'experimental', b'merge-track-salvaged')
357 366 and ms.active()
358 367 and ms.extras(fname).get(b'merge-removal-candidate') == b'yes'
359 368 ):
360 369 force_new_node = True
361 370 # is the file changed?
362 371 text = fctx.data()
363 372 if fparent2 != nullid or meta or flog.cmp(fparent1, text) or force_new_node:
364 373 if touched is None: # do not overwrite added
365 374 if fparent2 == nullid:
366 375 touched = 'modified'
367 376 else:
368 377 touched = 'merged'
369 378 fnode = flog.add(text, meta, tr, linkrev, fparent1, fparent2)
370 379 # are just the flags changed during merge?
371 380 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
372 381 touched = 'modified'
373 382 fnode = fparent1
374 383 else:
375 384 fnode = fparent1
376 385 return fnode, touched
377 386
378 387
379 388 def _commit_manifest(tr, linkrev, ctx, mctx, manifest, files, added, drop):
380 389 """make a new manifest entry (or reuse a new one)
381 390
382 391 given an initialised manifest context and precomputed list of
383 392 - files: files affected by the commit
384 393 - added: new entries in the manifest
385 394 - drop: entries present in parents but absent of this one
386 395
387 396 Create a new manifest revision, reuse existing ones if possible.
388 397
389 398 Return the nodeid of the manifest revision.
390 399 """
391 400 repo = ctx.repo()
392 401
393 402 md = None
394 403
395 404 # all this is cached, so it is find to get them all from the ctx.
396 405 p1 = ctx.p1()
397 406 p2 = ctx.p2()
398 407 m1ctx = p1.manifestctx()
399 408
400 409 m1 = m1ctx.read()
401 410
402 411 if not files:
403 412 # if no "files" actually changed in terms of the changelog,
404 413 # try hard to detect unmodified manifest entry so that the
405 414 # exact same commit can be reproduced later on convert.
406 415 md = m1.diff(manifest, scmutil.matchfiles(repo, ctx.files()))
407 416 if not files and md:
408 417 repo.ui.debug(
409 418 b'not reusing manifest (no file change in '
410 419 b'changelog, but manifest differs)\n'
411 420 )
412 421 if files or md:
413 422 repo.ui.note(_(b"committing manifest\n"))
414 423 # we're using narrowmatch here since it's already applied at
415 424 # other stages (such as dirstate.walk), so we're already
416 425 # ignoring things outside of narrowspec in most cases. The
417 426 # one case where we might have files outside the narrowspec
418 427 # at this point is merges, and we already error out in the
419 428 # case where the merge has files outside of the narrowspec,
420 429 # so this is safe.
421 430 mn = mctx.write(
422 431 tr,
423 432 linkrev,
424 433 p1.manifestnode(),
425 434 p2.manifestnode(),
426 435 added,
427 436 drop,
428 437 match=repo.narrowmatch(),
429 438 )
430 439 else:
431 440 repo.ui.debug(
432 441 b'reusing manifest from p1 (listed files ' b'actually unchanged)\n'
433 442 )
434 443 mn = p1.manifestnode()
435 444
436 445 return mn
437 446
438 447
439 448 def _extra_with_copies(repo, extra, files):
440 449 """encode copy information into a `extra` dictionnary"""
441 450 p1copies = files.copied_from_p1
442 451 p2copies = files.copied_from_p2
443 452 filesadded = files.added
444 453 filesremoved = files.removed
445 454 files = sorted(files.touched)
446 455 if not _write_copy_meta(repo)[1]:
447 456 # If writing only to changeset extras, use None to indicate that
448 457 # no entry should be written. If writing to both, write an empty
449 458 # entry to prevent the reader from falling back to reading
450 459 # filelogs.
451 460 p1copies = p1copies or None
452 461 p2copies = p2copies or None
453 462 filesadded = filesadded or None
454 463 filesremoved = filesremoved or None
455 464
456 465 extrasentries = p1copies, p2copies, filesadded, filesremoved
457 466 if extra is None and any(x is not None for x in extrasentries):
458 467 extra = {}
459 468 if p1copies is not None:
460 469 p1copies = metadata.encodecopies(files, p1copies)
461 470 extra[b'p1copies'] = p1copies
462 471 if p2copies is not None:
463 472 p2copies = metadata.encodecopies(files, p2copies)
464 473 extra[b'p2copies'] = p2copies
465 474 if filesadded is not None:
466 475 filesadded = metadata.encodefileindices(files, filesadded)
467 476 extra[b'filesadded'] = filesadded
468 477 if filesremoved is not None:
469 478 filesremoved = metadata.encodefileindices(files, filesremoved)
470 479 extra[b'filesremoved'] = filesremoved
471 480 return extra
General Comments 0
You need to be logged in to leave comments. Login now