rust/hg-cpython/src/copy_tracing.rs
author Martin von Zweigbergk <martinvonz@google.com>
Tue, 08 Dec 2020 16:45:13 -0800
changeset 46108 bdc2bf68f19e
parent 46057 e0313b0a6f7e
child 46149 294d5aca4ff5
permissions -rw-r--r--
mergetools: add new conflict marker format with diffs in I use 3-way conflict markers. Often when I resolve them, I manually compare one the base with one side and apply the differences to the other side. That can be hard when the conflict marker is large. This patch introduces a new type of conflict marker, which I'm hoping will make it easier to resolve conflicts. The new format uses `<<<<<<<` and `>>>>>>>` to open and close the markers, just like our existing 2-way and 3-way conflict markers. Instead of having 2 or 3 snapshots (left+right or left+base+right), it has a sequence of diffs. A diff looks like this: ``` ------- base +++++++ left a -b +c d ``` A diff that adds one side ("diff from nothing") has a `=======` header instead and does not have have `+` prefixed on its lines. A regular 3-way merge can be viewed as adding one side plus a diff between the base and the other side. It thus has two ways of being represented, depending on which side is being diffed: ``` <<<<<<< ======= left contents on left ------- base +++++++ right contents on -left +right >>>>>>> ``` or ``` <<<<<<< ------- base +++++++ left contents on -right +left ======= right contents on right >>>>>>> ``` I've made it so the new merge tool tries to pick a version that has the most common lines (no difference in the example above). I've called the new tool "mergediff" to stick to the convention of starting with "merge" if the tool tries a regular 3-way merge. The idea came from my pet VCS (placeholder name `jj`), which has support for octopus merges and other ways of ending up with merges of more than 3 versions. I wanted to be able to represent such conflicts in the working copy and therefore thought of this format (although I have not yet implemented it in my VCS). I then attended a meeting with Larry McVoy, who said BitKeeper has an option (`bk smerge -g`) for showing a similar format, which reminded me to actually attempt this in Mercurial. Differential Revision: https://phab.mercurial-scm.org/D9551

use cpython::ObjectProtocol;
use cpython::PyBool;
use cpython::PyBytes;
use cpython::PyDict;
use cpython::PyList;
use cpython::PyModule;
use cpython::PyObject;
use cpython::PyResult;
use cpython::PyTuple;
use cpython::Python;

use hg::copy_tracing::combine_changeset_copies;
use hg::copy_tracing::ChangedFiles;
use hg::copy_tracing::DataHolder;
use hg::copy_tracing::RevInfo;
use hg::copy_tracing::RevInfoMaker;
use hg::Revision;

/// Combines copies information contained into revision `revs` to build a copy
/// map.
///
/// See mercurial/copies.py for details
pub fn combine_changeset_copies_wrapper(
    py: Python,
    revs: PyList,
    children: PyDict,
    target_rev: Revision,
    rev_info: PyObject,
    is_ancestor: PyObject,
) -> PyResult<PyDict> {
    let revs: PyResult<_> =
        revs.iter(py).map(|r| Ok(r.extract(py)?)).collect();

    // Wrap the `is_ancestor` python callback as a Rust closure
    //
    // No errors are expected from the Python side, and they will should only
    // happens in case of programing error or severe data corruption. Such
    // errors will raise panic and the rust-cpython harness will turn them into
    // Python exception.
    let is_ancestor_wrap = |anc: Revision, desc: Revision| -> bool {
        is_ancestor
            .call(py, (anc, desc), None)
            .expect(
                "rust-copy-tracing: python call  to `is_ancestor` \
                failed",
            )
            .cast_into::<PyBool>(py)
            .expect(
                "rust-copy-tracing: python call  to `is_ancestor` \
                returned unexpected non-Bool value",
            )
            .is_true()
    };

    // Wrap the `rev_info_maker` python callback as a Rust closure
    //
    // No errors are expected from the Python side, and they will should only
    // happens in case of programing error or severe data corruption. Such
    // errors will raise panic and the rust-cpython harness will turn them into
    // Python exception.
    let rev_info_maker: RevInfoMaker<PyBytes> =
        Box::new(|rev: Revision, d: &mut DataHolder<PyBytes>| -> RevInfo {
            let res: PyTuple = rev_info
                .call(py, (rev,), None)
                .expect("rust-copy-tracing: python call to `rev_info` failed")
                .cast_into(py)
                .expect(
                    "rust-copy_tracing: python call to `rev_info` returned \
                    unexpected non-Tuple value",
                );
            let p1 = res.get_item(py, 0).extract(py).expect(
                "rust-copy-tracing: rev_info return is invalid, first item \
                is a not a revision",
            );
            let p2 = res.get_item(py, 1).extract(py).expect(
                "rust-copy-tracing: rev_info return is invalid, first item \
                is a not a revision",
            );

            let files = match res.get_item(py, 2).extract::<PyBytes>(py) {
                Ok(raw) => {
                    // Give responsability for the raw bytes lifetime to
                    // hg-core
                    d.data = Some(raw);
                    let addrs = d.data.as_ref().expect(
                        "rust-copy-tracing: failed to get a reference to the \
                        raw bytes for copy data").data(py);
                    ChangedFiles::new(addrs)
                }
                // value was presumably None, meaning they was no copy data.
                Err(_) => ChangedFiles::new_empty(),
            };

            (p1, p2, files)
        });
    let children: PyResult<_> = children
        .items(py)
        .iter()
        .map(|(k, v)| {
            let v: &PyList = v.cast_as(py)?;
            let v: PyResult<_> =
                v.iter(py).map(|child| Ok(child.extract(py)?)).collect();
            Ok((k.extract(py)?, v?))
        })
        .collect();

    let res = combine_changeset_copies(
        revs?,
        children?,
        target_rev,
        rev_info_maker,
        &is_ancestor_wrap,
    );
    let out = PyDict::new(py);
    for (dest, source) in res.into_iter() {
        out.set_item(
            py,
            PyBytes::new(py, &dest.into_vec()),
            PyBytes::new(py, &source.into_vec()),
        )?;
    }
    Ok(out)
}

/// Create the module, with `__package__` given from parent
pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
    let dotted_name = &format!("{}.copy_tracing", package);
    let m = PyModule::new(py, dotted_name)?;

    m.add(py, "__package__", package)?;
    m.add(py, "__doc__", "Copy tracing - Rust implementation")?;

    m.add(
        py,
        "combine_changeset_copies",
        py_fn!(
            py,
            combine_changeset_copies_wrapper(
                revs: PyList,
                children: PyDict,
                target_rev: Revision,
                rev_info: PyObject,
                is_ancestor: PyObject
            )
        ),
    )?;

    let sys = PyModule::import(py, "sys")?;
    let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
    sys_modules.set_item(py, dotted_name, &m)?;

    Ok(m)
}