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