diff mercurial/commit.py @ 45204:ce9ee81df9ff

commitctx: extract _filecommit too This function is exclusively used in `commitctx`. So we should extract it too for consistency and to reduce the `localrepo` bloat. This is part of a larger refactoring/cleanup of the commitctx code to clarify and augment the logic gathering metadata useful for copy tracing. The current code is a tad too long and entangled to make such update easy. Differential Revision: https://phab.mercurial-scm.org/D8710
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 07 Jul 2020 00:18:15 +0200
parents ae5c1a3bc339
children d056a131c93f
line wrap: on
line diff
--- a/mercurial/commit.py	Mon Jul 06 23:14:52 2020 +0200
+++ b/mercurial/commit.py	Tue Jul 07 00:18:15 2020 +0200
@@ -11,10 +11,13 @@
 from .i18n import _
 from .node import (
     hex,
+    nullid,
     nullrev,
 )
 
 from . import (
+    context,
+    mergestate,
     metadata,
     phases,
     scmutil,
@@ -98,8 +101,8 @@
                         removed.append(f)
                     else:
                         added.append(f)
-                        m[f], is_touched = repo._filecommit(
-                            fctx, m1, m2, linkrev, trp, writefilecopymeta,
+                        m[f], is_touched = _filecommit(
+                            repo, fctx, m1, m2, linkrev, trp, writefilecopymeta,
                         )
                         if is_touched:
                             touched.append(f)
@@ -213,3 +216,139 @@
             # if minimal phase was 0 we don't need to retract anything
             phases.registernew(repo, tr, targetphase, [n])
         return n
+
+
+def _filecommit(
+    repo, fctx, manifest1, manifest2, linkrev, tr, includecopymeta,
+):
+    """
+    commit an individual file as part of a larger transaction
+
+    input:
+
+        fctx:       a file context with the content we are trying to commit
+        manifest1:  manifest of changeset first parent
+        manifest2:  manifest of changeset second parent
+        linkrev:    revision number of the changeset being created
+        tr:         current transation
+        individual: boolean, set to False to skip storing the copy data
+                    (only used by the Google specific feature of using
+                    changeset extra as copy source of truth).
+
+    output: (filenode, touched)
+
+        filenode: the filenode that should be used by this changeset
+        touched:  one of: None, 'added' or 'modified'
+    """
+
+    fname = fctx.path()
+    fparent1 = manifest1.get(fname, nullid)
+    fparent2 = manifest2.get(fname, nullid)
+    touched = None
+    if fparent1 == fparent2 == nullid:
+        touched = 'added'
+
+    if isinstance(fctx, context.filectx):
+        # This block fast path most comparisons which are usually done. It
+        # assumes that bare filectx is used and no merge happened, hence no
+        # need to create a new file revision in this case.
+        node = fctx.filenode()
+        if node in [fparent1, fparent2]:
+            repo.ui.debug(b'reusing %s filelog entry\n' % fname)
+            if (
+                fparent1 != nullid and manifest1.flags(fname) != fctx.flags()
+            ) or (
+                fparent2 != nullid and manifest2.flags(fname) != fctx.flags()
+            ):
+                touched = 'modified'
+            return node, touched
+
+    flog = repo.file(fname)
+    meta = {}
+    cfname = fctx.copysource()
+    fnode = None
+
+    if cfname and cfname != fname:
+        # Mark the new revision of this file as a copy of another
+        # file.  This copy data will effectively act as a parent
+        # of this new revision.  If this is a merge, the first
+        # parent will be the nullid (meaning "look up the copy data")
+        # and the second one will be the other parent.  For example:
+        #
+        # 0 --- 1 --- 3   rev1 changes file foo
+        #   \       /     rev2 renames foo to bar and changes it
+        #    \- 2 -/      rev3 should have bar with all changes and
+        #                      should record that bar descends from
+        #                      bar in rev2 and foo in rev1
+        #
+        # this allows this merge to succeed:
+        #
+        # 0 --- 1 --- 3   rev4 reverts the content change from rev2
+        #   \       /     merging rev3 and rev4 should use bar@rev2
+        #    \- 2 --- 4        as the merge base
+        #
+
+        cnode = manifest1.get(cfname)
+        newfparent = fparent2
+
+        if manifest2:  # branch merge
+            if fparent2 == nullid or cnode is None:  # copied on remote side
+                if cfname in manifest2:
+                    cnode = manifest2[cfname]
+                    newfparent = fparent1
+
+        # Here, we used to search backwards through history to try to find
+        # where the file copy came from if the source of a copy was not in
+        # the parent directory. However, this doesn't actually make sense to
+        # do (what does a copy from something not in your working copy even
+        # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
+        # the user that copy information was dropped, so if they didn't
+        # expect this outcome it can be fixed, but this is the correct
+        # behavior in this circumstance.
+
+        if cnode:
+            repo.ui.debug(b" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
+            if includecopymeta:
+                meta[b"copy"] = cfname
+                meta[b"copyrev"] = hex(cnode)
+            fparent1, fparent2 = nullid, newfparent
+        else:
+            repo.ui.warn(
+                _(
+                    b"warning: can't find ancestor for '%s' "
+                    b"copied from '%s'!\n"
+                )
+                % (fname, cfname)
+            )
+
+    elif fparent1 == nullid:
+        fparent1, fparent2 = fparent2, nullid
+    elif fparent2 != nullid:
+        # is one parent an ancestor of the other?
+        fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
+        if fparent1 in fparentancestors:
+            fparent1, fparent2 = fparent2, nullid
+        elif fparent2 in fparentancestors:
+            fparent2 = nullid
+        elif not fparentancestors:
+            # TODO: this whole if-else might be simplified much more
+            ms = mergestate.mergestate.read(repo)
+            if (
+                fname in ms
+                and ms[fname] == mergestate.MERGE_RECORD_MERGED_OTHER
+            ):
+                fparent1, fparent2 = fparent2, nullid
+
+    # is the file changed?
+    text = fctx.data()
+    if fparent2 != nullid or meta or flog.cmp(fparent1, text):
+        if touched is None:  # do not overwrite added
+            touched = 'modified'
+        fnode = flog.add(text, meta, tr, linkrev, fparent1, fparent2)
+    # are just the flags changed during merge?
+    elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
+        touched = 'modified'
+        fnode = fparent1
+    else:
+        fnode = fparent1
+    return fnode, touched