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