{
    "file_diffs": [
        {
            "old_file_content": "",
            "new_file_content": "### 🐛 Bug Fixes\n\n- BF: GitRepo.merge do not allow merging unrelated unconditionally.  [PR #7312](https://github.com/datalad/datalad/pull/7312) (by [@yarikoptic](https://github.com/yarikoptic))\n",
            "header": {
                "file": {
                    "path": "changelog.d/pr-7312.md"
                },
                "misc_line": "new file mode 100644"
            },
            "index_line": {
                "old_commit_hash": "000000000",
                "new_commit_hash": "2187e5325",
                "mode": ""
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "/dev/null"
            },
            "plus_file": {
                "path": "b/changelog.d/pr-7312.md"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 0,
                            "length": 0
                        },
                        "new_range": {
                            "start": 1,
                            "length": 3
                        },
                        "section": ""
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "### 🐛 Bug Fixes",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "- BF: GitRepo.merge do not allow merging unrelated unconditionally.  [PR #7312](https://github.com/datalad/datalad/pull/7312) (by [@yarikoptic](https://github.com/yarikoptic))",
                                "type": "added"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# ex: set sts=4 ts=4 sw=4 et:\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n#\n#   See COPYING file distributed along with the datalad package for the\n#   copyright and license terms.\n#\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n\"\"\"Test update action\n\n\"\"\"\n\nimport os\nimport os.path as op\nfrom os.path import exists\nfrom os.path import join as opj\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom datalad import cfg as dl_cfg\nfrom datalad.api import (\n    clone,\n    install,\n    remove,\n    update,\n)\nfrom datalad.distribution.update import _process_how_args\nfrom datalad.support.annexrepo import AnnexRepo\nfrom datalad.support.external_versions import external_versions\nfrom datalad.support.gitrepo import GitRepo\nfrom datalad.tests.utils_pytest import (\n    DEFAULT_BRANCH,\n    DEFAULT_REMOTE,\n    SkipTest,\n    assert_false,\n    assert_in,\n    assert_in_results,\n    assert_is_instance,\n    assert_not_in,\n    assert_raises,\n    assert_repo_status,\n    assert_result_count,\n    assert_status,\n    create_tree,\n    eq_,\n    known_failure_windows,\n    maybe_adjust_repo,\n    neq_,\n    ok_,\n    ok_file_has_content,\n    skip_if_adjusted_branch,\n    slow,\n    with_tempfile,\n)\nfrom datalad.utils import (\n    Path,\n    chpwd,\n    knows_annex,\n    rmtree,\n)\n\nfrom ..dataset import Dataset\n\n\n@slow\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_update_simple(origin=None, src_path=None, dst_path=None):\n    ca = dict(result_renderer='disabled')\n    # a remote dataset with a subdataset underneath\n    origds = Dataset(origin).create(**ca)\n    # naming is weird, but a legacy artifact\n    _ = origds.create('subm 1', **ca)\n    _ = origds.create('2', **ca)\n\n    # prepare src\n    source = install(src_path, source=origin, recursive=True)\n    # forget we cloned it by removing remote, which should lead to\n    # setting tracking branch to target:\n    source.repo.remove_remote(DEFAULT_REMOTE)\n    # also forget the declared absolute location of the submodules, and turn them\n    # relative to this/a clone\n    for sub in source.subdatasets(result_xfm=lambda x: x['gitmodule_name']):\n        source.subdatasets(path=sub, set_property=[('url', './{}'.format(sub))])\n\n    # dataset without sibling will not need updates\n    assert_status('notneeded', source.update())\n    # deprecation message doesn't ruin things\n    assert_status('notneeded', source.update(fetch_all=True))\n    # but error if unknown sibling is given\n    assert_status('impossible', source.update(sibling='funky', on_failure='ignore'))\n\n    # get a clone to update later on:\n    dest = install(dst_path, source=src_path, recursive=True)\n    # test setup done;\n    # assert all fine\n    assert_repo_status(dst_path)\n    assert_repo_status(src_path)\n\n    # update yields nothing => up-to-date\n    assert_status('ok', dest.update())\n    assert_repo_status(dst_path)\n\n    # modify remote:\n    with open(opj(src_path, \"update.txt\"), \"w\") as f:\n        f.write(\"Additional content\")\n    source.save(path=\"update.txt\", message=\"Added update.txt\")\n    assert_repo_status(src_path)\n\n    # update without `merge` only fetches:\n    assert_status('ok', dest.update())\n    # modification is not known to active branch:\n    assert_not_in(\"update.txt\",\n                  dest.repo.get_files(dest.repo.get_active_branch()))\n    # modification is known to branch <default remote>/<default branch>\n    assert_in(\"update.txt\",\n              dest.repo.get_files(DEFAULT_REMOTE + \"/\" + DEFAULT_BRANCH))\n\n    # merge:\n    assert_status('ok', dest.update(merge=True))\n    # modification is now known to active branch:\n    assert_in(\"update.txt\",\n              dest.repo.get_files(dest.repo.get_active_branch()))\n    # it's known to annex, but has no content yet:\n    annexprops = dest.repo.get_file_annexinfo(\"update.txt\",\n                                              eval_availability=True)\n    annexprops['key']  # blows if unknown\n    eq_(False, annexprops['has_content'])\n\n    # check subdataset path constraints, baseline (parent + 2 subds)\n    assert_result_count(dest.update(recursive=True),\n                        3, status='ok', type='dataset')\n    # no recursion and invalid path still updates the parent\n    res = dest.update(path='whatever')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # invalid path with recursion also does\n    res = dest.update(recursive=True, path='whatever')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # valid path and no recursion only updates the parent\n    res = dest.update(path='subm 1')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # valid path and recursion updates matching\n    res = dest.update(recursive=True, path='subm 1')\n    assert_result_count(res, 2, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    assert_result_count(res, 1, status='ok', path=str(dest.pathobj / 'subm 1'))\n    # additional invalid path doesn't hurt\n    res = dest.update(recursive=True, path=['subm 1', 'mike'])\n    assert_result_count(res, 2, status='ok', type='dataset')\n    # full match\n    res = dest.update(recursive=True, path=['subm 1', '2'])\n    assert_result_count(res, 3, status='ok', type='dataset')\n\n    # test that update doesn't crash if we specify only a single path (submod) to\n    # operate on\n    with chpwd(dest.path):\n        # in 0.11.x it would be a single result since \"pwd\" dataset is not\n        # considered, and would be relative path (as specified).\n        # In 0.12.0 - it would include implicit pwd dataset, and paths would be absolute\n        res_update = update(path=['subm 1'], recursive=True)\n        assert_result_count(res_update, 2)\n        for p in dest.path, str(dest.pathobj / 'subm 1'):\n            assert_in_results(res_update, path=p, action='update', status='ok', type='dataset')\n\n        # and with merge we would also try to save (but there would be no changes)\n        res_merge = update(path=['subm 1'], recursive=True, merge=True)\n        assert_result_count(res_merge, 2, action='update')\n        # 2 of \"updates\" really.\n        assert_in_results(res_merge, action='update', status='ok', type='dataset')\n        assert_in_results(res_merge, action='save', status='notneeded', type='dataset')\n\n    # smoke-test if recursive update doesn't fail if submodule is removed\n    # and that we can run it from within a dataset without providing it\n    # explicitly\n    assert_result_count(\n        dest.remove('subm 1'), 1,\n        status='ok', action='remove', path=opj(dest.path, 'subm 1'))\n    with chpwd(dest.path):\n        assert_result_count(\n            update(recursive=True), 2,\n            status='ok', type='dataset')\n    assert_result_count(\n        dest.update(merge=True, recursive=True), 2,\n        action='update', status='ok', type='dataset')\n\n    # and now test recursive update with merging in differences\n    create_tree(opj(source.path, '2'), {'load.dat': 'heavy'})\n    source.save(opj('2', 'load.dat'),\n                message=\"saving changes within subm2\",\n                recursive=True)\n    assert_result_count(\n        dest.update(merge=True, recursive=True), 2,\n        action='update', status='ok', type='dataset')\n    # and now we can get new file\n    dest.get(opj('2', 'load.dat'))\n    ok_file_has_content(opj(dest.path, '2', 'load.dat'), 'heavy')\n\n\n@with_tempfile\n@with_tempfile\ndef test_update_git_smoke(src_path=None, dst_path=None):\n    # Apparently was just failing on git repos for basic lack of coverage, hence this quick test\n    ds = Dataset(src_path).create(annex=False)\n    target = install(\n        dst_path, source=src_path,\n        result_xfm='datasets', return_type='item-or-list')\n    create_tree(ds.path, {'file.dat': '123'})\n    ds.save('file.dat')\n    assert_result_count(\n        target.update(recursive=True, merge=True), 1,\n        action='update', status='ok', type='dataset')\n    ok_file_has_content(opj(target.path, 'file.dat'), '123')\n\n\n@slow  # ~9s\n@with_tempfile(mkdir=True)\ndef test_update_fetch_all(path=None):\n    path = Path(path)\n    remote_1 = str(path / \"remote_1\")\n    remote_2 = str(path / \"remote_2\")\n\n    ds = Dataset(path / \"src\").create()\n    src = ds.repo.path\n\n    ds_rmt1 = clone(source=src, path=remote_1)\n    ds_rmt2 = clone(source=src, path=remote_2)\n\n    ds.siblings('add', name=\"sibling_1\", url=remote_1)\n    ds.siblings('add', name=\"sibling_2\", url=remote_2)\n\n    # modify the remotes:\n    (ds_rmt1.pathobj / \"first.txt\").write_text(\"some file load\")\n    ds_rmt1.save()\n\n    # TODO: Modify an already present file!\n\n    (ds_rmt2.pathobj / \"second.txt\").write_text(\"different file load\")\n    ds_rmt2.save()\n\n    # Let's init some special remote which we couldn't really update/fetch\n    if not dl_cfg.get('datalad.tests.dataladremote'):\n        ds.repo.init_remote(\n            'datalad',\n            ['encryption=none', 'type=external', 'externaltype=datalad'])\n    # fetch all remotes\n    assert_result_count(\n        ds.update(), 1, status='ok', type='dataset')\n\n    # no merge, so changes are not in active branch:\n    assert_not_in(\"first.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    assert_not_in(\"second.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    # but we know the changes in remote branches:\n    assert_in(\"first.txt\", ds.repo.get_files(\"sibling_1/\" + DEFAULT_BRANCH))\n    assert_in(\"second.txt\", ds.repo.get_files(\"sibling_2/\" + DEFAULT_BRANCH))\n\n    # no merge strategy for multiple remotes yet:\n    # more clever now, there is a tracking branch that provides a remote\n    #assert_raises(NotImplementedError, ds.update, merge=True)\n\n    # merge a certain remote:\n    assert_result_count(\n        ds.update(sibling='sibling_1', merge=True),\n        1, action='update', status='ok', type='dataset')\n\n    # changes from sibling_2 still not present:\n    assert_not_in(\"second.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    # changes from sibling_1 merged:\n    assert_in(\"first.txt\",\n              ds.repo.get_files(ds.repo.get_active_branch()))\n    # it's known to annex, but has no content yet:\n    annexprops = ds.repo.get_file_annexinfo(\n        \"first.txt\", eval_availability=True)\n    annexprops['key']  # blows if unknown\n    eq_(False, annexprops['has_content'])\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_newthings_coming_down(originpath=None, destpath=None):\n    origin = GitRepo(originpath, create=True)\n    create_tree(originpath, {'load.dat': 'heavy'})\n    Dataset(originpath).save('load.dat')\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    assert_is_instance(ds.repo, GitRepo)\n    assert_in(DEFAULT_REMOTE, ds.repo.get_remotes())\n    # turn origin into an annex\n    origin = AnnexRepo(originpath, create=True)\n    # clone doesn't know yet\n    assert_false(knows_annex(ds.path))\n    # but after an update it should\n    # no merge, only one sibling, no parameters should be specific enough\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    assert(knows_annex(ds.path))\n    # no branches appeared\n    eq_(ds.repo.get_branches(), [DEFAULT_BRANCH])\n    # now merge, and get an annex\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    assert_in('git-annex', ds.repo.get_branches())\n    assert_is_instance(ds.repo, AnnexRepo)\n    # should be fully functional\n    testfname = opj(ds.path, 'load.dat')\n    assert_false(ds.repo.file_has_content(testfname))\n    ds.get('.')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    # check that a new tag comes down\n    origin.tag('first!')\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(ds.repo.get_tags(output='name')[0], 'first!')\n\n    # and now we destroy the remote annex\n    origin.call_git(['config', '--remove-section', 'annex'])\n    rmtree(opj(origin.path, '.git', 'annex'), chmod_files=True)\n    origin.call_git(['branch', '-D', 'git-annex'])\n    origin = GitRepo(originpath)\n    assert_false(knows_annex(originpath))\n\n    # and update the local clone\n    # for now this should simply not fail (see gh-793), later might be enhanced to a\n    # graceful downgrade\n    before_branches = ds.repo.get_branches()\n    ok_(any(\"git-annex\" in b\n            for b in ds.repo.get_remote_branches()))\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(before_branches, ds.repo.get_branches())\n    # annex branch got pruned\n    assert_false(any(\"git-annex\" in b\n                     for b in ds.repo.get_remote_branches()))\n    # check that a new tag comes down even if repo types mismatch\n    origin.tag('second!')\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(ds.repo.get_tags(output='name')[-1], 'second!')\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_update_volatile_subds(originpath=None, otherpath=None, destpath=None):\n    origin = Dataset(originpath).create()\n    repo = origin.repo\n    if repo.is_managed_branch() and repo.git_annex_version <= \"8.20201129\":\n        # Fails before git-annex's fd161da2c (adjustTree: Consider submodule\n        # deletions, 2021-01-06).\n        raise SkipTest(\n            \"On adjusted branch, test requires fix in more recent git-annex\")\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    # as a submodule\n    sname = 'subm 1'\n    osm1 = origin.create(sname)\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    # nothing without a merge, no inappropriate magic\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # and we should be able to do update with recursive invocation\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        1, action='update', status='ok', type='dataset')\n    # known, and placeholder exists\n    assert_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    ok_(exists(opj(ds.path, sname)))\n\n    # remove from origin\n    origin.remove(sname, reckless='availability')\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # gone locally, wasn't checked out\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    assert_false(exists(opj(ds.path, sname)))\n\n    # re-introduce at origin\n    osm1 = origin.create(sname)\n    create_tree(osm1.path, {'load.dat': 'heavy'})\n    origin.save(opj(osm1.path, 'load.dat'))\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # grab new content of uninstall subdataset, right away\n    ds.get(opj(ds.path, sname, 'load.dat'))\n    ok_file_has_content(opj(ds.path, sname, 'load.dat'), 'heavy')\n\n    # modify ds and subds at origin\n    create_tree(origin.path, {'mike': 'this', sname: {'probe': 'little'}})\n    origin.save(recursive=True)\n    assert_repo_status(origin.path)\n\n    # updates for both datasets should come down the pipe\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        2, action='update', status='ok', type='dataset')\n    assert_repo_status(ds.path)\n\n    # now remove just-installed subdataset from origin again\n    origin.remove(sname, reckless='kill')\n    assert_not_in(sname, origin.subdatasets(result_xfm='relpaths'))\n    assert_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    # merge should disconnect the installed subdataset, but leave the actual\n    # ex-subdataset alone\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        1, action='update', type='dataset')\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    ok_file_has_content(opj(ds.path, sname, 'load.dat'), 'heavy')\n    ok_(Dataset(opj(ds.path, sname)).is_installed())\n\n    # now remove the now disconnected subdataset for further tests\n    remove(dataset=op.join(ds.path, sname), reckless='kill')\n    assert_repo_status(ds.path)\n\n    # new separate subdataset, not within the origin dataset\n    otherds = Dataset(otherpath).create()\n    # install separate dataset as a submodule\n    ds.install(source=otherds.path, path='other')\n    create_tree(otherds.path, {'brand': 'new'})\n    otherds.save()\n    assert_repo_status(otherds.path)\n    # pull in changes\n    res = ds.update(merge=True, recursive=True)\n    assert_result_count(\n        res, 2, status='ok', action='update', type='dataset')\n    # the next is to check for #2858\n    assert_repo_status(ds.path)\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_reobtain_data(originpath=None, destpath=None):\n    origin = Dataset(originpath).create()\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    # no harm\n    assert_result_count(ds.update(merge=True, reobtain_data=True),\n                        1, action=\"update\", status=\"ok\")\n    # content\n    create_tree(origin.path, {'load.dat': 'heavy'})\n    origin.save(opj(origin.path, 'load.dat'))\n    # update does not bring data automatically\n    assert_result_count(ds.update(merge=True, reobtain_data=True),\n                        1, action=\"update\", status=\"ok\")\n    assert_in('load.dat', ds.repo.get_annexed_files())\n    assert_false(ds.repo.file_has_content('load.dat'))\n    # now get data\n    ds.get('load.dat')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    # new content at origin\n    create_tree(origin.path, {'novel': 'but boring'})\n    origin.save()\n    # update must not bring in data for new file\n    result = ds.update(merge=True, reobtain_data=True)\n    assert_in_results(result, action='get', status='notneeded')\n\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    assert_in('novel', ds.repo.get_annexed_files())\n    assert_false(ds.repo.file_has_content('novel'))\n    # modify content at origin\n    os.remove(opj(origin.path, 'load.dat'))\n    create_tree(origin.path, {'load.dat': 'light'})\n    origin.save()\n    # update must update file with existing data, but leave empty one alone\n    res = ds.update(merge=True, reobtain_data=True)\n    assert_result_count(res, 1, status='ok', type='dataset', action='update')\n    assert_result_count(res, 1, status='ok', type='file', action='get')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'light')\n    assert_false(ds.repo.file_has_content('novel'))\n\n\n@with_tempfile(mkdir=True)\ndef test_multiway_merge(path=None):\n    # prepare ds with two siblings, but no tracking branch\n    ds = Dataset(op.join(path, 'ds_orig')).create()\n    r1 = AnnexRepo(path=op.join(path, 'ds_r1'), git_opts={'bare': True})\n    r2 = GitRepo(path=op.join(path, 'ds_r2'), git_opts={'bare': True})\n    ds.siblings(action='add', name='r1', url=r1.path)\n    ds.siblings(action='add', name='r2', url=r2.path)\n    assert_status('ok', ds.push(to='r1'))\n    # push unlike publish reports on r2 not being an annex remote with a\n    # 'notneeded'\n    assert_status(('ok', 'notneeded'), ds.push(to='r2'))\n    # just a fetch should be no issue\n    assert_status('ok', ds.update())\n    # ATM we do not support multi-way merges\n    assert_status('impossible', ds.update(merge=True, on_failure='ignore'))\n\n\n# `git annex sync REMOTE` rather than `git merge TARGET` is used on an\n# adjusted branch, so we don't give an error if TARGET can't be\n# determined.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_no_merge_target(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    assert_repo_status(ds_src.path)\n    ds_clone.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n    res = ds_clone.update(merge=True, on_failure=\"ignore\")\n    assert_in_results(res, status=\"impossible\", action=\"update\")\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge TARGET` being used.\n@skip_if_adjusted_branch\n@slow  # 17sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_conflict(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s0 = Dataset(path / \"clone\" / \"s0\")\n    ds_clone_s1 = Dataset(path / \"clone\" / \"s1\")\n\n    (ds_src.pathobj / \"foo\").write_text(\"src content\")\n    ds_src.save(to_git=True)\n\n    (ds_clone.pathobj / \"foo\").write_text(\"clone content\")\n    ds_clone.save(to_git=True)\n\n    # Top-level merge failure\n    res = ds_clone.update(merge=True, on_failure=\"ignore\")\n    assert_in_results(res, action=\"merge\", status=\"error\")\n    assert_in_results(res, action=\"update\", status=\"error\")\n    # Deal with the conflicts. Note that save() won't handle this gracefully\n    # because it will try to commit with a pathspec, which git doesn't allow\n    # during a merge.\n    ds_clone.repo.call_git([\"checkout\", \"--theirs\", \"--\", \"foo\"])\n    ds_clone.repo.call_git([\"add\", \"--\", \"foo\"])\n    ds_clone.repo.call_git([\"commit\", \"--no-edit\"])\n    assert_repo_status(ds_clone.path)\n\n    # Top-level and subdataset merge failure\n    (ds_src_s0.pathobj / \"foo\").write_text(\"src s0 content\")\n    (ds_src_s1.pathobj / \"foo\").write_text(\"no conflict\")\n    ds_src.save(recursive=True, to_git=True)\n\n    (ds_clone_s0.pathobj / \"foo\").write_text(\"clone s0 content\")\n    ds_clone.save(recursive=True, to_git=True)\n    res = ds_clone.update(merge=True, recursive=True, on_failure=\"ignore\")\n    assert_result_count(res, 2, action=\"merge\", status=\"error\")\n    assert_result_count(res, 2, action=\"update\", status=\"error\")\n    assert_in_results(res, action=\"merge\", status=\"ok\",\n                      path=ds_clone_s1.path)\n    assert_in_results(res, action=\"update\", status=\"ok\",\n                      path=ds_clone_s1.path)\n    # No saving happens if there's a top-level conflict.\n    assert_repo_status(ds_clone.path,\n                       modified=[ds_clone_s0.path, ds_clone_s1.path])\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge TARGET` being used.\n@skip_if_adjusted_branch\n@slow  # 13sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_conflict_in_subdataset_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_src_sub_conflict = ds_src.create(\"sub_conflict\")\n    ds_src_sub_noconflict = ds_src.create(\"sub_noconflict\")\n    ds_src.save()\n\n    # Set up a scenario where one subdataset has a conflict between the remote\n    # and local version, but the parent dataset does not have a conflict\n    # because it hasn't recorded the subdataset state.\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_sub_conflict = Dataset(path / \"clone\" / \"sub_conflict\")\n    ds_clone_sub_noconflict = Dataset(path / \"clone\" / \"sub_noconflict\")\n\n    (ds_src_sub_conflict.pathobj / \"foo\").write_text(\"src content\")\n    ds_src_sub_conflict.save(to_git=True)\n\n    (ds_clone_sub_conflict.pathobj / \"foo\").write_text(\"clone content\")\n    ds_clone_sub_conflict.save(to_git=True)\n\n    (ds_src_sub_noconflict.pathobj / \"foo\").write_text(\"src content\")\n    ds_src_sub_noconflict.save()\n\n    res = ds_clone.update(merge=True, recursive=True, on_failure=\"ignore\")\n    assert_in_results(res, action=\"merge\", status=\"error\",\n                      path=ds_clone_sub_conflict.path)\n    assert_in_results(res, action=\"merge\", status=\"ok\",\n                      path=ds_clone_sub_noconflict.path)\n    assert_in_results(res, action=\"save\", status=\"ok\",\n                      path=ds_clone.path)\n    # We saved the subdataset without a conflict...\n    assert_repo_status(ds_clone_sub_noconflict.path)\n    # ... but the one with the conflict leaves it for the caller to handle.\n    ok_(ds_clone_sub_conflict.repo.call_git(\n        [\"ls-files\", \"--unmerged\", \"--\", \"foo\"], read_only=True).strip())\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge --ff-only ...` being used.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_ff_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_clone_ff = install(source=ds_src.path, path=path / \"clone_ff\",\n                          result_xfm=\"datasets\")\n\n    ds_clone_nonff = install(source=ds_src.path, path=path / \"clone_nonff\",\n                             result_xfm=\"datasets\")\n\n    (ds_clone_nonff.pathobj / \"foo\").write_text(\"local change\")\n    ds_clone_nonff.save(recursive=True)\n\n    (ds_src.pathobj / \"bar\").write_text(\"remote change\")\n    ds_src.save(recursive=True)\n\n    assert_in_results(\n        ds_clone_ff.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"ok\")\n\n    # ff-only prevents a non-fast-forward ...\n    assert_in_results(\n        ds_clone_nonff.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"error\")\n    # ... that would work with \"any\".\n    assert_in_results(\n        ds_clone_nonff.update(merge=\"any\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"ok\")\n\n\n@slow  # 11sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_other_branch(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    on_adjusted = ds_src.repo.is_managed_branch()\n    ds_src_subds = ds_src.create(\"subds\")\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n\n    ds_src_subds.repo.call_git([\"checkout\", \"-b\", \"other\"])\n    (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    res = ds_clone.update(merge=True, follow=\"parentds\", recursive=True,\n                          on_failure=\"ignore\")\n    if on_adjusted:\n        # Our git-annex sync based on approach on adjusted branches is\n        # incompatible with follow='parentds'.\n        assert_in_results(res, action=\"update\", status=\"impossible\")\n        return\n    else:\n        assert_in_results(res, action=\"update\", status=\"ok\")\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n    ok_(ds_clone_subds.repo.is_under_annex(\"foo\"))\n\n    (ds_src_subds.pathobj / \"bar\").write_text(\"bar content\")\n    ds_src.save(recursive=True)\n    ds_clone_subds.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n    ds_clone.update(merge=True, follow=\"parentds\", recursive=True)\n    if not on_adjusted:\n        eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n\n# This test depends on the source repo being an un-adjusted branch.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_adjusted_warning(path=None):\n    path = Path(path)\n\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_subds = ds_src.create(\"subds\")\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n    maybe_adjust_repo(ds_clone_subds.repo)\n    # Note: Were we to save ds_clone here, we would get a merge conflict in the\n    # top repo for the submodule (even if using 'git annex sync' rather than\n    # 'git merge').\n\n    ds_src_subds.repo.call_git([\"checkout\", DEFAULT_BRANCH + \"^0\"])\n    (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    assert_in_results(\n        ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                        on_failure=\"ignore\"),\n        status=\"impossible\",\n        path=ds_clone_subds.path,\n        action=\"update\")\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n\n@slow  # 12 + 21sec on Yarik's laptop\n@pytest.mark.parametrize(\"on_adjusted\", [True, False])\n# Skip non-adjusted case for systems that only support adjusted branches.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_detached(path=None, *, on_adjusted):\n    if on_adjusted and DEFAULT_REMOTE != \"origin\" and \\\n       external_versions['cmd:annex'] <= \"8.20210330\":\n        raise SkipTest(\n            \"'git annex init' with adjusted branch currently fails \"\n            \"due to hard-coded 'origin'\")\n\n    # Note: For the adjusted case, this is not much more than a smoke test that\n    # on an adjusted branch we fail sensibly. The resulting state is not easy\n    # to reason about nor desirable.\n    path = Path(path)\n    # $path/source/s0/s1\n    # The additional dataset level is to gain some confidence that this works\n    # for nested datasets.\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s1 = ds_src_s0.create(\"s1\")\n    ds_src.save(recursive=True)\n    if on_adjusted:\n        # Note: We adjust after creating all the datasets above to avoid a bug\n        # fixed in git-annex 7.20191024, specifically bbdeb1a1a (sync: Fix\n        # crash when there are submodules and an adjusted branch is checked\n        # out, 2019-10-23).\n        for ds in [ds_src, ds_src_s0, ds_src_s1]:\n            maybe_adjust_repo(ds.repo)\n        ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n\n    ds_src_s1.repo.checkout(DEFAULT_BRANCH + \"^0\")\n    (ds_src_s1.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    res = ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    if on_adjusted:\n        # The top-level update is okay because there is no parent revision to\n        # update to.\n        assert_in_results(\n            res,\n            status=\"ok\",\n            path=ds_clone.path,\n            action=\"update\")\n        # The subdataset, on the other hand, is impossible.\n        assert_in_results(\n            res,\n            status=\"impossible\",\n            path=ds_clone_s1.path,\n            action=\"update\")\n        return\n    assert_repo_status(ds_clone.path)\n\n    # We brought in the revision and got to the same state of the remote.\n    # Blind saving here without bringing in the current subdataset revision\n    # would have resulted in a new commit in ds_clone that reverting the\n    # last subdataset ID recorded in ds_src.\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n    # Record a revision in the parent and then move HEAD away from it so that\n    # the explicit revision fetch fails.\n    (ds_src_s1.pathobj / \"bar\").write_text(\"bar content\")\n    ds_src.save(recursive=True)\n    ds_src_s1.repo.checkout(DEFAULT_BRANCH)\n    # This is the default, but just in case:\n    ds_src_s1.repo.config.set(\"uploadpack.allowAnySHA1InWant\", \"false\",\n                              scope=\"local\")\n    # Configure the fetcher to use v0 because Git defaults to v2 as of\n    # v2.26.0, which allows fetching unadvertised objects regardless\n    # of the value of uploadpack.allowAnySHA1InWant.\n    ds_clone_s1.repo.config.set(\"protocol.version\", \"0\", scope=\"local\")\n    res = ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    # The fetch with the explicit ref fails because it isn't advertised.\n    assert_in_results(\n        res,\n        status=\"impossible\",\n        path=ds_clone_s1.path,\n        action=\"update\")\n\n    # Back to the detached head.\n    ds_src_s1.repo.checkout(\"HEAD@{1}\")\n    # Set up a case where update() will not resolve the sibling.\n    ds_clone_s1.repo.call_git([\"branch\", \"--unset-upstream\"])\n    ds_clone_s1.config.reload(force=True)\n    ds_clone_s1.repo.call_git([\"remote\", \"add\", \"other\", ds_src_s1.path])\n    res = ds_clone.update(recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    # In this case, update() won't abort if we call with merge=False, but\n    # it does if the revision wasn't brought down in the `fetch(all_=True)`\n    # call.\n    assert_in_results(\n        res,\n        status=\"impossible\",\n        path=ds_clone_s1.path,\n        action=\"update\")\n\n\n@with_tempfile(mkdir=True)\ndef test_update_unborn_master(path=None):\n    ds_a = Dataset(op.join(path, \"ds-a\")).create()\n    ds_a.repo.call_git([\"branch\", \"-m\", DEFAULT_BRANCH, \"other\"])\n    ds_a.repo.checkout(DEFAULT_BRANCH, options=[\"--orphan\"])\n    ds_b = install(source=ds_a.path, path=op.join(path, \"ds-b\"))\n\n    ds_a.repo.checkout(\"other\")\n    (ds_a.pathobj / \"foo\").write_text(\"content\")\n    ds_a.save()\n\n    # clone() will try to switch away from an unborn branch if there\n    # is another ref available.  Reverse these efforts so that we can\n    # test that update() fails reasonably here because we should still\n    # be able to update from remotes that datalad didn't clone.\n    ds_b.repo.update_ref(\"HEAD\", \"refs/heads/\" + DEFAULT_BRANCH,\n                         symbolic=True)\n    assert_false(ds_b.repo.commit_exists(\"HEAD\"))\n    assert_status(\"impossible\",\n                  ds_b.update(merge=True, on_failure=\"ignore\"))\n\n    ds_b.repo.checkout(\"other\")\n    assert_status(\"ok\",\n                  ds_b.update(merge=True, on_failure=\"ignore\"))\n    eq_(ds_a.repo.get_hexsha(), ds_b.repo.get_hexsha())\n\n\n@slow  # ~25s\n@with_tempfile(mkdir=True)\ndef test_update_follow_parentds_lazy(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s0_s0 = ds_src_s0.create(\"s0\")\n    ds_src_s0.create(\"s1\")\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src.create(\"s2\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s0 = Dataset(ds_clone.pathobj / \"s0\")\n    ds_clone_s0_s0 = Dataset(ds_clone.pathobj / \"s0\" / \"s0\")\n    ds_clone_s0_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s1\")\n    ds_clone_s2 = Dataset(ds_clone.pathobj / \"s2\")\n\n    (ds_src_s0_s0.pathobj / \"foo\").write_text(\"in s0 s0\")\n    ds_src_s0_s0.save()\n    (ds_src_s1.pathobj / \"foo\").write_text(\"in s1\")\n    ds_src.save(recursive=True)\n    # State:\n    # .\n    # |-- s0\n    # |   |-- s0\n    # |   `-- s1  * matches registered commit\n    # |-- s1\n    # `-- s2      * matches registered commit\n    res = ds_clone.update(follow=\"parentds-lazy\", merge=True, recursive=True,\n                          on_failure=\"ignore\")\n    on_adjusted = ds_clone.repo.is_managed_branch()\n    # For adjusted branches, follow=parentds* bails with an impossible result,\n    # so the s0 update doesn't get brought in and s0_s0 also matches the\n    # registered commit.\n    n_notneeded_expected = 3 if on_adjusted else 2\n    assert_result_count(res, n_notneeded_expected,\n                        action=\"update\", status=\"notneeded\")\n    assert_in_results(res, action=\"update\", status=\"notneeded\",\n                      path=ds_clone_s0_s1.repo.path)\n    assert_in_results(res, action=\"update\", status=\"notneeded\",\n                      path=ds_clone_s2.repo.path)\n    if on_adjusted:\n        assert_in_results(res, action=\"update\", status=\"notneeded\",\n                          path=ds_clone_s0_s0.repo.path)\n        assert_repo_status(ds_clone.path,\n                           modified=[ds_clone_s0.repo.path,\n                                     ds_clone_s1.repo.path])\n    else:\n        assert_repo_status(ds_clone.path)\n\n\n@slow  # ~10s\n@with_tempfile(mkdir=True)\ndef test_update_follow_parentds_lazy_other_branch(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_sub = ds_src.create(\"sub\")\n    ds_src_sub.repo.checkout(DEFAULT_BRANCH, options=[\"-bother\"])\n    (ds_src_sub.pathobj / \"foo\").write_text(\"on other branch\")\n    ds_src_sub.save()\n    ds_src_sub.repo.checkout(DEFAULT_BRANCH)\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_src_sub.repo.checkout(\"other\")\n    ds_src.save(recursive=True)\n\n    with patch(\"datalad.support.gitrepo.GitRepo.fetch\") as fetch_cmd:\n        ds_clone.update(follow=\"parentds\", merge=\"ff-only\",\n                        recursive=True, on_failure=\"ignore\")\n        eq_(fetch_cmd.call_count, 2)\n\n    # With parentds-lazy, an unneeded fetch call in the subdataset is dropped.\n    with patch(\"datalad.support.gitrepo.GitRepo.fetch\") as fetch_cmd:\n        ds_clone.update(follow=\"parentds-lazy\", merge=\"ff-only\",\n                        recursive=True, on_failure=\"ignore\")\n        eq_(fetch_cmd.call_count, 1)\n\n    if not ds_clone.repo.is_managed_branch():\n        # Now the real thing.\n        ds_clone.update(follow=\"parentds-lazy\", merge=\"ff-only\",\n                        recursive=True)\n        ok_(op.lexists(str(ds_clone.pathobj / \"sub\" / \"foo\")))\n\n\n@with_tempfile(mkdir=True)\ndef test_update_adjusted_incompatible_with_ff_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    maybe_adjust_repo(ds_clone.repo)\n\n    assert_in_results(\n        ds_clone.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"update\", status=\"impossible\")\n    assert_in_results(\n        ds_clone.update(on_failure=\"ignore\"),\n        action=\"update\", status=\"ok\")\n\n\n@pytest.mark.parametrize(\"follow,action\", [\n    # Ideally each combination would be checked, but this test is a bit slow.\n    (\"parentds\", \"reset\"),\n    (\"sibling\", \"checkout\"),\n])\n@slow  # ~10s\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_update_how_subds_different(path=None, *, follow, action):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_sub = ds_src.create(\"sub\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    (ds_clone.pathobj / \"foo\").write_text(\"foo\")\n    ds_clone.save()\n    ds_clone_sub = Dataset(ds_clone.pathobj / \"sub\")\n\n    (ds_src_sub.pathobj / \"bar\").write_text(\"bar\")\n    ds_src.save(recursive=True)\n\n    # Add unrecorded state to make --follow=sibling/parentds differ.\n    (ds_src_sub.pathobj / \"baz\").write_text(\"baz\")\n    ds_src_sub.save()\n\n    ds_clone_repo = ds_clone.repo\n    ds_clone_hexsha_pre = ds_clone_repo.get_hexsha()\n\n    ds_clone_sub_repo = ds_clone_sub.repo\n    ds_clone_sub_branch_pre = ds_clone_sub_repo.get_active_branch()\n\n    res = ds_clone.update(follow=follow, how=\"merge\", how_subds=action,\n                          recursive=True)\n\n    assert_result_count(res, 1, action=\"merge\", status=\"ok\",\n                        path=ds_clone.path)\n    assert_result_count(res, 1, action=f\"update.{action}\", status=\"ok\",\n                        path=ds_clone_sub.path)\n\n    ds_clone_hexsha_post = ds_clone_repo.get_hexsha()\n    neq_(ds_clone_hexsha_pre, ds_clone_hexsha_post)\n    neq_(ds_src.repo.get_hexsha(), ds_clone_hexsha_post)\n    ok_(ds_clone_repo.is_ancestor(ds_clone_hexsha_pre, ds_clone_hexsha_post))\n\n    eq_(ds_clone_sub.repo.get_hexsha(),\n        ds_src_sub.repo.get_hexsha(None if follow == \"sibling\" else \"HEAD~\"))\n    ds_clone_sub_branch_post = ds_clone_sub_repo.get_active_branch()\n\n    if action == \"checkout\":\n        neq_(ds_clone_sub_branch_pre, ds_clone_sub_branch_post)\n        assert_false(ds_clone_sub_branch_post)\n    else:\n        eq_(ds_clone_sub_branch_pre, ds_clone_sub_branch_post)\n\n\n@slow  # ~15s\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_update_reset_dirty(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src_s2 = ds_src.create(\"s2\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n\n    (ds_src_s1.pathobj / \"foo\").write_text(\"foo\")\n    (ds_src_s2.pathobj / \"bar\").write_text(\"bar\")\n    ds_src.save(recursive=True)\n\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s1\")\n    ds_clone_s2 = Dataset(ds_clone.pathobj / \"s2\")\n    (ds_clone_s1.pathobj / \"dirt\").write_text(\"\")\n\n    res = ds_clone.update(follow=\"sibling\", how=\"reset\", recursive=True,\n                          on_failure=\"ignore\")\n\n    assert_result_count(res, 1, path=ds_clone.path,\n                        action=f\"update.reset\", status=\"error\")\n    assert_result_count(res, 1, path=ds_clone_s1.path,\n                        action=f\"update.reset\", status=\"error\")\n    assert_result_count(res, 1, path=ds_clone_s2.path,\n                        action=f\"update.reset\", status=\"ok\")\n\n    # s2 was reset...\n    eq_(ds_src_s2.repo.get_hexsha(), ds_clone_s2.repo.get_hexsha())\n    # ... but s1 and the top-level dataset stayed behind due to the dirty tree.\n    eq_(ds_src.repo.get_hexsha(\"HEAD~\"), ds_clone.repo.get_hexsha())\n    eq_(ds_src_s1.repo.get_hexsha(\"HEAD~\"), ds_clone_s1.repo.get_hexsha())\n\n    assert_repo_status(ds_clone.path,\n                       modified=[ds_clone_s1.repo.path,\n                                 ds_clone_s2.repo.path])\n\n\ndef test_process_how_args():\n    # --merge maps onto --how values. It has no equivalent of --how-subds,\n    # --which just gets set to --how's value when unspecified.\n    eq_(_process_how_args(merge=False, how=None, how_subds=None),\n        (None, None))\n    eq_(_process_how_args(merge=True, how=None, how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=\"any\", how=None, how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=\"ff-only\", how=None, how_subds=None),\n        (\"ff-only\", \"ff-only\"))\n\n    # Values other than the default --merge=False can not be mixed with\n    # non-default how values.\n    with assert_raises(ValueError):\n        _process_how_args(merge=True, how=\"merge\", how_subds=None)\n    with assert_raises(ValueError):\n        _process_how_args(merge=True, how=None, how_subds=\"merge\")\n\n    # --how-subds inherits the value of --how...\n    eq_(_process_how_args(merge=False, how=\"fetch\", how_subds=None),\n        (None, None))\n    eq_(_process_how_args(merge=False, how=\"merge\", how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=False, how=\"ff-only\", how_subds=None),\n        (\"ff-only\", \"ff-only\"))\n    # ... unless --how-subds is explicitly specified.\n    eq_(_process_how_args(merge=False, how=\"merge\", how_subds=\"fetch\"),\n        (\"merge\", None))\n\n\n@with_tempfile(mkdir=True)\ndef test_update_fetch_failure(path=None):\n    path = Path(path)\n\n    ds_a = Dataset(path / \"ds_a\").create()\n    s1 = ds_a.create(\"s1\")\n    ds_a.create(\"s2\")\n\n    ds_b = install(source=ds_a.path, path=str(path / \"ds-b\"), recursive=True)\n\n    # Rename s1 to make fetch fail.\n    s1.pathobj.rename(s1.pathobj.parent / \"s3\")\n\n    res = ds_b.update(recursive=True, on_failure=\"ignore\")\n    assert_in_results(\n        res,\n        status=\"error\",\n        path=str(ds_b.pathobj / \"s1\"),\n        action=\"update\")\n    assert_in_results(\n        res,\n        status=\"ok\",\n        path=str(ds_b.pathobj / \"s2\"),\n        action=\"update\")\n    assert_in_results(\n        res,\n        status=\"ok\",\n        path=ds_b.path,\n        action=\"update\")\n",
            "new_file_content": "# ex: set sts=4 ts=4 sw=4 et:\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n#\n#   See COPYING file distributed along with the datalad package for the\n#   copyright and license terms.\n#\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n\"\"\"Test update action\n\n\"\"\"\n\nimport os\nimport os.path as op\nfrom os.path import exists\nfrom os.path import join as opj\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom datalad import cfg as dl_cfg\nfrom datalad.api import (\n    clone,\n    install,\n    remove,\n    update,\n)\nfrom datalad.distribution.update import _process_how_args\nfrom datalad.support.annexrepo import AnnexRepo\nfrom datalad.support.external_versions import external_versions\nfrom datalad.support.gitrepo import GitRepo\nfrom datalad.tests.utils_pytest import (\n    DEFAULT_BRANCH,\n    DEFAULT_REMOTE,\n    SkipTest,\n    assert_false,\n    assert_in,\n    assert_in_results,\n    assert_is_instance,\n    assert_not_in,\n    assert_raises,\n    assert_repo_status,\n    assert_result_count,\n    assert_status,\n    create_tree,\n    eq_,\n    known_failure_windows,\n    maybe_adjust_repo,\n    neq_,\n    ok_,\n    ok_file_has_content,\n    skip_if_adjusted_branch,\n    slow,\n    with_tempfile,\n)\nfrom datalad.utils import (\n    Path,\n    chpwd,\n    knows_annex,\n    rmtree,\n)\n\nfrom ..dataset import Dataset\n\n\n@slow\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_update_simple(origin=None, src_path=None, dst_path=None):\n    ca = dict(result_renderer='disabled')\n    # a remote dataset with a subdataset underneath\n    origds = Dataset(origin).create(**ca)\n    # naming is weird, but a legacy artifact\n    _ = origds.create('subm 1', **ca)\n    _ = origds.create('2', **ca)\n\n    # prepare src\n    source = install(src_path, source=origin, recursive=True)\n    # forget we cloned it by removing remote, which should lead to\n    # setting tracking branch to target:\n    source.repo.remove_remote(DEFAULT_REMOTE)\n    # also forget the declared absolute location of the submodules, and turn them\n    # relative to this/a clone\n    for sub in source.subdatasets(result_xfm=lambda x: x['gitmodule_name']):\n        source.subdatasets(path=sub, set_property=[('url', './{}'.format(sub))])\n\n    # dataset without sibling will not need updates\n    assert_status('notneeded', source.update())\n    # deprecation message doesn't ruin things\n    assert_status('notneeded', source.update(fetch_all=True))\n    # but error if unknown sibling is given\n    assert_status('impossible', source.update(sibling='funky', on_failure='ignore'))\n\n    # get a clone to update later on:\n    dest = install(dst_path, source=src_path, recursive=True)\n    # test setup done;\n    # assert all fine\n    assert_repo_status(dst_path)\n    assert_repo_status(src_path)\n\n    # update yields nothing => up-to-date\n    assert_status('ok', dest.update())\n    assert_repo_status(dst_path)\n\n    # modify remote:\n    with open(opj(src_path, \"update.txt\"), \"w\") as f:\n        f.write(\"Additional content\")\n    source.save(path=\"update.txt\", message=\"Added update.txt\")\n    assert_repo_status(src_path)\n\n    # update without `merge` only fetches:\n    assert_status('ok', dest.update())\n    # modification is not known to active branch:\n    assert_not_in(\"update.txt\",\n                  dest.repo.get_files(dest.repo.get_active_branch()))\n    # modification is known to branch <default remote>/<default branch>\n    assert_in(\"update.txt\",\n              dest.repo.get_files(DEFAULT_REMOTE + \"/\" + DEFAULT_BRANCH))\n\n    # merge:\n    assert_status('ok', dest.update(merge=True))\n    # modification is now known to active branch:\n    assert_in(\"update.txt\",\n              dest.repo.get_files(dest.repo.get_active_branch()))\n    # it's known to annex, but has no content yet:\n    annexprops = dest.repo.get_file_annexinfo(\"update.txt\",\n                                              eval_availability=True)\n    annexprops['key']  # blows if unknown\n    eq_(False, annexprops['has_content'])\n\n    # check subdataset path constraints, baseline (parent + 2 subds)\n    assert_result_count(dest.update(recursive=True),\n                        3, status='ok', type='dataset')\n    # no recursion and invalid path still updates the parent\n    res = dest.update(path='whatever')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # invalid path with recursion also does\n    res = dest.update(recursive=True, path='whatever')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # valid path and no recursion only updates the parent\n    res = dest.update(path='subm 1')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # valid path and recursion updates matching\n    res = dest.update(recursive=True, path='subm 1')\n    assert_result_count(res, 2, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    assert_result_count(res, 1, status='ok', path=str(dest.pathobj / 'subm 1'))\n    # additional invalid path doesn't hurt\n    res = dest.update(recursive=True, path=['subm 1', 'mike'])\n    assert_result_count(res, 2, status='ok', type='dataset')\n    # full match\n    res = dest.update(recursive=True, path=['subm 1', '2'])\n    assert_result_count(res, 3, status='ok', type='dataset')\n\n    # test that update doesn't crash if we specify only a single path (submod) to\n    # operate on\n    with chpwd(dest.path):\n        # in 0.11.x it would be a single result since \"pwd\" dataset is not\n        # considered, and would be relative path (as specified).\n        # In 0.12.0 - it would include implicit pwd dataset, and paths would be absolute\n        res_update = update(path=['subm 1'], recursive=True)\n        assert_result_count(res_update, 2)\n        for p in dest.path, str(dest.pathobj / 'subm 1'):\n            assert_in_results(res_update, path=p, action='update', status='ok', type='dataset')\n\n        # and with merge we would also try to save (but there would be no changes)\n        res_merge = update(path=['subm 1'], recursive=True, merge=True)\n        assert_result_count(res_merge, 2, action='update')\n        # 2 of \"updates\" really.\n        assert_in_results(res_merge, action='update', status='ok', type='dataset')\n        assert_in_results(res_merge, action='save', status='notneeded', type='dataset')\n\n    # smoke-test if recursive update doesn't fail if submodule is removed\n    # and that we can run it from within a dataset without providing it\n    # explicitly\n    assert_result_count(\n        dest.remove('subm 1'), 1,\n        status='ok', action='remove', path=opj(dest.path, 'subm 1'))\n    with chpwd(dest.path):\n        assert_result_count(\n            update(recursive=True), 2,\n            status='ok', type='dataset')\n    assert_result_count(\n        dest.update(merge=True, recursive=True), 2,\n        action='update', status='ok', type='dataset')\n\n    # and now test recursive update with merging in differences\n    create_tree(opj(source.path, '2'), {'load.dat': 'heavy'})\n    source.save(opj('2', 'load.dat'),\n                message=\"saving changes within subm2\",\n                recursive=True)\n    assert_result_count(\n        dest.update(merge=True, recursive=True), 2,\n        action='update', status='ok', type='dataset')\n    # and now we can get new file\n    dest.get(opj('2', 'load.dat'))\n    ok_file_has_content(opj(dest.path, '2', 'load.dat'), 'heavy')\n\n\n@with_tempfile\n@with_tempfile\ndef test_update_git_smoke(src_path=None, dst_path=None):\n    # Apparently was just failing on git repos for basic lack of coverage, hence this quick test\n    ds = Dataset(src_path).create(annex=False)\n    target = install(\n        dst_path, source=src_path,\n        result_xfm='datasets', return_type='item-or-list')\n    create_tree(ds.path, {'file.dat': '123'})\n    ds.save('file.dat')\n    assert_result_count(\n        target.update(recursive=True, merge=True), 1,\n        action='update', status='ok', type='dataset')\n    ok_file_has_content(opj(target.path, 'file.dat'), '123')\n\n\n@slow  # ~9s\n@with_tempfile(mkdir=True)\ndef test_update_fetch_all(path=None):\n    path = Path(path)\n    remote_1 = str(path / \"remote_1\")\n    remote_2 = str(path / \"remote_2\")\n\n    ds = Dataset(path / \"src\").create()\n    src = ds.repo.path\n\n    ds_rmt1 = clone(source=src, path=remote_1)\n    ds_rmt2 = clone(source=src, path=remote_2)\n\n    ds.siblings('add', name=\"sibling_1\", url=remote_1)\n    ds.siblings('add', name=\"sibling_2\", url=remote_2)\n\n    # modify the remotes:\n    (ds_rmt1.pathobj / \"first.txt\").write_text(\"some file load\")\n    ds_rmt1.save()\n\n    # TODO: Modify an already present file!\n\n    (ds_rmt2.pathobj / \"second.txt\").write_text(\"different file load\")\n    ds_rmt2.save()\n\n    # Let's init some special remote which we couldn't really update/fetch\n    if not dl_cfg.get('datalad.tests.dataladremote'):\n        ds.repo.init_remote(\n            'datalad',\n            ['encryption=none', 'type=external', 'externaltype=datalad'])\n    # fetch all remotes\n    assert_result_count(\n        ds.update(), 1, status='ok', type='dataset')\n\n    # no merge, so changes are not in active branch:\n    assert_not_in(\"first.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    assert_not_in(\"second.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    # but we know the changes in remote branches:\n    assert_in(\"first.txt\", ds.repo.get_files(\"sibling_1/\" + DEFAULT_BRANCH))\n    assert_in(\"second.txt\", ds.repo.get_files(\"sibling_2/\" + DEFAULT_BRANCH))\n\n    # no merge strategy for multiple remotes yet:\n    # more clever now, there is a tracking branch that provides a remote\n    #assert_raises(NotImplementedError, ds.update, merge=True)\n\n    # merge a certain remote:\n    assert_result_count(\n        ds.update(sibling='sibling_1', merge=True),\n        1, action='update', status='ok', type='dataset')\n\n    # changes from sibling_2 still not present:\n    assert_not_in(\"second.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    # changes from sibling_1 merged:\n    assert_in(\"first.txt\",\n              ds.repo.get_files(ds.repo.get_active_branch()))\n    # it's known to annex, but has no content yet:\n    annexprops = ds.repo.get_file_annexinfo(\n        \"first.txt\", eval_availability=True)\n    annexprops['key']  # blows if unknown\n    eq_(False, annexprops['has_content'])\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_newthings_coming_down(originpath=None, destpath=None):\n    origin = GitRepo(originpath, create=True)\n    create_tree(originpath, {'load.dat': 'heavy'})\n    Dataset(originpath).save('load.dat')\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    assert_is_instance(ds.repo, GitRepo)\n    assert_in(DEFAULT_REMOTE, ds.repo.get_remotes())\n    # turn origin into an annex\n    origin = AnnexRepo(originpath, create=True)\n    # clone doesn't know yet\n    assert_false(knows_annex(ds.path))\n    # but after an update it should\n    # no merge, only one sibling, no parameters should be specific enough\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    assert(knows_annex(ds.path))\n    # no branches appeared\n    eq_(ds.repo.get_branches(), [DEFAULT_BRANCH])\n    # now merge, and get an annex\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    assert_in('git-annex', ds.repo.get_branches())\n    assert_is_instance(ds.repo, AnnexRepo)\n    # should be fully functional\n    testfname = opj(ds.path, 'load.dat')\n    assert_false(ds.repo.file_has_content(testfname))\n    ds.get('.')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    # check that a new tag comes down\n    origin.tag('first!')\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(ds.repo.get_tags(output='name')[0], 'first!')\n\n    # and now we destroy the remote annex\n    origin.call_git(['config', '--remove-section', 'annex'])\n    rmtree(opj(origin.path, '.git', 'annex'), chmod_files=True)\n    origin.call_git(['branch', '-D', 'git-annex'])\n    origin = GitRepo(originpath)\n    assert_false(knows_annex(originpath))\n\n    # and update the local clone\n    # for now this should simply not fail (see gh-793), later might be enhanced to a\n    # graceful downgrade\n    before_branches = ds.repo.get_branches()\n    ok_(any(\"git-annex\" in b\n            for b in ds.repo.get_remote_branches()))\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(before_branches, ds.repo.get_branches())\n    # annex branch got pruned\n    assert_false(any(\"git-annex\" in b\n                     for b in ds.repo.get_remote_branches()))\n    # check that a new tag comes down even if repo types mismatch\n    origin.tag('second!')\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(ds.repo.get_tags(output='name')[-1], 'second!')\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_update_volatile_subds(originpath=None, otherpath=None, destpath=None):\n    origin = Dataset(originpath).create()\n    repo = origin.repo\n    if repo.is_managed_branch() and repo.git_annex_version <= \"8.20201129\":\n        # Fails before git-annex's fd161da2c (adjustTree: Consider submodule\n        # deletions, 2021-01-06).\n        raise SkipTest(\n            \"On adjusted branch, test requires fix in more recent git-annex\")\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    # as a submodule\n    sname = 'subm 1'\n    osm1 = origin.create(sname)\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    # nothing without a merge, no inappropriate magic\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # and we should be able to do update with recursive invocation\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        1, action='update', status='ok', type='dataset')\n    # known, and placeholder exists\n    assert_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    ok_(exists(opj(ds.path, sname)))\n\n    # remove from origin\n    origin.remove(sname, reckless='availability')\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # gone locally, wasn't checked out\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    assert_false(exists(opj(ds.path, sname)))\n\n    # re-introduce at origin\n    osm1 = origin.create(sname)\n    create_tree(osm1.path, {'load.dat': 'heavy'})\n    origin.save(opj(osm1.path, 'load.dat'))\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # grab new content of uninstall subdataset, right away\n    ds.get(opj(ds.path, sname, 'load.dat'))\n    ok_file_has_content(opj(ds.path, sname, 'load.dat'), 'heavy')\n\n    # modify ds and subds at origin\n    create_tree(origin.path, {'mike': 'this', sname: {'probe': 'little'}})\n    origin.save(recursive=True)\n    assert_repo_status(origin.path)\n\n    # updates for both datasets should come down the pipe\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        2, action='update', status='ok', type='dataset')\n    assert_repo_status(ds.path)\n\n    # now remove just-installed subdataset from origin again\n    origin.remove(sname, reckless='kill')\n    assert_not_in(sname, origin.subdatasets(result_xfm='relpaths'))\n    assert_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    # merge should disconnect the installed subdataset, but leave the actual\n    # ex-subdataset alone\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        1, action='update', type='dataset')\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    ok_file_has_content(opj(ds.path, sname, 'load.dat'), 'heavy')\n    ok_(Dataset(opj(ds.path, sname)).is_installed())\n\n    # now remove the now disconnected subdataset for further tests\n    remove(dataset=op.join(ds.path, sname), reckless='kill')\n    assert_repo_status(ds.path)\n\n    # new separate subdataset, not within the origin dataset\n    otherds = Dataset(otherpath).create()\n    # install separate dataset as a submodule\n    ds.install(source=otherds.path, path='other')\n    create_tree(otherds.path, {'brand': 'new'})\n    otherds.save()\n    assert_repo_status(otherds.path)\n    # pull in changes\n    res = ds.update(merge=True, recursive=True)\n    assert_result_count(\n        res, 2, status='ok', action='update', type='dataset')\n    # the next is to check for #2858\n    assert_repo_status(ds.path)\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_reobtain_data(originpath=None, destpath=None):\n    origin = Dataset(originpath).create()\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    # no harm\n    assert_result_count(ds.update(merge=True, reobtain_data=True),\n                        1, action=\"update\", status=\"ok\")\n    # content\n    create_tree(origin.path, {'load.dat': 'heavy'})\n    origin.save(opj(origin.path, 'load.dat'))\n    # update does not bring data automatically\n    assert_result_count(ds.update(merge=True, reobtain_data=True),\n                        1, action=\"update\", status=\"ok\")\n    assert_in('load.dat', ds.repo.get_annexed_files())\n    assert_false(ds.repo.file_has_content('load.dat'))\n    # now get data\n    ds.get('load.dat')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    # new content at origin\n    create_tree(origin.path, {'novel': 'but boring'})\n    origin.save()\n    # update must not bring in data for new file\n    result = ds.update(merge=True, reobtain_data=True)\n    assert_in_results(result, action='get', status='notneeded')\n\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    assert_in('novel', ds.repo.get_annexed_files())\n    assert_false(ds.repo.file_has_content('novel'))\n    # modify content at origin\n    os.remove(opj(origin.path, 'load.dat'))\n    create_tree(origin.path, {'load.dat': 'light'})\n    origin.save()\n    # update must update file with existing data, but leave empty one alone\n    res = ds.update(merge=True, reobtain_data=True)\n    assert_result_count(res, 1, status='ok', type='dataset', action='update')\n    assert_result_count(res, 1, status='ok', type='file', action='get')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'light')\n    assert_false(ds.repo.file_has_content('novel'))\n\n\n@with_tempfile(mkdir=True)\ndef test_multiway_merge(path=None):\n    # prepare ds with two siblings, but no tracking branch\n    ds = Dataset(op.join(path, 'ds_orig')).create()\n    r1 = AnnexRepo(path=op.join(path, 'ds_r1'), git_opts={'bare': True})\n    r2 = GitRepo(path=op.join(path, 'ds_r2'), git_opts={'bare': True})\n    ds.siblings(action='add', name='r1', url=r1.path)\n    ds.siblings(action='add', name='r2', url=r2.path)\n    assert_status('ok', ds.push(to='r1'))\n    # push unlike publish reports on r2 not being an annex remote with a\n    # 'notneeded'\n    assert_status(('ok', 'notneeded'), ds.push(to='r2'))\n    # just a fetch should be no issue\n    assert_status('ok', ds.update())\n    # ATM we do not support multi-way merges\n    assert_status('impossible', ds.update(merge=True, on_failure='ignore'))\n\n\ndef test_unrelated_history_merge(tmp_path):\n    # prepare two independent datasets and try merging one into another\n    ds = Dataset(tmp_path / 'ds').create()\n    repo = AnnexRepo(tmp_path / 'repo')\n    f = (tmp_path / 'repo' / 'file.dat')\n    f.write_text(\"data\")\n    repo.add(str(f))\n    repo.commit()\n    # ATM we do not do any checks and allow such addition\n    assert_status('ok', ds.siblings(action='add', name='repo', url=repo.path))\n    res = ds.update(how='merge', on_failure='ignore')\n    # ATM we do not have any special handling, just that the first result record\n    # would have that error in \"message\" field\n    assert_status('error', res[0])\n    assert_in('refusing to merge unrelated histories', res[0]['message'])\n\n\n# `git annex sync REMOTE` rather than `git merge TARGET` is used on an\n# adjusted branch, so we don't give an error if TARGET can't be\n# determined.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_no_merge_target(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    assert_repo_status(ds_src.path)\n    ds_clone.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n    res = ds_clone.update(merge=True, on_failure=\"ignore\")\n    assert_in_results(res, status=\"impossible\", action=\"update\")\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge TARGET` being used.\n@skip_if_adjusted_branch\n@slow  # 17sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_conflict(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s0 = Dataset(path / \"clone\" / \"s0\")\n    ds_clone_s1 = Dataset(path / \"clone\" / \"s1\")\n\n    (ds_src.pathobj / \"foo\").write_text(\"src content\")\n    ds_src.save(to_git=True)\n\n    (ds_clone.pathobj / \"foo\").write_text(\"clone content\")\n    ds_clone.save(to_git=True)\n\n    # Top-level merge failure\n    res = ds_clone.update(merge=True, on_failure=\"ignore\")\n    assert_in_results(res, action=\"merge\", status=\"error\")\n    assert_in_results(res, action=\"update\", status=\"error\")\n    # Deal with the conflicts. Note that save() won't handle this gracefully\n    # because it will try to commit with a pathspec, which git doesn't allow\n    # during a merge.\n    ds_clone.repo.call_git([\"checkout\", \"--theirs\", \"--\", \"foo\"])\n    ds_clone.repo.call_git([\"add\", \"--\", \"foo\"])\n    ds_clone.repo.call_git([\"commit\", \"--no-edit\"])\n    assert_repo_status(ds_clone.path)\n\n    # Top-level and subdataset merge failure\n    (ds_src_s0.pathobj / \"foo\").write_text(\"src s0 content\")\n    (ds_src_s1.pathobj / \"foo\").write_text(\"no conflict\")\n    ds_src.save(recursive=True, to_git=True)\n\n    (ds_clone_s0.pathobj / \"foo\").write_text(\"clone s0 content\")\n    ds_clone.save(recursive=True, to_git=True)\n    res = ds_clone.update(merge=True, recursive=True, on_failure=\"ignore\")\n    assert_result_count(res, 2, action=\"merge\", status=\"error\")\n    assert_result_count(res, 2, action=\"update\", status=\"error\")\n    assert_in_results(res, action=\"merge\", status=\"ok\",\n                      path=ds_clone_s1.path)\n    assert_in_results(res, action=\"update\", status=\"ok\",\n                      path=ds_clone_s1.path)\n    # No saving happens if there's a top-level conflict.\n    assert_repo_status(ds_clone.path,\n                       modified=[ds_clone_s0.path, ds_clone_s1.path])\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge TARGET` being used.\n@skip_if_adjusted_branch\n@slow  # 13sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_conflict_in_subdataset_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_src_sub_conflict = ds_src.create(\"sub_conflict\")\n    ds_src_sub_noconflict = ds_src.create(\"sub_noconflict\")\n    ds_src.save()\n\n    # Set up a scenario where one subdataset has a conflict between the remote\n    # and local version, but the parent dataset does not have a conflict\n    # because it hasn't recorded the subdataset state.\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_sub_conflict = Dataset(path / \"clone\" / \"sub_conflict\")\n    ds_clone_sub_noconflict = Dataset(path / \"clone\" / \"sub_noconflict\")\n\n    (ds_src_sub_conflict.pathobj / \"foo\").write_text(\"src content\")\n    ds_src_sub_conflict.save(to_git=True)\n\n    (ds_clone_sub_conflict.pathobj / \"foo\").write_text(\"clone content\")\n    ds_clone_sub_conflict.save(to_git=True)\n\n    (ds_src_sub_noconflict.pathobj / \"foo\").write_text(\"src content\")\n    ds_src_sub_noconflict.save()\n\n    res = ds_clone.update(merge=True, recursive=True, on_failure=\"ignore\")\n    assert_in_results(res, action=\"merge\", status=\"error\",\n                      path=ds_clone_sub_conflict.path)\n    assert_in_results(res, action=\"merge\", status=\"ok\",\n                      path=ds_clone_sub_noconflict.path)\n    assert_in_results(res, action=\"save\", status=\"ok\",\n                      path=ds_clone.path)\n    # We saved the subdataset without a conflict...\n    assert_repo_status(ds_clone_sub_noconflict.path)\n    # ... but the one with the conflict leaves it for the caller to handle.\n    ok_(ds_clone_sub_conflict.repo.call_git(\n        [\"ls-files\", \"--unmerged\", \"--\", \"foo\"], read_only=True).strip())\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge --ff-only ...` being used.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_ff_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_clone_ff = install(source=ds_src.path, path=path / \"clone_ff\",\n                          result_xfm=\"datasets\")\n\n    ds_clone_nonff = install(source=ds_src.path, path=path / \"clone_nonff\",\n                             result_xfm=\"datasets\")\n\n    (ds_clone_nonff.pathobj / \"foo\").write_text(\"local change\")\n    ds_clone_nonff.save(recursive=True)\n\n    (ds_src.pathobj / \"bar\").write_text(\"remote change\")\n    ds_src.save(recursive=True)\n\n    assert_in_results(\n        ds_clone_ff.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"ok\")\n\n    # ff-only prevents a non-fast-forward ...\n    assert_in_results(\n        ds_clone_nonff.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"error\")\n    # ... that would work with \"any\".\n    assert_in_results(\n        ds_clone_nonff.update(merge=\"any\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"ok\")\n\n\n@slow  # 11sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_other_branch(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    on_adjusted = ds_src.repo.is_managed_branch()\n    ds_src_subds = ds_src.create(\"subds\")\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n\n    ds_src_subds.repo.call_git([\"checkout\", \"-b\", \"other\"])\n    (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    res = ds_clone.update(merge=True, follow=\"parentds\", recursive=True,\n                          on_failure=\"ignore\")\n    if on_adjusted:\n        # Our git-annex sync based on approach on adjusted branches is\n        # incompatible with follow='parentds'.\n        assert_in_results(res, action=\"update\", status=\"impossible\")\n        return\n    else:\n        assert_in_results(res, action=\"update\", status=\"ok\")\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n    ok_(ds_clone_subds.repo.is_under_annex(\"foo\"))\n\n    (ds_src_subds.pathobj / \"bar\").write_text(\"bar content\")\n    ds_src.save(recursive=True)\n    ds_clone_subds.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n    ds_clone.update(merge=True, follow=\"parentds\", recursive=True)\n    if not on_adjusted:\n        eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n\n# This test depends on the source repo being an un-adjusted branch.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_adjusted_warning(path=None):\n    path = Path(path)\n\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_subds = ds_src.create(\"subds\")\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n    maybe_adjust_repo(ds_clone_subds.repo)\n    # Note: Were we to save ds_clone here, we would get a merge conflict in the\n    # top repo for the submodule (even if using 'git annex sync' rather than\n    # 'git merge').\n\n    ds_src_subds.repo.call_git([\"checkout\", DEFAULT_BRANCH + \"^0\"])\n    (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    assert_in_results(\n        ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                        on_failure=\"ignore\"),\n        status=\"impossible\",\n        path=ds_clone_subds.path,\n        action=\"update\")\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n\n@slow  # 12 + 21sec on Yarik's laptop\n@pytest.mark.parametrize(\"on_adjusted\", [True, False])\n# Skip non-adjusted case for systems that only support adjusted branches.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_detached(path=None, *, on_adjusted):\n    if on_adjusted and DEFAULT_REMOTE != \"origin\" and \\\n       external_versions['cmd:annex'] <= \"8.20210330\":\n        raise SkipTest(\n            \"'git annex init' with adjusted branch currently fails \"\n            \"due to hard-coded 'origin'\")\n\n    # Note: For the adjusted case, this is not much more than a smoke test that\n    # on an adjusted branch we fail sensibly. The resulting state is not easy\n    # to reason about nor desirable.\n    path = Path(path)\n    # $path/source/s0/s1\n    # The additional dataset level is to gain some confidence that this works\n    # for nested datasets.\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s1 = ds_src_s0.create(\"s1\")\n    ds_src.save(recursive=True)\n    if on_adjusted:\n        # Note: We adjust after creating all the datasets above to avoid a bug\n        # fixed in git-annex 7.20191024, specifically bbdeb1a1a (sync: Fix\n        # crash when there are submodules and an adjusted branch is checked\n        # out, 2019-10-23).\n        for ds in [ds_src, ds_src_s0, ds_src_s1]:\n            maybe_adjust_repo(ds.repo)\n        ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n\n    ds_src_s1.repo.checkout(DEFAULT_BRANCH + \"^0\")\n    (ds_src_s1.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    res = ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    if on_adjusted:\n        # The top-level update is okay because there is no parent revision to\n        # update to.\n        assert_in_results(\n            res,\n            status=\"ok\",\n            path=ds_clone.path,\n            action=\"update\")\n        # The subdataset, on the other hand, is impossible.\n        assert_in_results(\n            res,\n            status=\"impossible\",\n            path=ds_clone_s1.path,\n            action=\"update\")\n        return\n    assert_repo_status(ds_clone.path)\n\n    # We brought in the revision and got to the same state of the remote.\n    # Blind saving here without bringing in the current subdataset revision\n    # would have resulted in a new commit in ds_clone that reverting the\n    # last subdataset ID recorded in ds_src.\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n    # Record a revision in the parent and then move HEAD away from it so that\n    # the explicit revision fetch fails.\n    (ds_src_s1.pathobj / \"bar\").write_text(\"bar content\")\n    ds_src.save(recursive=True)\n    ds_src_s1.repo.checkout(DEFAULT_BRANCH)\n    # This is the default, but just in case:\n    ds_src_s1.repo.config.set(\"uploadpack.allowAnySHA1InWant\", \"false\",\n                              scope=\"local\")\n    # Configure the fetcher to use v0 because Git defaults to v2 as of\n    # v2.26.0, which allows fetching unadvertised objects regardless\n    # of the value of uploadpack.allowAnySHA1InWant.\n    ds_clone_s1.repo.config.set(\"protocol.version\", \"0\", scope=\"local\")\n    res = ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    # The fetch with the explicit ref fails because it isn't advertised.\n    assert_in_results(\n        res,\n        status=\"impossible\",\n        path=ds_clone_s1.path,\n        action=\"update\")\n\n    # Back to the detached head.\n    ds_src_s1.repo.checkout(\"HEAD@{1}\")\n    # Set up a case where update() will not resolve the sibling.\n    ds_clone_s1.repo.call_git([\"branch\", \"--unset-upstream\"])\n    ds_clone_s1.config.reload(force=True)\n    ds_clone_s1.repo.call_git([\"remote\", \"add\", \"other\", ds_src_s1.path])\n    res = ds_clone.update(recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    # In this case, update() won't abort if we call with merge=False, but\n    # it does if the revision wasn't brought down in the `fetch(all_=True)`\n    # call.\n    assert_in_results(\n        res,\n        status=\"impossible\",\n        path=ds_clone_s1.path,\n        action=\"update\")\n\n\n@with_tempfile(mkdir=True)\ndef test_update_unborn_master(path=None):\n    ds_a = Dataset(op.join(path, \"ds-a\")).create()\n    ds_a.repo.call_git([\"branch\", \"-m\", DEFAULT_BRANCH, \"other\"])\n    ds_a.repo.checkout(DEFAULT_BRANCH, options=[\"--orphan\"])\n    ds_b = install(source=ds_a.path, path=op.join(path, \"ds-b\"))\n\n    ds_a.repo.checkout(\"other\")\n    (ds_a.pathobj / \"foo\").write_text(\"content\")\n    ds_a.save()\n\n    # clone() will try to switch away from an unborn branch if there\n    # is another ref available.  Reverse these efforts so that we can\n    # test that update() fails reasonably here because we should still\n    # be able to update from remotes that datalad didn't clone.\n    ds_b.repo.update_ref(\"HEAD\", \"refs/heads/\" + DEFAULT_BRANCH,\n                         symbolic=True)\n    assert_false(ds_b.repo.commit_exists(\"HEAD\"))\n    assert_status(\"impossible\",\n                  ds_b.update(merge=True, on_failure=\"ignore\"))\n\n    ds_b.repo.checkout(\"other\")\n    assert_status(\"ok\",\n                  ds_b.update(merge=True, on_failure=\"ignore\"))\n    eq_(ds_a.repo.get_hexsha(), ds_b.repo.get_hexsha())\n\n\n@slow  # ~25s\n@with_tempfile(mkdir=True)\ndef test_update_follow_parentds_lazy(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s0_s0 = ds_src_s0.create(\"s0\")\n    ds_src_s0.create(\"s1\")\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src.create(\"s2\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s0 = Dataset(ds_clone.pathobj / \"s0\")\n    ds_clone_s0_s0 = Dataset(ds_clone.pathobj / \"s0\" / \"s0\")\n    ds_clone_s0_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s1\")\n    ds_clone_s2 = Dataset(ds_clone.pathobj / \"s2\")\n\n    (ds_src_s0_s0.pathobj / \"foo\").write_text(\"in s0 s0\")\n    ds_src_s0_s0.save()\n    (ds_src_s1.pathobj / \"foo\").write_text(\"in s1\")\n    ds_src.save(recursive=True)\n    # State:\n    # .\n    # |-- s0\n    # |   |-- s0\n    # |   `-- s1  * matches registered commit\n    # |-- s1\n    # `-- s2      * matches registered commit\n    res = ds_clone.update(follow=\"parentds-lazy\", merge=True, recursive=True,\n                          on_failure=\"ignore\")\n    on_adjusted = ds_clone.repo.is_managed_branch()\n    # For adjusted branches, follow=parentds* bails with an impossible result,\n    # so the s0 update doesn't get brought in and s0_s0 also matches the\n    # registered commit.\n    n_notneeded_expected = 3 if on_adjusted else 2\n    assert_result_count(res, n_notneeded_expected,\n                        action=\"update\", status=\"notneeded\")\n    assert_in_results(res, action=\"update\", status=\"notneeded\",\n                      path=ds_clone_s0_s1.repo.path)\n    assert_in_results(res, action=\"update\", status=\"notneeded\",\n                      path=ds_clone_s2.repo.path)\n    if on_adjusted:\n        assert_in_results(res, action=\"update\", status=\"notneeded\",\n                          path=ds_clone_s0_s0.repo.path)\n        assert_repo_status(ds_clone.path,\n                           modified=[ds_clone_s0.repo.path,\n                                     ds_clone_s1.repo.path])\n    else:\n        assert_repo_status(ds_clone.path)\n\n\n@slow  # ~10s\n@with_tempfile(mkdir=True)\ndef test_update_follow_parentds_lazy_other_branch(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_sub = ds_src.create(\"sub\")\n    ds_src_sub.repo.checkout(DEFAULT_BRANCH, options=[\"-bother\"])\n    (ds_src_sub.pathobj / \"foo\").write_text(\"on other branch\")\n    ds_src_sub.save()\n    ds_src_sub.repo.checkout(DEFAULT_BRANCH)\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_src_sub.repo.checkout(\"other\")\n    ds_src.save(recursive=True)\n\n    with patch(\"datalad.support.gitrepo.GitRepo.fetch\") as fetch_cmd:\n        ds_clone.update(follow=\"parentds\", merge=\"ff-only\",\n                        recursive=True, on_failure=\"ignore\")\n        eq_(fetch_cmd.call_count, 2)\n\n    # With parentds-lazy, an unneeded fetch call in the subdataset is dropped.\n    with patch(\"datalad.support.gitrepo.GitRepo.fetch\") as fetch_cmd:\n        ds_clone.update(follow=\"parentds-lazy\", merge=\"ff-only\",\n                        recursive=True, on_failure=\"ignore\")\n        eq_(fetch_cmd.call_count, 1)\n\n    if not ds_clone.repo.is_managed_branch():\n        # Now the real thing.\n        ds_clone.update(follow=\"parentds-lazy\", merge=\"ff-only\",\n                        recursive=True)\n        ok_(op.lexists(str(ds_clone.pathobj / \"sub\" / \"foo\")))\n\n\n@with_tempfile(mkdir=True)\ndef test_update_adjusted_incompatible_with_ff_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    maybe_adjust_repo(ds_clone.repo)\n\n    assert_in_results(\n        ds_clone.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"update\", status=\"impossible\")\n    assert_in_results(\n        ds_clone.update(on_failure=\"ignore\"),\n        action=\"update\", status=\"ok\")\n\n\n@pytest.mark.parametrize(\"follow,action\", [\n    # Ideally each combination would be checked, but this test is a bit slow.\n    (\"parentds\", \"reset\"),\n    (\"sibling\", \"checkout\"),\n])\n@slow  # ~10s\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_update_how_subds_different(path=None, *, follow, action):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_sub = ds_src.create(\"sub\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    (ds_clone.pathobj / \"foo\").write_text(\"foo\")\n    ds_clone.save()\n    ds_clone_sub = Dataset(ds_clone.pathobj / \"sub\")\n\n    (ds_src_sub.pathobj / \"bar\").write_text(\"bar\")\n    ds_src.save(recursive=True)\n\n    # Add unrecorded state to make --follow=sibling/parentds differ.\n    (ds_src_sub.pathobj / \"baz\").write_text(\"baz\")\n    ds_src_sub.save()\n\n    ds_clone_repo = ds_clone.repo\n    ds_clone_hexsha_pre = ds_clone_repo.get_hexsha()\n\n    ds_clone_sub_repo = ds_clone_sub.repo\n    ds_clone_sub_branch_pre = ds_clone_sub_repo.get_active_branch()\n\n    res = ds_clone.update(follow=follow, how=\"merge\", how_subds=action,\n                          recursive=True)\n\n    assert_result_count(res, 1, action=\"merge\", status=\"ok\",\n                        path=ds_clone.path)\n    assert_result_count(res, 1, action=f\"update.{action}\", status=\"ok\",\n                        path=ds_clone_sub.path)\n\n    ds_clone_hexsha_post = ds_clone_repo.get_hexsha()\n    neq_(ds_clone_hexsha_pre, ds_clone_hexsha_post)\n    neq_(ds_src.repo.get_hexsha(), ds_clone_hexsha_post)\n    ok_(ds_clone_repo.is_ancestor(ds_clone_hexsha_pre, ds_clone_hexsha_post))\n\n    eq_(ds_clone_sub.repo.get_hexsha(),\n        ds_src_sub.repo.get_hexsha(None if follow == \"sibling\" else \"HEAD~\"))\n    ds_clone_sub_branch_post = ds_clone_sub_repo.get_active_branch()\n\n    if action == \"checkout\":\n        neq_(ds_clone_sub_branch_pre, ds_clone_sub_branch_post)\n        assert_false(ds_clone_sub_branch_post)\n    else:\n        eq_(ds_clone_sub_branch_pre, ds_clone_sub_branch_post)\n\n\n@slow  # ~15s\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_update_reset_dirty(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src_s2 = ds_src.create(\"s2\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n\n    (ds_src_s1.pathobj / \"foo\").write_text(\"foo\")\n    (ds_src_s2.pathobj / \"bar\").write_text(\"bar\")\n    ds_src.save(recursive=True)\n\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s1\")\n    ds_clone_s2 = Dataset(ds_clone.pathobj / \"s2\")\n    (ds_clone_s1.pathobj / \"dirt\").write_text(\"\")\n\n    res = ds_clone.update(follow=\"sibling\", how=\"reset\", recursive=True,\n                          on_failure=\"ignore\")\n\n    assert_result_count(res, 1, path=ds_clone.path,\n                        action=f\"update.reset\", status=\"error\")\n    assert_result_count(res, 1, path=ds_clone_s1.path,\n                        action=f\"update.reset\", status=\"error\")\n    assert_result_count(res, 1, path=ds_clone_s2.path,\n                        action=f\"update.reset\", status=\"ok\")\n\n    # s2 was reset...\n    eq_(ds_src_s2.repo.get_hexsha(), ds_clone_s2.repo.get_hexsha())\n    # ... but s1 and the top-level dataset stayed behind due to the dirty tree.\n    eq_(ds_src.repo.get_hexsha(\"HEAD~\"), ds_clone.repo.get_hexsha())\n    eq_(ds_src_s1.repo.get_hexsha(\"HEAD~\"), ds_clone_s1.repo.get_hexsha())\n\n    assert_repo_status(ds_clone.path,\n                       modified=[ds_clone_s1.repo.path,\n                                 ds_clone_s2.repo.path])\n\n\ndef test_process_how_args():\n    # --merge maps onto --how values. It has no equivalent of --how-subds,\n    # --which just gets set to --how's value when unspecified.\n    eq_(_process_how_args(merge=False, how=None, how_subds=None),\n        (None, None))\n    eq_(_process_how_args(merge=True, how=None, how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=\"any\", how=None, how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=\"ff-only\", how=None, how_subds=None),\n        (\"ff-only\", \"ff-only\"))\n\n    # Values other than the default --merge=False can not be mixed with\n    # non-default how values.\n    with assert_raises(ValueError):\n        _process_how_args(merge=True, how=\"merge\", how_subds=None)\n    with assert_raises(ValueError):\n        _process_how_args(merge=True, how=None, how_subds=\"merge\")\n\n    # --how-subds inherits the value of --how...\n    eq_(_process_how_args(merge=False, how=\"fetch\", how_subds=None),\n        (None, None))\n    eq_(_process_how_args(merge=False, how=\"merge\", how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=False, how=\"ff-only\", how_subds=None),\n        (\"ff-only\", \"ff-only\"))\n    # ... unless --how-subds is explicitly specified.\n    eq_(_process_how_args(merge=False, how=\"merge\", how_subds=\"fetch\"),\n        (\"merge\", None))\n\n\n@with_tempfile(mkdir=True)\ndef test_update_fetch_failure(path=None):\n    path = Path(path)\n\n    ds_a = Dataset(path / \"ds_a\").create()\n    s1 = ds_a.create(\"s1\")\n    ds_a.create(\"s2\")\n\n    ds_b = install(source=ds_a.path, path=str(path / \"ds-b\"), recursive=True)\n\n    # Rename s1 to make fetch fail.\n    s1.pathobj.rename(s1.pathobj.parent / \"s3\")\n\n    res = ds_b.update(recursive=True, on_failure=\"ignore\")\n    assert_in_results(\n        res,\n        status=\"error\",\n        path=str(ds_b.pathobj / \"s1\"),\n        action=\"update\")\n    assert_in_results(\n        res,\n        status=\"ok\",\n        path=str(ds_b.pathobj / \"s2\"),\n        action=\"update\")\n    assert_in_results(\n        res,\n        status=\"ok\",\n        path=ds_b.path,\n        action=\"update\")\n",
            "header": {
                "file": {
                    "path": "datalad/distribution/tests/test_update.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "03e51846d",
                "new_commit_hash": "befcd4077",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/datalad/distribution/tests/test_update.py"
            },
            "plus_file": {
                "path": "b/datalad/distribution/tests/test_update.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 490,
                            "length": 6
                        },
                        "new_range": {
                            "start": 490,
                            "length": 23
                        },
                        "section": "def test_multiway_merge(path=None):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    assert_status('impossible', ds.update(merge=True, on_failure='ignore'))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_unrelated_history_merge(tmp_path):",
                                "type": "added"
                            },
                            {
                                "content": "    # prepare two independent datasets and try merging one into another",
                                "type": "added"
                            },
                            {
                                "content": "    ds = Dataset(tmp_path / 'ds').create()",
                                "type": "added"
                            },
                            {
                                "content": "    repo = AnnexRepo(tmp_path / 'repo')",
                                "type": "added"
                            },
                            {
                                "content": "    f = (tmp_path / 'repo' / 'file.dat')",
                                "type": "added"
                            },
                            {
                                "content": "    f.write_text(\"data\")",
                                "type": "added"
                            },
                            {
                                "content": "    repo.add(str(f))",
                                "type": "added"
                            },
                            {
                                "content": "    repo.commit()",
                                "type": "added"
                            },
                            {
                                "content": "    # ATM we do not do any checks and allow such addition",
                                "type": "added"
                            },
                            {
                                "content": "    assert_status('ok', ds.siblings(action='add', name='repo', url=repo.path))",
                                "type": "added"
                            },
                            {
                                "content": "    res = ds.update(how='merge', on_failure='ignore')",
                                "type": "added"
                            },
                            {
                                "content": "    # ATM we do not have any special handling, just that the first result record",
                                "type": "added"
                            },
                            {
                                "content": "    # would have that error in \"message\" field",
                                "type": "added"
                            },
                            {
                                "content": "    assert_status('error', res[0])",
                                "type": "added"
                            },
                            {
                                "content": "    assert_in('refusing to merge unrelated histories', res[0]['message'])",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "# `git annex sync REMOTE` rather than `git merge TARGET` is used on an",
                                "type": "context"
                            },
                            {
                                "content": "# adjusted branch, so we don't give an error if TARGET can't be",
                                "type": "context"
                            },
                            {
                                "content": "# determined.",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "datalad/distribution/tests/test_update.py",
                            "type": "function",
                            "name": "test_unrelated_history_merge",
                            "content": "def test_unrelated_history_merge(tmp_path):\n    # prepare two independent datasets and try merging one into another\n    ds = Dataset(tmp_path / 'ds').create()\n    repo = AnnexRepo(tmp_path / 'repo')\n    f = (tmp_path / 'repo' / 'file.dat')\n    f.write_text(\"data\")\n    repo.add(str(f))\n    repo.commit()\n    # ATM we do not do any checks and allow such addition\n    assert_status('ok', ds.siblings(action='add', name='repo', url=repo.path))\n    res = ds.update(how='merge', on_failure='ignore')\n    # ATM we do not have any special handling, just that the first result record\n    # would have that error in \"message\" field\n    assert_status('error', res[0])\n    assert_in('refusing to merge unrelated histories', res[0]['message'])",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 493,
                            "end_lineno": 507,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-\n# ex: set sts=4 ts=4 sw=4 et:\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n#\n#   See COPYING file distributed along with the datalad package for the\n#   copyright and license terms.\n#\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n\"\"\"Internal low-level interface to Git repositories\n\n\"\"\"\n\nimport logging\nimport os\nimport os.path as op\nimport posixpath\nimport re\nimport warnings\nfrom collections.abc import Mapping\nfrom functools import wraps\nfrom itertools import chain\nfrom os import linesep\nfrom os.path import (\n    commonprefix,\n    curdir,\n    dirname,\n    exists,\n    isabs,\n)\nfrom os.path import join as opj\nfrom os.path import (\n    pardir,\n    relpath,\n    sep,\n)\n\nimport datalad.utils as ut\nfrom datalad import ssh_manager\nfrom datalad.cmd import (\n    BatchedCommand,\n    GitWitlessRunner,\n    NoCapture,\n    StdOutErrCapture,\n    WitlessProtocol,\n)\nfrom datalad.config import (\n    parse_gitconfig_dump,\n    write_config_section,\n)\nfrom datalad.consts import (\n    ILLEGAL_CHARS_WIN,\n    RESERVED_NAMES_WIN,\n)\nfrom datalad.core.local.repo import repo_from_path\nfrom datalad.dataset.gitrepo import GitRepo as CoreGitRepo\nfrom datalad.dataset.gitrepo import (\n    _get_dot_git,\n    path_based_str_repr,\n)\nfrom datalad.log import log_progress\nfrom datalad.support.due import (\n    Doi,\n    due,\n)\nfrom datalad.utils import (\n    Path,\n    PurePosixPath,\n    ensure_dir,\n    ensure_list,\n    ensure_unicode,\n    generate_file_chunks,\n    getpwd,\n    is_interactive,\n    on_windows,\n    optional_args,\n    path_is_subpath,\n    posix_relpath,\n)\n\nfrom .exceptions import (\n    CapturedException,\n    CommandError,\n    FileNotInRepositoryError,\n    InvalidGitReferenceError,\n    InvalidGitRepositoryError,\n    NoSuchPathError,\n)\n# imports from same module:\nfrom .external_versions import external_versions\nfrom .network import (\n    RI,\n    PathRI,\n    is_ssh,\n)\nfrom .path import get_parent_paths, get_filtered_paths_\n\n# shortcuts\n_curdirsep = curdir + sep\n_pardirsep = pardir + sep\n\n\nlgr = logging.getLogger('datalad.gitrepo')\n\n\n# outside the repo base classes only used in ConfigManager\ndef to_options(split_single_char_options=True, **kwargs):\n    \"\"\"Transform keyword arguments into a list of cmdline options\n\n    Imported from GitPython.\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n\n    Parameters\n    ----------\n    split_single_char_options: bool\n\n    kwargs:\n\n    Returns\n    -------\n    list\n    \"\"\"\n    def dashify(string):\n        return string.replace('_', '-')\n\n    def transform_kwarg(name, value, split_single_char_options):\n        if len(name) == 1:\n            if value is True:\n                return [\"-%s\" % name]\n            elif value not in (False, None):\n                if split_single_char_options:\n                    return [\"-%s\" % name, \"%s\" % value]\n                else:\n                    return [\"-%s%s\" % (name, value)]\n        else:\n            if value is True:\n                return [\"--%s\" % dashify(name)]\n            elif value is not False and value is not None:\n                return [\"--%s=%s\" % (dashify(name), value)]\n        return []\n\n    args = []\n    kwargs = dict(sorted(kwargs.items(), key=lambda x: x[0]))\n    for k, v in kwargs.items():\n        if isinstance(v, (list, tuple)):\n            for value in v:\n                args += transform_kwarg(k, value, split_single_char_options)\n        else:\n            args += transform_kwarg(k, v, split_single_char_options)\n    return args\n\n\ndef _normalize_path(base_dir, path):\n    \"\"\"Helper to check paths passed to methods of this class.\n\n    Checks whether `path` is beneath `base_dir` and normalizes it.\n    Additionally paths are converted into relative paths with respect to\n    `base_dir`, considering PWD in case of relative paths. This\n    is intended to be used in repository classes, which means that\n    `base_dir` usually will be the repository's base directory.\n\n    Parameters\n    ----------\n    base_dir: str\n        directory to serve as base to normalized, relative paths\n    path: str\n        path to be normalized\n\n    Returns\n    -------\n    str:\n        path, that is a relative path with respect to `base_dir`\n    \"\"\"\n    if not path:\n        return path\n    pathobj = Path(path)\n\n    # do absolute() in addition to always get an absolute path\n    # even with non-existing base_dirs on windows\n    base_dir = str(Path(base_dir).resolve().absolute())  # realpath OK\n\n    # path = normpath(path)\n    # Note: disabled normpath, because it may break paths containing symlinks;\n    # But we don't want to realpath relative paths, in case cwd isn't the\n    # correct base.\n\n    if pathobj.is_absolute():\n        # path might already be a symlink pointing to annex etc,\n        # so realpath only its directory, to get \"inline\" with\n        # realpath(base_dir) above\n        path = str(pathobj.parent.resolve() / pathobj.name)  # realpath OK\n    # Executive decision was made to not do this kind of magic!\n    #\n    # elif commonprefix([realpath(getpwd()), base_dir]) == base_dir:\n    #     # If we are inside repository, rebuilt relative paths.\n    #     path = opj(realpath(getpwd()), path)\n    #\n    # BUT with relative curdir/pardir start it would assume relative to curdir\n    #\n    elif path.startswith(_curdirsep) or path.startswith(_pardirsep):\n        path = str(Path(getpwd()).resolve() / pathobj)  # realpath OK\n    else:\n        # We were called from outside the repo. Therefore relative paths\n        # are interpreted as being relative to self.path already.\n        return path\n\n    if commonprefix([path, base_dir]) != base_dir:\n        raise FileNotInRepositoryError(msg=\"Path outside repository: %s\"\n                                           % base_dir, filename=path)\n\n    return relpath(path, start=base_dir)\n\n\n@optional_args\ndef normalize_path(func):\n    \"\"\"Decorator to provide unified path conversion for a single file\n\n    Unlike normalize_paths, intended to be used for functions dealing with a\n    single filename at a time\n\n    Note\n    ----\n    This is intended to be used within the repository classes and therefore\n    returns a class method!\n\n    The decorated function is expected to take a path at\n    first positional argument (after 'self'). Additionally the class `func`\n    is a member of, is expected to have an attribute 'path'.\n    \"\"\"\n\n    @wraps(func)\n    def  _wrap_normalize_path(self, file_, *args, **kwargs):\n        file_new = _normalize_path(self.path, file_)\n        return func(self, file_new, *args, **kwargs)\n\n    return  _wrap_normalize_path\n\n\n@optional_args\ndef normalize_paths(func, match_return_type=True, map_filenames_back=False,\n                    serialize=False):\n    \"\"\"Decorator to provide unified path conversions.\n\n    Note\n    ----\n    This is intended to be used within the repository classes and therefore\n    returns a class method!\n\n    The decorated function is expected to take a path or a list of paths at\n    first positional argument (after 'self'). Additionally the class `func`\n    is a member of, is expected to have an attribute 'path'.\n\n    Accepts either a list of paths or a single path in a str. Passes a list\n    to decorated function either way, but would return based on the value of\n    match_return_type and possibly input argument.\n\n    If a call to the wrapped function includes normalize_path and it is False\n    no normalization happens for that function call (used for calls to wrapped\n    functions within wrapped functions, while possible CWD is within a\n    repository)\n\n    Parameters\n    ----------\n    match_return_type : bool, optional\n      If True, and a single string was passed in, it would return the first\n      element of the output (after verifying that it is a list of length 1).\n      It makes easier to work with single files input.\n    map_filenames_back : bool, optional\n      If True and returned value is a dictionary, it assumes to carry entries\n      one per file, and then filenames are mapped back to as provided from the\n      normalized (from the root of the repo) paths\n    serialize : bool, optional\n      Loop through files giving only a single one to the function one at a time.\n      This allows to simplify implementation and interface to annex commands\n      which do not take multiple args in the same call (e.g. checkpresentkey)\n    \"\"\"\n\n    @wraps(func)\n    def  _wrap_normalize_paths(self, files, *args, **kwargs):\n\n        normalize = _normalize_path if kwargs.pop('normalize_paths', True) \\\n            else lambda rpath, filepath: filepath\n\n        if files:\n            if isinstance(files, str) or not files:\n                files_new = [normalize(self.path, files)]\n                single_file = True\n            elif isinstance(files, list):\n                files_new = [normalize(self.path, path) for path in files]\n                single_file = False\n            else:\n                raise ValueError(\"_files_decorator: Don't know how to handle \"\n                                 \"instance of %s.\" % type(files))\n        else:\n            single_file = None\n            files_new = []\n\n        if map_filenames_back:\n            def remap_filenames(out):\n                \"\"\"Helper to map files back to non-normalized paths\"\"\"\n                if isinstance(out, dict):\n                    assert(len(out) == len(files_new))\n                    files_ = [files] if single_file else files\n                    mapped = out.__class__()\n                    for fin, fout in zip(files_, files_new):\n                        mapped[fin] = out[fout]\n                    return mapped\n                else:\n                    return out\n        else:\n            remap_filenames = lambda x: x\n\n        if serialize:  # and not single_file:\n            result = [\n                func(self, f, *args, **kwargs)\n                for f in files_new\n            ]\n        else:\n            result = func(self, files_new, *args, **kwargs)\n\n        if single_file is None:\n            # no files were provided, nothing we can do really\n            return result\n        elif (result is None) or not match_return_type or not single_file:\n            # If function doesn't return anything or no denormalization\n            # was requested or it was not a single file\n            return remap_filenames(result)\n        elif single_file:\n            if len(result) != 1:\n                # Magic doesn't apply\n                return remap_filenames(result)\n            elif isinstance(result, (list, tuple)):\n                return result[0]\n            elif isinstance(result, dict) and tuple(result)[0] == files_new[0]:\n                # assume that returned dictionary has files as keys.\n                return tuple(result.values())[0]\n            else:\n                # no magic can apply\n                return remap_filenames(result)\n        else:\n            return RuntimeError(\"should have not got here... check logic\")\n\n    return  _wrap_normalize_paths\n\n\nif \"2.24.0\" <= external_versions[\"cmd:git\"] < \"2.25.0\":\n    # An unintentional change in Git 2.24.0 led to `ls-files -o` traversing\n    # into untracked submodules when multiple pathspecs are given, returning\n    # repositories that are deeper than the first level. This helper filters\n    # these deeper levels out so that save_() doesn't fail trying to add them.\n    #\n    # This regression fixed with upstream's 072a231016 (2019-12-10).\n    def _prune_deeper_repos(repos):\n        firstlevel_repos = []\n        prev = None\n        for repo in sorted(repos):\n            if not (prev and str(repo).startswith(prev)):\n                prev = str(repo)\n                firstlevel_repos.append(repo)\n        return firstlevel_repos\nelse:\n    def _prune_deeper_repos(repos):\n        return repos\n\n\nclass GitProgress(WitlessProtocol):\n    \"\"\"Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n    # inform super-class to capture stderr\n    proc_err = True\n\n    _num_op_codes = 10\n    BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT, ENUMERATING = \\\n        [1 << x for x in range(_num_op_codes)]\n    STAGE_MASK = BEGIN | END\n    OP_MASK = ~STAGE_MASK\n\n    DONE_TOKEN = 'done.'\n    TOKEN_SEPARATOR = ', '\n\n    _known_ops = {\n        COUNTING: (\"Counting\", \"Objects\"),\n        ENUMERATING: (\"Enumerating\", \"Objects\"),\n        COMPRESSING: (\"Compressing\", \"Objects\"),\n        WRITING: (\"Writing\", \"Objects\"),\n        RECEIVING: (\"Receiving\", \"Objects\"),\n        RESOLVING: (\"Resolving\", \"Deltas\"),\n        FINDING_SOURCES: (\"Finding\", \"Sources\"),\n        CHECKING_OUT: (\"Check out\", \"Things\"),\n    }\n\n    __slots__ = ('_seen_ops', '_pbars', '_encoding')\n\n    re_op_absolute = re.compile(r\"(remote: )?([\\w\\s]+):\\s+()(\\d+)()(.*)\")\n    re_op_relative = re.compile(r\"(remote: )?([\\w\\s]+):\\s+(\\d+)% \\((\\d+)/(\\d+)\\)(.*)\")\n\n    def __init__(self, *args):\n        super().__init__(*args)\n        self._unprocessed = None\n\n    def connection_made(self, transport):\n        super().connection_made(transport)\n        self._seen_ops = []\n        self._pbars = set()\n\n    def process_exited(self):\n        # take down any progress bars that were not closed orderly\n        for pbar_id in self._pbars:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Finished',\n            )\n        super().process_exited()\n\n    def pipe_data_received(self, fd, byts):\n        # progress reports only come from stderr\n        if fd != 2:\n            # let the base class decide what to do with it\n            super().pipe_data_received(fd, byts)\n            return\n        for line in byts.splitlines(keepends=True):\n            # put any unprocessed content back in front\n            line = self._unprocessed + line if self._unprocessed else line\n            self._unprocessed = None\n            if not self._parse_progress_line(line):\n                # anything that doesn't look like a progress report\n                # is retained and returned\n                # in case of partial progress lines, this can lead to\n                # leakage of progress info into the output, but\n                # it is better to enable better (maybe more expensive)\n                # subsequent filtering than hiding lines with\n                # unknown, potentially important info\n                lgr.debug('Non-progress stderr: %s', line)\n                if line.endswith((b'\\r', b'\\n')):\n                    # complete non-progress line, pass on\n                    super().pipe_data_received(fd, line)\n                else:\n                    # an incomplete line, maybe the next batch completes\n                    # it to become a recognizable progress report\n                    self._unprocessed = line\n\n    def _parse_progress_line(self, line):\n        \"\"\"Process a single line\n\n        Parameters\n        ----------\n        line : bytes\n\n        Returns\n        -------\n        bool\n          Flag whether the line was recognized as a Git progress report.\n        \"\"\"\n        # handle\n        # Counting objects: 4, done.\n        # Compressing objects:  50% (1/2)\n        # Compressing objects: 100% (2/2)\n        # Compressing objects: 100% (2/2), done.\n        line = line.decode(self.encoding) if isinstance(line, bytes) else line\n        if line.startswith(('warning:', 'error:', 'fatal:')):\n            return False\n\n        # find escape characters and cut them away - regex will not work with\n        # them as they are non-ascii. As git might expect a tty, it will send them\n        last_valid_index = None\n        for i, c in enumerate(reversed(line)):\n            if ord(c) < 32:\n                # its a slice index\n                last_valid_index = -i - 1\n            # END character was non-ascii\n        # END for each character in line\n        if last_valid_index is not None:\n            line = line[:last_valid_index]\n        # END cut away invalid part\n        line = line.rstrip()\n\n        cur_count, max_count = None, None\n        match = self.re_op_relative.match(line)\n        if match is None:\n            match = self.re_op_absolute.match(line)\n\n        if not match:\n            return False\n        # END could not get match\n\n        op_code = 0\n        _remote, op_name, _percent, cur_count, max_count, message = match.groups()\n\n        # get operation id\n        if op_name == \"Counting objects\":\n            op_code |= self.COUNTING\n        elif op_name == \"Compressing objects\":\n            op_code |= self.COMPRESSING\n        elif op_name == \"Writing objects\":\n            op_code |= self.WRITING\n        elif op_name == 'Receiving objects':\n            op_code |= self.RECEIVING\n        elif op_name == 'Resolving deltas':\n            op_code |= self.RESOLVING\n        elif op_name == 'Finding sources':\n            op_code |= self.FINDING_SOURCES\n        elif op_name == 'Checking out files':\n            op_code |= self.CHECKING_OUT\n        elif op_name == 'Enumerating objects':\n            op_code |= self.ENUMERATING\n        else:\n            # Note: On windows it can happen that partial lines are sent\n            # Hence we get something like \"CompreReceiving objects\", which is\n            # a blend of \"Compressing objects\" and \"Receiving objects\".\n            # This can't really be prevented.\n            lgr.debug(\n                'Output line matched a progress report of an unknown type: %s',\n                line)\n            # TODO investigate if there is any chance that we might swallow\n            # important info -- until them do not flag this line\n            # as progress\n            return False\n        # END handle op code\n\n        pbar_id = 'gitprogress-{}-{}'.format(id(self), op_code)\n\n        op_props = self._known_ops[op_code]\n\n        # figure out stage\n        if op_code not in self._seen_ops:\n            self._seen_ops.append(op_code)\n            op_code |= self.BEGIN\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start {} {}'.format(\n                    op_props[0].lower(),\n                    op_props[1].lower(),\n                ),\n                label=op_props[0],\n                unit=' {}'.format(op_props[1]),\n                total=float(max_count) if max_count else None,\n            )\n            self._pbars.add(pbar_id)\n        # END begin opcode\n\n        if message is None:\n            message = ''\n        # END message handling\n\n        done_progress = False\n        message = message.strip()\n        if message.endswith(self.DONE_TOKEN):\n            op_code |= self.END\n            message = message[:-len(self.DONE_TOKEN)]\n            done_progress = True\n        # END end message handling\n        message = message.strip(self.TOKEN_SEPARATOR)\n\n        if cur_count and max_count:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                line,\n                update=float(cur_count),\n                noninteractive_level=logging.DEBUG,\n            )\n\n        if done_progress:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Finished {} {}'.format(\n                    op_props[0].lower(),\n                    op_props[1].lower(),\n                ),\n                noninteractive_level=logging.DEBUG,\n            )\n            self._pbars.discard(pbar_id)\n        return True\n\n\nclass StdOutCaptureWithGitProgress(GitProgress):\n    proc_out = True\n\n\nclass FetchInfo(dict):\n    \"\"\"\n    dict that carries results of a fetch operation of a single head\n\n    Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n\n    NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \\\n        FAST_FORWARD, ERROR = [1 << x for x in range(8)]\n\n    _re_fetch_result = re.compile(r'^\\s*(.) (\\[?[\\w\\s\\.$@]+\\]?)\\s+(.+) [-> ]+ ([^\\s]+)(    \\(.*\\)?$)?')\n\n    _flag_map = {\n        '!': ERROR,\n        '+': FORCED_UPDATE,\n        '*': 0,\n        '=': HEAD_UPTODATE,\n        ' ': FAST_FORWARD,\n        '-': TAG_UPDATE,\n    }\n    _operation_map = {\n        NEW_TAG: 'new-tag',\n        NEW_HEAD: 'new-branch',\n        HEAD_UPTODATE: 'uptodate',\n        TAG_UPDATE: 'tag-update',\n        REJECTED: 'rejected',\n        FORCED_UPDATE: 'forced-update',\n        FAST_FORWARD: 'fast-forward',\n        ERROR: 'error',\n    }\n\n    @classmethod\n    def _from_line(cls, line):\n        \"\"\"Parse information from the given line as returned by git-fetch -v\n        and return a new FetchInfo object representing this information.\n        \"\"\"\n        match = cls._re_fetch_result.match(line)\n        if match is None:\n            raise ValueError(\"Failed to parse line: %r\" % line)\n\n        # parse lines\n        control_character, operation, local_remote_ref, remote_local_ref, note = \\\n            match.groups()\n\n        # parse flags from control_character\n        flags = 0\n        try:\n            flags |= cls._flag_map[control_character]\n        except KeyError:\n            raise ValueError(\n                \"Control character %r unknown as parsed from line %r\"\n                % (control_character, line))\n        # END control char exception handling\n\n        # parse operation string for more info - makes no sense for symbolic refs,\n        # but we parse it anyway\n        old_commit = None\n        if 'rejected' in operation:\n            flags |= cls.REJECTED\n        if 'new tag' in operation:\n            flags |= cls.NEW_TAG\n        if 'tag update' in operation:\n            flags |= cls.TAG_UPDATE\n        if 'new branch' in operation:\n            flags |= cls.NEW_HEAD\n        if '...' in operation or '..' in operation:\n            split_token = '...'\n            if control_character == ' ':\n                split_token = split_token[:-1]\n            old_commit = operation.split(split_token)[0]\n        # END handle refspec\n\n        return cls(\n            ref=remote_local_ref.strip(),\n            local_ref=local_remote_ref.strip(),\n            # convert flag int into a list of operation labels\n            operations=[\n                cls._operation_map[o]\n                for o in cls._operation_map.keys()\n                if flags & o\n            ],\n            note=note,\n            old_commit=old_commit,\n        )\n\n\nclass PushInfo(dict):\n    \"\"\"dict that carries results of a push operation of a single head\n\n    Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n    NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \\\n        FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)]\n\n    _flag_map = {'X': NO_MATCH,\n                 '-': DELETED,\n                 '*': 0,\n                 '+': FORCED_UPDATE,\n                 ' ': FAST_FORWARD,\n                 '=': UP_TO_DATE,\n                 '!': ERROR}\n\n    _operation_map = {\n        NEW_TAG: 'new-tag',\n        NEW_HEAD: 'new-branch',\n        NO_MATCH: 'no-match',\n        REJECTED: 'rejected',\n        REMOTE_REJECTED: 'remote-rejected',\n        REMOTE_FAILURE: 'remote-failure',\n        DELETED: 'deleted',\n        FORCED_UPDATE: 'forced-update',\n        FAST_FORWARD: 'fast-forward',\n        UP_TO_DATE: 'uptodate',\n        ERROR: 'error',\n    }\n\n    @classmethod\n    def _from_line(cls, line):\n        \"\"\"Create a new PushInfo instance as parsed from line which is expected to be like\n            refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes\"\"\"\n        control_character, from_to, summary = line.split('\\t', 3)\n        flags = 0\n\n        # control character handling\n        try:\n            flags |= cls._flag_map[control_character]\n        except KeyError:\n            raise ValueError(\"Control character %r unknown as parsed from line %r\" % (control_character, line))\n        # END handle control character\n\n        # from_to handling\n        from_ref_string, to_ref_string = from_to.split(':')\n\n        # commit handling, could be message or commit info\n        old_commit = None\n        if summary.startswith('['):\n            if \"[rejected]\" in summary:\n                flags |= cls.REJECTED\n            elif \"[remote rejected]\" in summary:\n                flags |= cls.REMOTE_REJECTED\n            elif \"[remote failure]\" in summary:\n                flags |= cls.REMOTE_FAILURE\n            elif \"[no match]\" in summary:\n                flags |= cls.ERROR\n            elif \"[new tag]\" in summary:\n                flags |= cls.NEW_TAG\n            elif \"[new branch]\" in summary:\n                flags |= cls.NEW_HEAD\n            # up-to-date encoded in control character\n        else:\n            # fast-forward or forced update - was encoded in control character,\n            # but we parse the old and new commit\n            split_token = \"...\"\n            if control_character == \" \":\n                split_token = \"..\"\n            old_sha, _new_sha = summary.split(' ')[0].split(split_token)\n            # have to use constructor here as the sha usually is abbreviated\n            old_commit = old_sha\n        # END message handling\n\n        return cls(\n            from_ref=from_ref_string.strip(),\n            to_ref=to_ref_string.strip(),\n            # convert flag int into a list of operation labels\n            operations=[\n                cls._operation_map[o]\n                for o in cls._operation_map.keys()\n                if flags & o\n            ],\n            note=summary.strip(),\n            old_commit=old_commit,\n        )\n\n\n@path_based_str_repr\nclass GitRepo(CoreGitRepo):\n    \"\"\"Representation of a git repository\n\n    \"\"\"\n    # We must check git config to have name and email set, but\n    # should do it once\n    _config_checked = False\n\n    GIT_MIN_VERSION = \"2.19.1\"\n    git_version = None\n\n    @classmethod\n    def _check_git_version(cls):\n        external_versions.check(\"cmd:git\", min_version=cls.GIT_MIN_VERSION)\n        cls.git_version = external_versions['cmd:git']\n\n    # This is the least common denominator to claim that a user\n    # used DataLad.\n    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262\n    @due.dcite(Doi(\"10.21105/joss.03262\"),\n               # override path since there is no need ATM for such details\n               path=\"datalad\",\n               description=\"DataLad - Data management and distribution platform\")\n    def __init__(self, path, runner=None, create=True,\n                 git_opts=None, repo=None, fake_dates=False,\n                 create_sanity_checks=True,\n                 **kwargs):\n        \"\"\"Creates representation of git repository at `path`.\n\n        Can also be used to create a git repository at `path`.\n\n        Parameters\n        ----------\n        path: str\n          path to the git repository; In case it's not an absolute path,\n          it's relative to PWD\n        create: bool, optional\n          if true, creates a git repository at `path` if there is none. Also\n          creates `path`, if it doesn't exist.\n          If set to false, an exception is raised in case `path` doesn't exist\n          or doesn't contain a git repository.\n        repo: git.Repo, optional\n          This argument is ignored.\n        create_sanity_checks: bool, optional\n          Whether to perform sanity checks during initialization (when\n          `create=True` and target path is not a valid repo already), such as\n          that new repository is not created in the directory where git already\n          tracks some files.\n        kwargs:\n          keyword arguments serving as additional options to the git-init\n          command. Therefore, it makes sense only if called with `create`.\n\n          Generally, this way of passing options to the git executable is\n          (or will be) used a lot in this class. It's a transformation of\n          python-style keyword arguments (or a `dict`) to command line arguments,\n          provided by GitPython.\n\n          A single character keyword will be prefixed by '-', multiple characters\n          by '--'. An underscore in the keyword becomes a dash. The value of the\n          keyword argument is used as the value for the corresponding command\n          line argument. Assigning a boolean creates a flag.\n\n          Examples:\n          no_commit=True => --no-commit\n          C='/my/path'   => -C /my/path\n\n        \"\"\"\n        # this will set up .pathobj and .dot_git\n        super().__init__(path)\n\n        if self.git_version is None:\n            self._check_git_version()\n\n        # BEGIN Repo validity test\n        # We want to fail early for tests, that would be performed a lot. In\n        # particular this is about GitRepo.is_valid_repo. We would use the\n        # latter to decide whether or not to call GitRepo() only for __init__ to\n        # then test the same things again. If we fail early we can save the\n        # additional test from outer scope.\n        self.path = path\n\n        # Note, that the following three path objects are used often and\n        # therefore are stored for performance. Path object creation comes with\n        # a cost. Most notably, this is used for validity checking of the\n        # repository.\n        _valid_repo = self.is_valid_git()\n\n        do_create = False\n        if create and not _valid_repo:\n            if repo is not None:\n                # `repo` passed with `create`, which doesn't make sense\n                raise TypeError(\"argument 'repo' must not be used with 'create'\")\n            do_create = True\n        else:\n            # Note: We used to call gitpy.Repo(path) here, which potentially\n            # raised NoSuchPathError or InvalidGitRepositoryError. This is\n            # used by callers of GitRepo.__init__() to detect whether we have a\n            # valid repo at `path`. Now, with switching to lazy loading property\n            # `repo`, we detect those cases without instantiating a\n            # gitpy.Repo().\n\n            if not exists(path):\n                raise NoSuchPathError(path)\n            if not _valid_repo:\n                raise InvalidGitRepositoryError(path)\n        # END Repo validity test\n\n        # So that we \"share\" control paths with git/git-annex\n        if ssh_manager:\n            ssh_manager.ensure_initialized()\n\n        # note: we may also want to distinguish between a path to the worktree\n        # and the actual repository\n\n        if git_opts is None:\n            git_opts = {}\n        if kwargs:\n            git_opts.update(kwargs)\n\n        self._cfg = None\n\n        if do_create:  # we figured it out earlier\n            from_cmdline = git_opts.pop('_from_cmdline_', [])\n            self.init(\n                sanity_checks=create_sanity_checks,\n                init_options=from_cmdline + to_options(**git_opts),\n            )\n\n        # with DryRunProtocol path might still not exist\n        if exists(self.path):\n            self.inode = os.stat(self.path).st_ino\n        else:\n            self.inode = None\n\n        if fake_dates:\n            self.configure_fake_dates()\n\n    @property\n    def bare(self):\n        \"\"\"Returns a bool indicating whether the repository is bare\n\n        Importantly, this is not reporting the configuration value\n        of 'core.bare', in order to be usable at a stage where a\n        Repo instance is not yet equipped with a ConfigManager.\n        Instead, it is testing whether the repository path and its\n        \"dot_git\" are identical. The value of 'core.bare' can be query\n        from the ConfigManager in a fully initialized instance.\n        \"\"\"\n        return self.pathobj == self.dot_git\n\n    @classmethod\n    def clone(cls, url, path, *args, clone_options=None, **kwargs):\n        \"\"\"Clone url into path\n\n        Provides workarounds for known issues (e.g.\n        https://github.com/datalad/datalad/issues/785)\n\n        Parameters\n        ----------\n        url : str\n        path : str\n        clone_options : dict or list\n          Arbitrary options that will be passed on to the underlying call to\n          `git-clone`. This may be a list of plain options or key-value pairs\n          that will be converted to a list of plain options with `to_options`.\n        expect_fail : bool\n          Whether expect that command might fail, so error should be logged then\n          at DEBUG level instead of ERROR\n        kwargs:\n          Passed to the Repo class constructor.\n        \"\"\"\n\n        if 'repo' in kwargs:\n            raise TypeError(\"argument 'repo' conflicts with cloning\")\n            # TODO: what about 'create'?\n\n        expect_fail = kwargs.pop('expect_fail', False)\n        # fail early on non-empty target:\n        from os import listdir\n        if exists(path) and listdir(path):\n            raise ValueError(\n                \"destination path '%s' already exists and is not an \"\n                \"empty directory.\" % path)\n        else:\n            # protect against cloning into existing and obviously dangling\n            # instance for that location\n            try:\n                del cls._unique_instances[path]\n            except KeyError:\n                # didn't exist - all fine\n                pass\n\n        # Massage URL\n        url_ri = RI(url) if not isinstance(url, RI) else url\n        if on_windows:\n            # When we're cloning from a local path on Windows, the URL at\n            # this point is platform-specific (e.g., \"..\\\\origin\"). According \n            # to Git clone's manpage, clone urls can't have backslashes.\n            # While Git does manage to clone a URL with backslashes, \n            # in the case of subdatasets cloned from relative paths it nevertheless\n            # messed up the resulting remote url, resulting in a mix of\n            # front and backslashes (see also gh-7180): \n            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\\\origin' \n            # Therefore, we're turning it to Posix now.\n            if isinstance(url_ri, PathRI):\n                url = Path(url).as_posix()\n                url_ri = PathRI(url)\n\n        else:\n            # if we are on windows, the local path of a URL\n            # would not end up being a proper local path and cloning\n            # would fail. Don't try to be smart and just pass the\n            # URL along unmodified\n\n            # try to get a local path from `url`:\n            try:\n                url = url_ri.localpath\n                url_ri = RI(url)\n            except ValueError:\n                pass\n\n        if is_ssh(url_ri):\n            ssh_manager.get_connection(url).open()\n        else:\n            if isinstance(url_ri, PathRI):\n                # expand user, because execution not going through a shell\n                # doesn't work well otherwise\n                new_url = os.path.expanduser(url)\n                if url != new_url:\n                    lgr.info(\"Expanded source path to %s from %s\", new_url, url)\n                    url = new_url\n\n        cmd = cls._git_cmd_prefix + ['clone', '--progress']\n        if clone_options:\n            if isinstance(clone_options, Mapping):\n                clone_options = to_options(**clone_options)\n            cmd.extend(clone_options)\n        cmd.extend([url, path])\n\n        fix_annex = None\n        ntries = 5  # 3 is not enough for robust workaround\n        for trial in range(ntries):\n            try:\n                lgr.debug(\"Git clone from %s to %s\", url, path)\n\n                res = GitWitlessRunner().run(cmd, protocol=GitProgress)\n                # fish out non-critical warnings by git-clone\n                # (empty repo clone, etc.), all other content is logged\n                # by the progress helper to 'debug'\n                for errline in res['stderr'].splitlines():\n                    if errline.startswith('warning:'):\n                        lgr.warning(errline[8:].strip())\n                lgr.debug(\"Git clone completed\")\n                break\n            except CommandError as e:\n                # log here but let caller decide what to do\n                ce = CapturedException(e)\n                str_e = str(e)\n                # see https://github.com/datalad/datalad/issues/785\n                if re.search(\"Request for .*aborted.*Unable to find\", str_e,\n                             re.DOTALL) \\\n                        and trial < ntries - 1:\n                    lgr.info(\n                        \"Hit a known issue with Git (see GH#785). Trial #%d, \"\n                        \"retrying\",\n                        trial)\n                    continue\n                    #(lgr.debug if expect_fail else lgr.error)(e_str)\n\n                if \"Clone succeeded, but checkout failed.\" in str_e:\n                    fix_annex = ce\n                    break\n\n                raise\n\n        # get ourselves a repository instance\n        gr = cls(path, *args, **kwargs)\n        if fix_annex:\n            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then\n            # would need to import our own subclass\n            if hasattr(gr, 'is_valid_annex'):\n                lgr.warning(\"Experienced issues while cloning. \"\n                            \"Trying to fix it, using git-annex-fsck.\")\n                if not gr.is_initialized():\n                    gr._init()\n                gr.fsck()\n            else:\n                lgr.warning(\"Experienced issues while cloning: %s\", fix_annex)\n        # ensure that Git doesn't mangle relative paths into obscure absolute\n        # paths: https://github.com/datalad/datalad/issues/3538\n        if isinstance(url_ri, PathRI):\n            url_path = Path(url)\n            if not url_path.is_absolute():\n                # get git-created path\n                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'\n                git_url = gr.config.get(remote_url)\n                if Path(git_url).is_absolute():\n                    # Git created an absolute path from a relative URL.\n                    git_url = op.relpath(git_url, gr.path)\n                path = Path(git_url)\n                # always in POSIX even on Windows\n                path = path.as_posix()\n                gr.config.set(remote_url, path,\n                              scope='local', force=True)\n        return gr\n\n    # Note: __del__ shouldn't be needed anymore as we switched to\n    #       `weakref.finalize`.\n    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods\n    #\n    #       Keeping both methods and this comment around as a reminder to not\n    #       use __del__, if we figure there's a need for cleanup in the future.\n    #\n    # def __del__(self):\n    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird\n    #     # stalls etc\n    #     self._cfg = None\n\n    def is_valid_git(self):\n        \"\"\"Returns whether the underlying repository appears to be still valid\n\n        Note, that this almost identical to the classmethod is_valid_repo().\n        However, if we are testing an existing instance, we can save Path object\n        creations. Since this testing is done a lot, this is relevant. Creation\n        of the Path objects in is_valid_repo() takes nearly half the time of the\n        entire function.\n\n        Also note, that this method is bound to an instance but still\n        class-dependent, meaning that a subclass cannot simply overwrite it.\n        This is particularly important for the call from within __init__(),\n        which in turn is called by the subclasses' __init__. Using an overwrite\n        would lead to the wrong thing being called.\n        \"\"\"\n        return self.is_valid()\n\n    @classmethod\n    def is_valid_repo(cls, path):\n        \"\"\"Returns if a given path points to a git repository\"\"\"\n        return cls.is_valid(path)\n\n    @staticmethod\n    def get_git_dir(repo):\n        \"\"\"figure out a repo's gitdir\n\n        '.git' might be a  directory, a symlink or a file\n\n        Note\n        ----\n        This method is likely to get deprecated, please use GitRepo.dot_git instead!\n        That one's not static, but it's cheaper and you should avoid\n        not having an instance of a repo you're working on anyway.\n        Note, that the property in opposition to this method returns an absolute path.\n\n\n        Parameters\n        ----------\n        repo: path or Repo instance\n          currently expected to be the repos base dir\n\n        Returns\n        -------\n        str\n          relative path to the repo's git dir; So, default would be \".git\"\n        \"\"\"\n        if isinstance(repo, GitRepo):\n            return str(repo.dot_git)\n        pathobj = Path(repo)\n        dot_git = _get_dot_git(pathobj, ok_missing=False)\n        try:\n            dot_git = dot_git.relative_to(pathobj)\n        except ValueError:\n            # is not a subpath, return as is\n            lgr.debug(\"Path %r is not subpath of %r\", dot_git, pathobj)\n        return str(dot_git)\n\n    @property\n    def config(self):\n        # just proxy the core repo APIs property for backward-compatibility\n        return self.cfg\n\n    def is_with_annex(self):\n        \"\"\"Report if GitRepo (assumed) has (remotes with) a git-annex branch\n        \"\"\"\n        return any(\n            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])\n        )\n\n    @classmethod\n    def get_toppath(cls, path, follow_up=True, git_options=None):\n        \"\"\"Return top-level of a repository given the path.\n\n        Parameters\n        -----------\n        follow_up : bool\n          If path has symlinks -- they get resolved by git.  If follow_up is\n          True, we will follow original path up until we hit the same resolved\n          path.  If no such path found, resolved one would be returned.\n        git_options: list of str\n          options to be passed to the git rev-parse call\n\n        Return None if no parent directory contains a git repository.\n        \"\"\"\n        cmd = ['git']\n        if git_options:\n            cmd.extend(git_options)\n        cmd += [\"rev-parse\", \"--show-toplevel\"]\n        try:\n            out = GitWitlessRunner(cwd=path).run(\n                cmd, protocol=StdOutErrCapture)\n            toppath = out['stdout'].rstrip('\\n\\r')\n        except CommandError:\n            return None\n        except OSError:\n            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,\n                                          git_options=git_options)\n\n        # normalize the report, because, e.g. on windows it can come out\n        # with improper directory separators (C:/Users/datalad)\n        toppath = str(Path(toppath))\n\n        if follow_up:\n            path_ = path\n            path_prev = \"\"\n            while path_ and path_ != path_prev:  # on top /.. = /\n                if str(Path(path_).resolve()) == toppath:\n                    toppath = path_\n                    break\n                path_prev = path_\n                path_ = dirname(path_)\n\n        return toppath\n\n    @normalize_paths\n    def add(self, files, git=True, git_options=None, update=False):\n        \"\"\"Adds file(s) to the repository.\n\n        Parameters\n        ----------\n        files: list\n          list of paths to add\n        git: bool\n          somewhat ugly construction to be compatible with AnnexRepo.add();\n          has to be always true.\n        update: bool\n          --update option for git-add. From git's manpage:\n           Update the index just where it already has an entry matching\n           <pathspec>. This removes as well as modifies index entries to match\n           the working tree, but adds no new files.\n\n           If no <pathspec> is given when --update option is used, all tracked\n           files in the entire working tree are updated (old versions of Git\n           used to limit the update to the current directory and its\n           subdirectories).\n\n        Returns\n        -------\n        list\n          Of status dicts.\n        \"\"\"\n        # under all circumstances call this class' add_ (otherwise\n        # AnnexRepo.add would go into a loop\n        return list(GitRepo.add_(self, files, git=git, git_options=git_options,\n                    update=update))\n\n    def add_(self, files, git=True, git_options=None, update=False):\n        \"\"\"Like `add`, but returns a generator\"\"\"\n        # TODO: git_options is used as options for the git-add here,\n        # instead of options to the git executable => rename for consistency\n\n        if not git:\n            lgr.warning(\n                'GitRepo.add() called with git=%s, this should not happen',\n                git)\n            git = True\n\n        # there is no other way then to collect all files into a list\n        # at this point, because we need to pass them at once to a single\n        # `git add` call\n        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]\n\n        if not (files or git_options or update):\n            # wondering why just a warning? in cmdline this is also not an error\n            lgr.warning(\"add was called with empty file list and no options.\")\n            return\n\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.gitaddtoannex to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.gitaddtoannex=false', 'add'] +\n                ensure_list(git_options) +\n                to_options(update=update) + ['--verbose'],\n                files=files,\n                pathspec_from_file=True,\n                read_only=False,\n            )\n            # get all the entries\n            for o in self._process_git_get_output(*add_out):\n                yield o\n            # Note: as opposed to git cmdline, force is True by default in\n            #       gitpython, which would lead to add things, that are\n            #       ignored or excluded otherwise\n            # 2. Note: There is an issue with globbing (like adding '.'),\n            #       which apparently doesn't care for 'force' and therefore\n            #       adds '.git/...'. May be it's expanded at the wrong\n            #       point in time or sth. like that.\n            # For now, use direct call to git add.\n            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,\n            #                      force=False)\n            # TODO: May be make use of 'fprogress'-option to indicate\n            # progress\n            # But then, we don't have it for git-annex add, anyway.\n            #\n            # TODO: Is write=True a reasonable way to do it?\n            # May be should not write until success of operation is\n            # confirmed?\n            # What's best in case of a list of files?\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n        # Make sure return value from GitRepo is consistent with AnnexRepo\n        # currently simulating similar return value, assuming success\n        # for all files:\n        # TODO: Make return values consistent across both *Repo classes!\n        return\n\n    @staticmethod\n    def _process_git_get_output(stdout, stderr=None):\n        \"\"\"Given both outputs (stderr is ignored atm) of git add - process it\n\n        Primarily to centralize handling in both indirect annex and direct\n        modes when ran through proxy\n        \"\"\"\n        return [{u'file': f, u'success': True}\n                for f in re.findall(\"'(.*)'[\\n$]\", ensure_unicode(stdout))]\n\n    @normalize_paths(match_return_type=False)\n    def remove(self, files, recursive=False, **kwargs):\n        \"\"\"Remove files.\n\n        Calls git-rm.\n\n        Parameters\n        ----------\n        files: list of str\n          list of paths to remove\n        recursive: False\n          whether to allow recursive removal from subdirectories\n        kwargs:\n          see `__init__`\n\n        Returns\n        -------\n        [str]\n          list of successfully removed files.\n        \"\"\"\n        if recursive:\n            kwargs['r'] = True\n\n        # the name is chosen badly, but the purpose is to make sure that\n        # any pending operations actually manifest themselves in the Git repo\n        # on disk (in case of an AnnexRepo, it could be pending batch\n        # processes that need closing)\n        self.precommit()\n\n        # output per removed file is expected to be \"rm 'PATH'\":\n        return [\n            line.strip()[4:-1]\n            for line in self.call_git_items_(\n                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)\n        ]\n\n    def precommit(self):\n        \"\"\"Perform pre-commit maintenance tasks\n        \"\"\"\n        # we used to clean up GitPython here\n        pass\n\n    @staticmethod\n    def _get_prefixed_commit_msg(msg):\n        DATALAD_PREFIX = \"[DATALAD]\"\n        return DATALAD_PREFIX if not msg else \"%s %s\" % (DATALAD_PREFIX, msg)\n\n    def configure_fake_dates(self):\n        \"\"\"Configure repository to use fake dates.\n        \"\"\"\n        lgr.debug(\"Enabling fake dates\")\n        self.config.set(\"datalad.fake-dates\", \"true\")\n\n    @property\n    def fake_dates_enabled(self):\n        \"\"\"Is the repository configured to use fake dates?\n        \"\"\"\n        # this turned into a private property of the CoreGitRepo\n        return self._fake_dates_enabled\n\n    def add_fake_dates(self, env):\n        # was renamed in CoreGitRepo\n        return self.add_fake_dates_to_env(env)\n\n    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,\n               files=None, date=None, index_file=None):\n        \"\"\"Commit changes to git.\n\n        Parameters\n        ----------\n        msg: str, optional\n          commit-message\n        options: list of str, optional\n          cmdline options for git-commit\n        _datalad_msg: bool, optional\n          To signal that commit is automated commit by datalad, so\n          it would carry the [DATALAD] prefix\n        careless: bool, optional\n          if False, raise when there's nothing actually committed;\n          if True, don't care\n        files: list of str, optional\n          path(s) to commit\n        date: str, optional\n          Date in one of the formats git understands\n        index_file: str, optional\n          An alternative index to use\n        \"\"\"\n\n        self.precommit()\n\n        # assemble commandline\n        cmd = ['commit']\n        options = ensure_list(options)\n\n        if date:\n            options += [\"--date\", date]\n\n        orig_msg = msg\n        if not msg:\n            if '--amend' in options:\n                if '--no-edit' not in options:\n                    # don't overwrite old commit message with our default\n                    # message by default, but re-use old one. In other words:\n                    # Make --no-edit the default:\n                    options += [\"--no-edit\"]\n            else:\n                msg = 'Recorded changes'\n                _datalad_msg = True\n\n        if _datalad_msg:\n            msg = self._get_prefixed_commit_msg(msg)\n\n        if msg:\n            options += [\"-m\", msg]\n        cmd.extend(options)\n\n        # set up env for commit\n        env = self.add_fake_dates(None) \\\n            if self.fake_dates_enabled else os.environ.copy()\n        if index_file:\n            env['GIT_INDEX_FILE'] = index_file\n\n        lgr.debug(\"Committing via direct call of git: %s\", cmd)\n\n        prev_sha = self.get_hexsha()\n\n        # Old code was doing clever --amend'ing of chunked series of commits manually\n        # here, but with pathspec_from_file it is no longer needed.\n        # store pre-commit state to be able to check if anything was committed\n        try:\n            # Note: call_git operates via joining call_git_items_ and that one wipes out\n            # .stdout from exception and collects/repopulates stderr only. Let's use\n            # _call_git which returns both outputs and collects/re-populates both stdout\n            # **and** stderr\n            _ = self._call_git(\n                    cmd,\n                    files=files,\n                    env=env,\n                    pathspec_from_file=True,\n            )\n        except CommandError as e:\n            # real errors first\n            if \"did not match any file(s) known to git\" in e.stderr:\n                raise FileNotInRepositoryError(\n                    cmd=e.cmd,\n                    msg=\"File(s) unknown to git\",\n                    code=e.code,\n                    filename=linesep.join([\n                        l for l in e.stderr.splitlines()\n                        if l.startswith(\"error: pathspec\")\n                    ])\n                )\n            # behavior choices now\n            elif not careless:\n                # not willing to compromise at all\n                raise\n            elif 'nothing to commit' in e.stdout:\n                lgr.debug(\"nothing to commit in %s. Ignored.\", self)\n            elif 'no changes added to commit' in e.stdout or \\\n                    'nothing added to commit' in e.stdout:\n                lgr.debug(\"no changes added to commit in %s. Ignored.\", self)\n            else:\n                raise\n        if orig_msg \\\n                or '--dry-run' in cmd \\\n                or prev_sha == self.get_hexsha() \\\n                or ('--amend' in cmd and '--no-edit' in cmd) \\\n                or (not is_interactive()) \\\n                or self.config.obtain('datalad.save.no-message') != 'interactive':\n            # we had a message given, or nothing was committed, or prev. commit\n            # was amended, or we are not connected to a terminal, or no\n            # interactive message input is desired:\n            # we can go home\n            return\n\n        # handle interactive message entry by running another `git-commit`\n        self._git_runner.run(\n            cmd + ['--amend', '--edit'],\n            protocol=NoCapture,\n            stdin=None,\n            env=env,\n        )\n\n    # TODO usage is only in the tests, consider making a test helper and\n    # remove from GitRepo API\n    def get_indexed_files(self):\n        \"\"\"Get a list of files in git's index\n\n        Returns\n        -------\n        list\n            list of paths rooting in git's base dir\n        \"\"\"\n\n        return [\n            str(r.relative_to(self.pathobj))\n            for r in self.get_content_info(\n                paths=None, ref=None, untracked='no')\n        ]\n\n    def format_commit(self, fmt, commitish=None):\n        \"\"\"Return `git show` output for `commitish`.\n\n        Parameters\n        ----------\n        fmt : str\n            A format string accepted by `git show`.\n        commitish: str, optional\n          Any commit identifier (defaults to \"HEAD\").\n\n        Returns\n        -------\n        str or, if there are not commits yet, None.\n        \"\"\"\n        # use git-log and not git-show due to faster performance with\n        # complex commits (e.g. octopus merges)\n        # https://github.com/datalad/datalad/issues/4801\n        cmd = ['log', '-1', '-z', '--format=' + fmt]\n        if commitish is not None:\n            cmd.append(commitish + \"^{commit}\")\n        # make sure Git takes our argument as a revision\n        cmd.append('--')\n        try:\n            stdout = self.call_git(\n                cmd, expect_stderr=True, expect_fail=True,\n                read_only=True)\n        except CommandError as e:\n            if 'bad revision' in e.stderr:\n                raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n            elif 'does not have any commits yet' in e.stderr:\n                return None\n            else:\n                raise e\n        # This trailing null is coming from the -z above, which avoids the\n        # newline that Git would append to the output. We could drop -z and\n        # strip the newline directly, but then we'd have to worry about\n        # compatibility across platforms.\n        return stdout.rsplit(\"\\0\", 1)[0]\n\n    def get_hexsha(self, commitish=None, short=False):\n        \"\"\"Return a hexsha for a given commitish.\n\n        Parameters\n        ----------\n        commitish : str, optional\n          Any identifier that refers to a commit (defaults to \"HEAD\").\n        short : bool, optional\n          Return the abbreviated form of the hexsha.\n\n        Returns\n        -------\n        str or, if no commitish was given and there are no commits yet, None.\n\n        Raises\n        ------\n        ValueError\n          If a commitish was given, but no corresponding commit could be\n          determined.\n        \"\"\"\n        # use --quiet because the 'Needed a single revision' error message\n        # that is the result of running this in a repo with no commits\n        # isn't useful to report\n        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(\n            commitish if commitish else 'HEAD')\n        ]\n        if short:\n            cmd.append('--short')\n        try:\n            return self.call_git_oneline(cmd, read_only=True)\n        except CommandError as e:\n            if commitish is None:\n                return None\n            raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n\n    @normalize_paths(match_return_type=False)\n    def get_last_commit_hexsha(self, files):\n        \"\"\"Return the hash of the last commit the modified any of the given\n        paths\"\"\"\n        try:\n            commit = self.call_git(\n                ['rev-list', '-n1', 'HEAD'],\n                files=files,\n                expect_fail=True,\n                read_only=True,\n            )\n            commit = commit.strip()\n            return commit if commit else None\n        except CommandError:\n            if self.get_hexsha() is None:\n                # unborn branch, don't freak out\n                return None\n            raise\n\n    def get_revisions(self, revrange=None, fmt=\"%H\", options=None):\n        \"\"\"Return list of revisions in `revrange`.\n\n        Parameters\n        ----------\n        revrange : str or list of str or None, optional\n            Revisions or revision ranges to walk. If None, revision defaults to\n            HEAD unless a revision-modifying option like `--all` or\n            `--branches` is included in `options`.\n        fmt : string, optional\n            Format accepted by `--format` option of `git log`. This should not\n            contain new lines because the output is split on new lines.\n        options : list of str, optional\n            Options to pass to `git log`.  This should not include `--format`.\n\n        Returns\n        -------\n        List of revisions (str), formatted according to `fmt`.\n        \"\"\"\n        if revrange is None:\n            revrange = []\n        elif isinstance(revrange, str):\n            revrange = [revrange]\n\n        cmd = [\"log\", \"--format={}\".format(fmt)]\n        cmd.extend((options or []) + revrange + [\"--\"])\n        try:\n            stdout = self.call_git(cmd, expect_fail=True, read_only=True)\n        except CommandError as e:\n            if \"does not have any commits\" in e.stderr:\n                return []\n            raise\n        return stdout.splitlines()\n\n    def commit_exists(self, commitish):\n        \"\"\"Does `commitish` exist in the repo?\n\n        Parameters\n        ----------\n        commitish : str\n            A commit or an object that can be dereferenced to one.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        # Note: The peeling operator \"^{commit}\" is required so that rev-parse\n        # doesn't succeed if passed a full hexsha that is valid but doesn't\n        # exist.\n        return self.call_git_success(\n            [\"rev-parse\", \"--verify\", commitish + \"^{commit}\"],\n            read_only=True,\n        )\n\n    def get_merge_base(self, commitishes):\n        \"\"\"Get a merge base hexsha\n\n        Parameters\n        ----------\n        commitishes: str or list of str\n          List of commitishes (branches, hexshas, etc) to determine the merge\n          base of. If a single value provided, returns merge_base with the\n          current branch.\n\n        Returns\n        -------\n        str or None\n          If no merge-base for given commits, or specified treeish doesn't\n          exist, None returned\n        \"\"\"\n        if isinstance(commitishes, str):\n            commitishes = [commitishes]\n        if not commitishes:\n            raise ValueError(\"Provide at least a single value\")\n        elif len(commitishes) == 1:\n            commitishes = commitishes + [self.get_active_branch()]\n\n        try:\n            base = self.call_git_oneline(['merge-base'] + commitishes,\n                                         read_only=True)\n        except CommandError as exc:\n            if exc.code == 1 and not (exc.stdout or exc.stderr):\n                # No merge base was found (unrelated commits).\n                return None\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                return None\n            raise\n\n        return base\n\n    def is_ancestor(self, reva, revb):\n        \"\"\"Is `reva` an ancestor of `revb`?\n\n        Parameters\n        ----------\n        reva, revb : str\n            Revisions.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        return self.call_git_success(\n            [\"merge-base\", \"--is-ancestor\", reva, revb],\n            read_only=True)\n\n    def get_commit_date(self, branch=None, date='authored'):\n        \"\"\"Get the date stamp of the last commit (in a branch or head otherwise)\n\n        Parameters\n        ----------\n        date: {'authored', 'committed'}\n          Which date to return.  \"authored\" will be the date shown by \"git show\"\n          and the one possibly specified via --date to `git commit`\n\n        Returns\n        -------\n        int or None\n          None if no commit\n        \"\"\"\n        if date == 'committed':\n            format = '%ct'\n        elif date == 'authored':\n            format = '%at'\n        else:\n            raise ValueError('unknow date type: {}'.format(date))\n        d = self.format_commit(format, commitish=branch)\n        return int(d) if d else None\n\n    def get_active_branch(self):\n        \"\"\"Get the name of the active branch\n\n        Returns\n        -------\n        str or None\n          Returns None if there is no active branch, i.e. detached HEAD,\n          and the branch name otherwise.\n        \"\"\"\n        try:\n            out = self.call_git([\"symbolic-ref\", \"HEAD\"], expect_fail=True,\n                                read_only=True)\n        except CommandError as e:\n            if 'HEAD is not a symbolic ref' in e.stderr:\n                lgr.debug(\"detached HEAD in %s\", self)\n                return None\n            else:\n                raise e\n        return out.strip()[11:]  # strip refs/heads/\n\n    def get_corresponding_branch(self, branch=None):\n        \"\"\"Always returns None, a plain GitRepo has no managed branches\"\"\"\n        return None\n\n    def get_branches(self):\n        \"\"\"Get all branches of the repo.\n\n        Returns\n        -------\n        [str]\n            Names of all branches of this repository.\n        \"\"\"\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')\n        ]\n\n    def get_remote_branches(self):\n        \"\"\"Get all branches of all remotes of the repo.\n\n        Returns\n        -----------\n        [str]\n            Names of all remote branches.\n        \"\"\"\n        # TODO: Reconsider melting with get_branches()\n\n        # TODO: treat entries like this: origin/HEAD -> origin/master'\n        # currently this is done in collection\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')\n        ]\n\n    def get_remotes(self, with_urls_only=False):\n        \"\"\"Get known remotes of the repository\n\n        Parameters\n        ----------\n        with_urls_only : bool, optional\n          return only remotes which have urls\n\n        Returns\n        -------\n        remotes : list of str\n          List of names of the remotes\n        \"\"\"\n        from datalad.utils import unique\n\n        self.config.reload()\n        remotes = unique([x[7:] for x in self.config.sections()\n                          if x.startswith(\"remote.\")])\n\n        if with_urls_only:\n            remotes = [\n                r for r in remotes\n                if self.config.get('remote.%s.url' % r)\n            ]\n        return remotes\n\n    # TODO this is practically unused outside the tests, consider turning\n    # into a test helper and trim from the API\n    def get_files(self, branch=None):\n        \"\"\"Get a list of files in git.\n\n        Lists the files in the (remote) branch.\n\n        Parameters\n        ----------\n        branch: str\n          Name of the branch to query. Default: active branch.\n\n        Returns\n        -------\n        [str]\n          list of files.\n        \"\"\"\n        return [\n            str(p.relative_to(self.pathobj))\n            for p in self.get_content_info(\n                paths=None, ref=branch, untracked='no')\n            ]\n\n    def add_remote(self, name, url, options=None):\n        \"\"\"Register remote pointing to a url\n        \"\"\"\n        cmd = ['remote', 'add']\n        if options:\n            cmd += options\n        cmd += [name, url]\n\n        # for historical reasons this method returns stdout and\n        # stderr, keeping that for now\n        result = self._call_git(cmd)\n        self.config.reload()\n        return result\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n\n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n                raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\n            else:\n                raise e\n\n        # config.reload necessary, because the associated remote config\n        # will vanish\n        self.config.reload()\n        return\n\n    def _maybe_open_ssh_connection(self, remote, prefer_push=True):\n        \"\"\"Open connection if `remote` has an SSH URL.\n\n        Doing so enables SSH caching, preventing datalad-sshrun subprocesses\n        from opening (and then closing) their own.\n\n        Parameters\n        ----------\n        remote : str\n        prefer_push : bool, optional\n            Use `remote.<remote>.pushurl` if there is one, falling back to\n            `remote.<remote>.url`.\n        \"\"\"\n        if remote:\n            url = None\n            if prefer_push:\n                url = self.get_remote_url(remote, push=True)\n            url = url or self.get_remote_url(remote)\n            if url and is_ssh(url):\n                ssh_manager.get_connection(url).open()\n\n    def update_remote(self, name=None, verbose=False):\n        \"\"\"\n        \"\"\"\n        options = [\"-v\"] if verbose else []\n        self._maybe_open_ssh_connection(name)\n        name = [name] if name else []\n        self.call_git(\n            ['remote'] + name + ['update'] + options,\n            expect_stderr=True\n        )\n\n    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,\n              **kwargs):\n        \"\"\"Fetches changes from a remote (or all remotes).\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to fetch from. If no remote is given and\n          `all_` is not set, the tracking branch is fetched.\n        refspec : str or list, optional\n          refspec(s) to fetch.\n        all_ : bool, optional\n          fetch all remotes (and all of their branches).\n          Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-fetch.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-fetch.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        return list(\n            self.fetch_(\n                remote=remote,\n                refspec=refspec,\n                all_=all_,\n                git_options=git_options,\n            )\n        )\n\n    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `fetch`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],\n            action='fetch',\n            urlvars=('remote.{}.url', 'remote.{}.url'),\n            protocol=GitProgress,\n            info_cls=FetchInfo,\n            info_from='stderr',\n            add_remote=False,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def push(self, remote=None, refspec=None, all_remotes=False,\n             all_=False, git_options=None, **kwargs):\n        \"\"\"Push changes to a remote (or all remotes).\n\n        If remote and refspec are specified, and remote has\n        `remote.{remote}.datalad-push-default-first` configuration variable\n        set (e.g. by `create-sibling-github`), we will first push the first\n        refspec separately to possibly ensure that the first refspec is chosen\n        by remote as the \"default branch\".\n        See https://github.com/datalad/datalad/issues/4997\n        Upon successful push if this variable was set in the local git config,\n        we unset it, so subsequent pushes would proceed normally.\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to push to. If no remote is given and\n          `all_` is not set, the tracking branch is pushed.\n        refspec : str or list, optional\n          refspec(s) to push.\n        all_ : bool, optional\n          push to all remotes. Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-push.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-push.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        if all_remotes:\n            # be nice to the elderly\n            all_ = True\n\n        push_refspecs = [refspec]\n        cfg = self.config  # shortcut\n        cfg_push_var = \"remote.{}.datalad-push-default-first\".format(remote)\n        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):\n            refspec = ensure_list(refspec)\n            lgr.debug(\"As indicated by %s pushing first refspec %s separately first\",\n                      cfg_push_var, refspec[0])\n            push_refspecs = [[refspec[0]], refspec[1:]]\n\n        push_res = []\n        for refspecs in push_refspecs:\n            push_res.extend(\n                self.push_(\n                    remote=remote,\n                    refspec=refspecs,\n                    all_=all_,\n                    git_options=git_options,\n                )\n            )\n        # note: above push_ should raise exception if errors out\n        if '--dry-run' not in git_options \\\n            and cfg.get_from_source('local', cfg_push_var) is not None:\n            lgr.debug(\"Removing %s variable from local git config after successful push\", cfg_push_var)\n            cfg.unset(cfg_push_var, 'local')\n        return push_res\n\n    def push_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `push`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],\n            action='push',\n            urlvars=('remote.{}.pushurl', 'remote.{}.url'),\n            protocol=StdOutCaptureWithGitProgress,\n            info_cls=PushInfo,\n            info_from='stdout',\n            add_remote=True,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def _fetch_push_helper(\n            self,\n            base_cmd,     # arg list\n            action,       # label fetch|push\n            urlvars,      # variables to query for URLs\n            protocol,     # processor for output\n            info_cls,     # Push|FetchInfo\n            info_from,    # stdout, stderr\n            add_remote,   # whether to add a 'remote' field to the info dict\n            remote=None, refspec=None, all_=False, git_options=None):\n\n        git_options = ensure_list(git_options)\n\n        cmd = base_cmd + git_options\n\n        if remote is None:\n            if refspec:\n                # conflicts with using tracking branch or push all remotes\n                # For now: Just fail.\n                # TODO: May be check whether it fits to tracking branch\n                raise ValueError(\n                    \"refspec specified without a remote. ({})\".format(refspec))\n            if all_:\n                remotes_to_process = self.get_remotes(with_urls_only=True)\n            else:\n                # No explicit remote to fetch.\n                # => get tracking branch:\n                tb_remote, refspec = self.get_tracking_branch()\n                if tb_remote is not None:\n                    remotes_to_process = [tb_remote]\n                else:\n                    # No remote, no tracking branch\n                    # => fail\n                    raise ValueError(\n                        \"Neither a remote is specified to {} \"\n                        \"from nor a tracking branch is set up.\".format(action))\n        else:\n            if all_:\n                raise ValueError(\n                    \"Option 'all_' conflicts with specified remote \"\n                    \"'{}'.\".format(remote))\n            remotes_to_process = [remote]\n\n        if refspec:\n            # prep for appending to cmd\n            refspec = ensure_list(refspec)\n\n        # no need for progress report, when there is just one remote\n        log_remote_progress = len(remotes_to_process) > 1\n        if log_remote_progress:\n            pbar_id = '{}remotes-{}'.format(action, id(self))\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start %sing remotes for %s', action, self,\n                total=len(remotes_to_process),\n                label=action.capitalize(),\n                unit=' Remotes',\n            )\n        try:\n            for remote in remotes_to_process:\n                r_cmd = cmd + [remote]\n                if refspec:\n                    r_cmd += refspec\n\n                if log_remote_progress:\n                    log_progress(\n                        lgr.info,\n                        pbar_id,\n                        '{}ing remote %s'.format(action.capitalize()),\n                        remote,\n                        update=1,\n                        increment=True,\n                    )\n                # best effort to enable SSH connection caching\n                url = self.config.get(\n                    # make two attempts to get a URL\n                    urlvars[0].format(remote),\n                    self.config.get(\n                        urlvars[1].format(remote),\n                        None)\n                )\n                if url and is_ssh(url):\n                    ssh_manager.get_connection(url).open()\n                try:\n                    out = self._git_runner.run(\n                        r_cmd,\n                        protocol=protocol,\n                    )\n                    output = out[info_from] or ''\n                except CommandError as e:\n                    output = None\n                    # intercept some errors that we express as an error report\n                    # in the info dicts\n                    if re.match(\n                            '.*^error: failed to (push|fetch) some refs',\n                            e.stderr,\n                            re.DOTALL | re.MULTILINE):\n                        output = getattr(e, info_from)\n                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()\n                                          if l.startswith('hint: ')])\n                        if output is None:\n                            output = ''\n                    if not output:\n                        raise\n\n                for line in output.splitlines():\n                    try:\n                        # push info doesn't identify a remote, add it here\n                        pi = info_cls._from_line(line)\n                        if add_remote:\n                            pi['remote'] = remote\n                        # There were errors, but Git provided hints\n                        if 'error' in pi['operations']:\n                            pi['hints'] = hints or None\n                        yield pi\n                    except Exception:\n                        # it is not progress and no push info\n                        # don't hide it completely\n                        lgr.debug('git-%s reported: %s', action, line)\n        finally:\n            if log_remote_progress:\n                log_progress(\n                    lgr.info,\n                    pbar_id,\n                    'Finished %sing remotes for %s', action, self,\n                )\n\n    def get_remote_url(self, name, push=False):\n        \"\"\"Get the url of a remote.\n\n        Reads the configuration of remote `name` and returns its url or None,\n        if there is no url configured.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        push: bool\n          if True, get the pushurl instead of the fetch url.\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        return self.config.get(var, None)\n\n    def set_remote_url(self, name, url, push=False):\n        \"\"\"Set the URL a remote is pointing to\n\n        Sets the URL of the remote `name`. Requires the remote to already exist.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        url: str\n        push: bool\n          if True, set the push URL, otherwise the fetch URL\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        self.config.set(var, url, scope='local', reload=True)\n\n    def get_branch_commits_(self, branch=None, limit=None, stop=None):\n        \"\"\"Return commit hexshas for a branch\n\n        Parameters\n        ----------\n        branch: str, optional\n          If not provided, assumes current branch\n        limit: None | 'left-only', optional\n          Limit which commits to report.  If None -- all commits (merged or not),\n          if 'left-only' -- only the commits from the left side of the tree upon\n          merges\n        stop: str, optional\n          hexsha of the commit at which stop reporting (matched one is not\n          reported either)\n\n        Yields\n        ------\n        str\n        \"\"\"\n        cmd = ['rev-list']\n        if limit == 'left-only':\n            cmd.append('--left-only')\n        if not branch:\n            branch = self.get_active_branch()\n        cmd.append(branch)\n        # and trailing -- marker to make sure that Git never confused the branch\n        # with a potentially existing directory of the same name\n        cmd.append('--')\n        for r in self.call_git_items_(cmd):\n            if stop and stop == r:\n                return\n            yield r\n\n    def checkout(self, name, options=None):\n        \"\"\"\n        \"\"\"\n        # TODO: May be check for the need of -b options herein?\n        cmd = ['checkout']\n        if options:\n            cmd += options\n        cmd += [str(name)]\n\n        self.call_git(cmd, expect_stderr=True)\n        # checkout can change committed config, or create branch config\n        self.config.reload()\n\n    # TODO: Before implementing annex merge, find usages and check for a needed\n    # change to call super().merge\n    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):\n        if options is None:\n            options = []\n        if msg:\n            options = options + [\"-m\", msg]\n        options += ['--allow-unrelated-histories']\n        self.call_git(\n            ['merge'] + options + [name],\n            **kwargs\n        )\n\n    def remove_branch(self, branch):\n        self.call_git(['branch', '-D', branch])\n\n    def cherry_pick(self, commit):\n        \"\"\"Cherry pick `commit` to the current branch.\n\n        Parameters\n        ----------\n        commit : str\n            A single commit.\n        \"\"\"\n        self.call_git([\"cherry-pick\", commit])\n\n    @property\n    def dirty(self):\n        \"\"\"Is the repository dirty?\n\n        Note: This provides a quick answer when you simply want to know if\n        there are any untracked changes or modifications in this repository or\n        its submodules. For finer-grained control and more detailed reporting,\n        use status() instead.\n        \"\"\"\n        stdout = self.call_git(\n            [\"status\", \"--porcelain\",\n             # Ensure the result isn't influenced by status.showUntrackedFiles.\n             \"--untracked-files=normal\",\n             # Ensure the result isn't influenced by diff.ignoreSubmodules.\n             \"--ignore-submodules=none\"])\n        if bool(stdout.strip()):\n            # The quick `git status`-based check can give a different answer\n            # than `datalad status` for submodules on an adjusted branch.\n            st = self.diffstatus(fr=\"HEAD\" if self.get_hexsha() else None,\n                                 to=None, untracked=\"normal\")\n            return any(r.get(\"state\") != \"clean\" for r in st.values())\n        return False\n\n    @property\n    def untracked_files(self):\n        \"\"\"Legacy interface, do not use! Use the status() method instead.\n\n        Despite its name, it also reports on untracked datasets, and\n        yields their names with trailing path separators.\n        \"\"\"\n        return [\n            '{}{}'.format(\n                str(p.relative_to(self.pathobj)),\n                os.sep if props['type'] != 'file' else ''\n            )\n            for p, props in self.status(\n                    untracked='all', eval_submodule_state='no').items()\n            if props.get('state', None) == 'untracked'\n        ]\n\n    def gc(self, allow_background=False, auto=False):\n        \"\"\"Perform house keeping (garbage collection, repacking)\"\"\"\n        cmd_options = []\n        if not allow_background:\n            cmd_options += ['-c', 'gc.autodetach=0']\n        cmd_options += ['gc', '--aggressive']\n        if auto:\n            cmd_options += ['--auto']\n        self.call_git(cmd_options)\n\n    def _parse_gitmodules(self):\n        # TODO read .gitconfig from Git blob?\n        gitmodules = self.pathobj / '.gitmodules'\n        if not gitmodules.exists():\n            return {}\n        # pull out file content\n        out = self.call_git(\n            ['config', '-z', '-l', '--file', '.gitmodules'],\n            read_only=True)\n        # abuse our config parser\n        # disable multi-value report, because we could not deal with them\n        # anyways, and they should not appear in a normal .gitmodules file\n        # but could easily appear when duplicates are included. In this case,\n        # we better not crash\n        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)\n        mods = {}\n        for k, v in db.items():\n            if not k.startswith('submodule.'):\n                # we don't know what this is\n                lgr.warning(\"Skip unrecognized .gitmodule specification: %s=%s\", k, v)\n                continue\n            k_l = k.split('.')\n            # module name is everything after 'submodule.' that is not the variable\n            # name\n            mod_name = '.'.join(k_l[1:-1])\n            mod = mods.get(mod_name, {})\n            # variable name is the last 'dot-free' segment in the key\n            mod[k_l[-1]] = v\n            mods[mod_name] = mod\n\n        out = {}\n        # bring into traditional shape\n        for name, props in mods.items():\n            if 'path' not in props:\n                lgr.warning(\"Failed to get '%s.path', skipping this submodule\", name)\n                continue\n            modprops = {'gitmodule_{}'.format(k): v\n                        for k, v in props.items()\n                        if not k.startswith('__')}\n            # Keep as PurePosixPath for possible normalization of / in the path etc\n            modpath = PurePosixPath(props['path'])\n            modprops['gitmodule_name'] = name\n            out[modpath] = modprops\n        return out\n\n    def get_submodules_(self, paths=None):\n        \"\"\"Yield submodules in this repository.\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`. Paths must be relative\n            to the resolved repository root, and must be normed to match the\n            reporting done by Git, i.e. no parent dir components\n            (ala \"some/../this\").\n\n        Returns\n        -------\n        A generator that yields a dictionary with information for each\n        submodule.\n        \"\"\"\n        if not (self.pathobj / \".gitmodules\").exists():\n            return\n\n        modinfo = self._parse_gitmodules()\n        if not modinfo:\n            # we exit early, if there is nothing on record (even though\n            # a .gitmodules file exists).\n            # without this conditional exit, we would be able to discover\n            # subprojects even when they are not recorded in .gitmodules,\n            # but at the cost of running an unconstrained ls-files call\n            # below\n            return\n\n        posix_mod_paths = [m.as_posix() for m in modinfo]\n        if paths:\n            # harmonize them into relative to the repository\n            posix_paths = []\n            for path in paths:\n                path = ut.PurePath(path)\n                if path.is_absolute():\n                    try:\n                        path = path.relative_to(self.pathobj)\n                    except ValueError as exc:\n                        lgr.debug(\n                            \"Path %s it not underneath %s, skipping since nothing should match it: %s\",\n                            path, self.pathobj, CapturedException(exc)\n                        )\n                        continue\n                posix_paths.append(path.as_posix())\n\n            # constrain the report by the given paths, make sure all paths are POSIX\n            posix_mod_paths = list(get_filtered_paths_(\n                posix_mod_paths,\n                posix_paths,\n                include_within_path=True,\n            ))\n\n        for r in self.call_git_items_(\n            ['ls-files', '--stage', '-z'],\n            sep='\\0',\n            files=posix_mod_paths,\n            read_only=True,\n            keep_ends=True,\n        ):\n            if not r.startswith('160000'):\n                # make sure this method never talks about non-dataset\n                # content\n                continue\n            props, rpath = r.split('\\t')\n            mode, gitsha, stage = props.split(' ')\n            if stage not in ('0', '2'):\n                # we either have non-merge situation, or a simple merge\n                # situation (i.e. stage=0). the reported gitsha always\n                # matches what we have locally.\n                # or we are in a three-way merge, in which case stage=2\n                # is what we want to report, because it matches the\n                # current HEAD (see git-read-tree manpage).\n                # there is either a stage 2 or stage 0, never both\n                continue\n            # remove the expected line separator from the path\n            rpath = rpath[:-1]\n            path = PurePosixPath(rpath)\n            yield dict(\n                path=self.pathobj / rpath, # full path returned here\n                type='dataset',\n                gitshasum=gitsha,\n                **modinfo.get(path, {})\n            )\n\n    def get_submodules(self, sorted_=True, paths=None):\n        \"\"\"Return list of submodules.\n\n        Parameters\n        ----------\n        sorted_ : bool, optional\n            Sort submodules by path name.\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`.\n\n        Returns\n        -------\n        List of submodule namedtuples if `compat` is true or otherwise a list\n        of dictionaries as returned by `get_submodules_`.\n        \"\"\"\n        xs = self.get_submodules_(paths=paths)\n\n        if sorted_:\n            xs = sorted(xs, key=lambda x: x[\"path\"])\n        return list(xs)\n\n    def update_ref(self, ref, value, oldvalue=None, symbolic=False):\n        \"\"\"Update the object name stored in a ref \"safely\".\n\n        Just a shim for `git update-ref` call if not symbolic, and\n        `git symbolic-ref` if symbolic\n\n        Parameters\n        ----------\n        ref : str\n          Reference, such as `ref/heads/BRANCHNAME` or HEAD.\n        value : str\n          Value to update to, e.g. hexsha of a commit when updating for a\n          branch ref, or branch ref if updating HEAD\n        oldvalue: str\n          Value to update from. Safeguard to be verified by git. This is only\n          valid if `symbolic` is not True.\n        symbolic : None\n          To instruct if ref is symbolic, e.g. should be used in case of\n          ref=HEAD\n        \"\"\"\n        if symbolic:\n            if oldvalue:\n                raise ValueError(\"oldvalue and symbolic must not be given both\")\n            cmd = ['symbolic-ref', ref, value]\n        else:\n            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])\n        self.call_git(cmd)\n\n    def tag(self, tag, message=None, commit=None, options=None):\n        \"\"\"Tag a commit\n\n        Parameters\n        ----------\n        tag : str\n          Custom tag label. Must be a valid tag name.\n        message : str, optional\n          If provided, adds ['-m', <message>] to the list of `git tag`\n          arguments.\n        commit : str, optional\n          If provided, will be appended as last argument to the `git tag` call,\n          and can be used to identify the commit that shall be tagged, if\n          not HEAD.\n        options : list, optional\n          Additional command options, inserted prior a potential `commit`\n          argument.\n        \"\"\"\n        # TODO: call in save.py complains about extensive logging. When does it\n        # happen in what way? Figure out, whether to just silence it or raise or\n        # whatever else.\n        args = ['tag']\n        if message:\n            args += ['-m', message]\n        if options is not None:\n            args.extend(options)\n        args.append(tag)\n        if commit:\n            args.append(commit)\n        self.call_git(args)\n\n    def get_tags(self, output=None):\n        \"\"\"Get list of tags\n\n        Parameters\n        ----------\n        output : str, optional\n          If given, limit the return value to a list of values matching that\n          particular key of the tag properties.\n\n        Returns\n        -------\n        list\n          Each item is a dictionary with information on a tag. At present\n          this includes 'hexsha', and 'name', where the latter is the string\n          label of the tag, and the former the hexsha of the object the tag\n          is attached to. The list is sorted by the creator date (committer\n          date for lightweight tags and tagger date for annotated tags), with\n          the most recent commit being the last element.\n        \"\"\"\n        tags = [\n            dict(\n                name=t['refname:strip=2'],\n                hexsha=t['object'] if t['object'] else t['objectname'],\n            )\n            for t in self.for_each_ref_(\n                fields=['refname:strip=2', 'objectname', 'object'],\n                pattern='refs/tags',\n                sort='creatordate')\n        ]\n        if output:\n            return [t[output] for t in tags]\n        else:\n            return tags\n\n    def describe(self, commitish=None, **kwargs):\n        \"\"\" Quick and dirty implementation to call git-describe\n\n        Parameters\n        ----------\n        kwargs:\n            transformed to cmdline options for git-describe;\n            see __init__ for description of the transformation\n        \"\"\"\n        # TODO: be more precise what failure to expect when and raise actual\n        # errors\n        cmd = ['describe'] + to_options(**kwargs)\n        if commitish is not None:\n            cmd.append(commitish)\n        try:\n            describe = self.call_git(cmd, expect_fail=True)\n            return describe.strip()\n        # TODO: WTF \"catch everything\"?\n        except:\n            return None\n\n    def get_tracking_branch(self, branch=None, remote_only=False):\n        \"\"\"Get the tracking branch for `branch` if there is any.\n\n        Parameters\n        ----------\n        branch: str\n            local branch to look up. If none is given, active branch is used.\n        remote_only : bool\n            Don't return a value if the upstream remote is set to \".\" (meaning\n            this repository).\n\n        Returns\n        -------\n        tuple\n            (remote or None, refspec or None) of the tracking branch\n        \"\"\"\n        if branch is None:\n            branch = self.get_corresponding_branch() or self.get_active_branch()\n            if branch is None:\n                return None, None\n\n        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)\n        if remote_only and track_remote == \".\":\n            return None, None\n        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)\n        return track_remote, track_branch\n\n    @property\n    def count_objects(self):\n        \"\"\"return dictionary with count, size(in KiB) information of git objects\n        \"\"\"\n\n        count_cmd = ['count-objects', '-v']\n        count_str = self.call_git(count_cmd)\n        count = {key: int(value)\n                 for key, value in [item.split(': ')\n                                    for item in count_str.split('\\n')\n                                    if len(item.split(': ')) == 2]}\n        return count\n\n    def get_git_attributes(self):\n        \"\"\"Query gitattributes which apply to top level directory\n\n        It is a thin compatibility/shortcut wrapper around more versatile\n        get_gitattributes which operates on a list of paths and returns\n        a dictionary per each path\n\n        Returns\n        -------\n        dict:\n          a dictionary with attribute name and value items relevant for the\n          top ('.') directory of the repository, and thus most likely the\n          default ones (if not overwritten with more rules) for all files within\n          repo.\n        \"\"\"\n        return self.get_gitattributes('.')['.']\n\n    def get_gitattributes(self, path, index_only=False):\n        \"\"\"Query gitattributes for one or more paths\n\n        Parameters\n        ----------\n        path: path or list\n          Path(s) to query. Paths may be relative or absolute.\n        index_only: bool\n          Flag whether to consider only gitattribute setting that are reflected\n          in the repository index, not just in the work tree content.\n\n        Returns\n        -------\n        dict:\n          Each key is a queried path (always relative to the repository root),\n          each value is a dictionary with attribute\n          name and value items. Attribute values are either True or False,\n          for set and unset attributes, or are the literal attribute value.\n        \"\"\"\n        path = ensure_list(path)\n        cmd = [\"check-attr\", \"-z\", \"--all\"]\n        if index_only:\n            cmd.append('--cached')\n        # make sure we have one entry for each query path to\n        # simplify work with the result\n        attributes = {p: {} for p in path}\n        attr = []\n        for item in self.call_git_items_(cmd, files=path, sep='\\0',\n                                         read_only=True):\n            attr.append(item)\n            if len(attr) < 3:\n                continue\n            # we have a full record\n            p, name, value = attr\n            attrs = attributes[p]\n            attrs[name] = \\\n                True if value == 'set' else False if value == 'unset' else value\n            # done, reset item\n            attr = []\n        return {relpath(k, self.path) if isabs(k) else k: v\n                for k, v in attributes.items()}\n\n    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):\n        \"\"\"Set gitattributes\n\n        By default appends additional lines to `attrfile`. Note, that later\n        lines in `attrfile` overrule earlier ones, which may or may not be\n        what you want. Set `mode` to 'w' to replace the entire file by\n        what you provided in `attrs`.\n\n        Parameters\n        ----------\n        attrs : list\n          Each item is a 2-tuple, where the first element is a path pattern,\n          and the second element is a dictionary with attribute key/value\n          pairs. The attribute dictionary must use the same semantics as those\n          returned by `get_gitattributes()`. Path patterns can use absolute paths,\n          in which case they will be normalized relative to the directory\n          that contains the target .gitattributes file (see `attrfile`).\n        attrfile: path\n          Path relative to the repository root of the .gitattributes file the\n          attributes shall be set in.\n        mode: str\n          'a' to append .gitattributes, 'w' to replace it\n        \"\"\"\n\n        git_attributes_file = op.join(self.path, attrfile)\n        attrdir = op.dirname(git_attributes_file)\n        if not op.exists(attrdir):\n            os.makedirs(attrdir)\n\n        with open(git_attributes_file, mode + '+') as f:\n            # for append, fix existing files that do not end with \\n\n            if mode == 'a' and f.tell():\n                f.seek(max(0, f.tell() - len(os.linesep)))\n                if not f.read().endswith('\\n'):\n                    f.write('\\n')\n\n            for pattern, attr in sorted(attrs, key=lambda x: x[0]):\n                # normalize the pattern relative to the target .gitattributes file\n                npath = _normalize_path(\n                    op.join(self.path, op.dirname(attrfile)), pattern)\n                # paths in gitattributes always have to be POSIX\n                npath = Path(npath).as_posix()\n                attrline = u''\n                if npath.count(' '):\n                    # quote patterns with spaces\n                    attrline += u'\"{}\"'.format(npath.replace('\"', '\\\\\"'))\n                else:\n                    attrline += npath\n                for a in sorted(attr):\n                    val = attr[a]\n                    if val is True:\n                        attrline += ' {}'.format(a)\n                    elif val is False:\n                        attrline += ' -{}'.format(a)\n                    else:\n                        attrline += ' {}={}'.format(a, val)\n                f.write('{}\\n'.format(attrline))\n\n    def get_content_info(self, paths=None, ref=None, untracked='all'):\n        \"\"\"Get identifier and type information from repository content.\n\n        This is simplified front-end for `git ls-files/tree`.\n\n        Both commands differ in their behavior when queried about subdataset\n        paths. ls-files will not report anything, ls-tree will report on the\n        subdataset record. This function uniformly follows the behavior of\n        ls-tree (report on the respective subdataset mount).\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath) or None\n          Specific paths, relative to the resolved repository root, to query\n          info for. Paths must be normed to match the reporting done by Git,\n          i.e. no parent dir components (ala \"some/../this\").\n          If `None`, info is reported for all content.\n        ref : gitref or None\n          If given, content information is retrieved for this Git reference\n          (via ls-tree), otherwise content information is produced for the\n          present work tree (via ls-files). With a given reference, the\n          reported content properties also contain a 'bytesize' record,\n          stating the size of a file in bytes.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when no `ref` was given:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n\n          `gitshasum`\n            SHASUM of the item as tracked by Git, or None, if not\n            tracked. This could be different from the SHASUM of the file\n            in the worktree, if it was modified.\n\n        Raises\n        ------\n        ValueError\n          In case of an invalid Git reference (e.g. 'HEAD' in an empty\n          repository)\n        \"\"\"\n        lgr.debug('%s.get_content_info(...)', self)\n        # TODO limit by file type to replace code in subdatasets command\n        info = dict()\n\n        if paths:  # is not None separate after\n            # path matching will happen against what Git reports\n            # and Git always reports POSIX paths\n            # any incoming path has to be relative already, so we can simply\n            # convert unconditionally\n            # note: will be list-ified below\n            posix_paths = [ut.PurePath(p).as_posix() for p in paths]\n        elif paths is not None:\n            return info\n        else:\n            posix_paths = None\n\n        if posix_paths and (not ref or external_versions[\"cmd:git\"] >= \"2.29.0\"):\n            # If a path points within a submodule, we need to map it to the\n            # containing submodule before feeding it to ls-files or ls-tree.\n            #\n            # Before Git 2.29.0, ls-tree and ls-files differed in how they\n            # reported paths within submodules: ls-files provided no output,\n            # and ls-tree listed the submodule. Now they both return no output.\n            submodules = [s[\"path\"].relative_to(self.pathobj).as_posix()\n                          for s in self.get_submodules_()]\n            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well\n            posix_paths = get_parent_paths(posix_paths, submodules)\n\n        # this will not work in direct mode, but everything else should be\n        # just fine\n        if not ref:\n            # make sure no operations are pending before we figure things\n            # out in the worktree\n            self.precommit()\n\n            # --exclude-standard will make sure to honor and standard way\n            # git can be instructed to ignore content, and will prevent\n            # crap from contaminating untracked file reports\n            cmd = ['ls-files', '--stage', '-z']\n            # untracked report mode, using labels from `git diff` option style\n            if untracked == 'all':\n                cmd += ['--exclude-standard', '-o']\n            elif untracked == 'normal':\n                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']\n            elif untracked == 'no':\n                pass\n            else:\n                raise ValueError(\n                    'unknown value for `untracked`: {}'.format(untracked))\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\\t(?P<fname>.*)$')\n        else:\n            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\\s]*(?P<size>[0-9-]+)\\t(?P<fname>.*)$')\n\n        lgr.debug('Query repo: %s', cmd)\n        try:\n            stdout = self.call_git(\n                cmd,\n                files=posix_paths,\n                expect_fail=True,\n                read_only=True)\n        except CommandError as exc:\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                raise InvalidGitReferenceError(ref)\n            raise\n        lgr.debug('Done query repo: %s', cmd)\n\n        self._get_content_info_line_helper(\n            ref,\n            info,\n            stdout.split('\\0'),\n            props_re)\n\n        lgr.debug('Done %s.get_content_info(...)', self)\n        return info\n\n    def _get_content_info_line_helper(self, ref, info, lines, props_re):\n        \"\"\"Internal helper of get_content_info() to parse Git output\"\"\"\n        mode_type_map = {\n            '100644': 'file',\n            '100755': 'file',\n            '120000': 'symlink',\n            '160000': 'dataset',\n        }\n        for line in lines:\n            if not line:\n                continue\n            inf = {}\n            props = props_re.match(line)\n            if not props:\n                # Kludge: Filter out paths starting with .git/ to work around\n                # an `ls-files -o` bug that was fixed in Git 2.25.\n                #\n                # TODO: Drop this condition when GIT_MIN_VERSION is at least\n                # 2.25.\n                if line.startswith(\".git/\"):\n                    lgr.debug(\"Filtering out .git/ file: %s\", line)\n                    continue\n                # not known to Git, but Git always reports POSIX\n                path = ut.PurePosixPath(line)\n                inf['gitshasum'] = None\n            else:\n                # again Git reports always in POSIX\n                path = ut.PurePosixPath(props.group('fname'))\n\n            # revisit the file props after this path has not been rejected\n            if props:\n                inf['gitshasum'] = props.group('sha')\n                inf['type'] = mode_type_map.get(\n                    props.group('type'), props.group('type'))\n\n                if ref and inf['type'] == 'file':\n                    inf['bytesize'] = int(props.group('size'))\n\n            # join item path with repo path to get a universally useful\n            # path representation with auto-conversion and tons of other\n            # stuff\n            path = self.pathobj.joinpath(path)\n            if 'type' not in inf:\n                # be nice and assign types for untracked content\n                inf['type'] = 'symlink' if path.is_symlink() \\\n                    else 'directory' if path.is_dir() else 'file'\n            info[path] = inf\n\n    def status(self, paths=None, untracked='all', eval_submodule_state='full'):\n        \"\"\"Simplified `git status` equivalent.\n\n        Parameters\n        ----------\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list. If a query path points\n          into a subdataset, a report is made on the subdataset record\n          within the queried dataset only (no recursion).\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        lgr.debug('Query status of %r for %s paths',\n                  self, len(paths) if paths is not None else 'all')\n        return self.diffstatus(\n            fr='HEAD' if self.get_hexsha() else None,\n            to=None,\n            paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state)\n\n    def diff(self, fr, to, paths=None, untracked='all',\n             eval_submodule_state='full'):\n        \"\"\"Like status(), but reports changes between to arbitrary revisions\n\n        Parameters\n        ----------\n        fr : str or None\n          Revision specification (anything that Git understands). Passing\n          `None` considers anything in the target state as new.\n        to : str or None\n          Revision specification (anything that Git understands), or None\n          to compare to the state of the work tree.\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when `to` is None:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        return {k: v for k, v in self.diffstatus(\n            fr=fr, to=to, paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state).items()\n            if v.get('state', None) != 'clean'}\n\n    def diffstatus(self, fr, to, paths=None, untracked='all',\n                   eval_submodule_state='full', _cache=None):\n        \"\"\"Like diff(), but reports the status of 'clean' content too.\n\n        It supports an additional submodule evaluation state 'global'.\n        If given, it will return a single 'modified'\n        (vs. 'clean') state label for the entire repository, as soon as\n        it can.\n        \"\"\"\n        def _get_cache_key(label, paths, ref, untracked=None):\n            return self.path, label, tuple(paths) if paths else None, \\\n                ref, untracked\n\n        if _cache is None:\n            _cache = {}\n\n        if paths is not None:\n            # at this point we must normalize paths to the form that\n            # Git would report them, to easy matching later on\n            paths = map(ut.Path, paths)\n            paths = [\n                p.relative_to(self.pathobj) if p.is_absolute() else p\n                for p in paths\n            ]\n\n        # TODO report more info from get_content_info() calls in return\n        # value, those are cheap and possibly useful to a consumer\n        # we need (at most) three calls to git\n        if to is None:\n            # everything we know about the worktree, including os.stat\n            # for each file\n            key = _get_cache_key('ci', paths, None, untracked)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(\n                    paths=paths, ref=None, untracked=untracked)\n                _cache[key] = to_state\n            # we want Git to tell us what it considers modified and avoid\n            # reimplementing logic ourselves\n            key = _get_cache_key('mod', paths, None)\n            if key in _cache:\n                modified = _cache[key]\n            else:\n                # from Git 2.31.0 onwards ls-files has --deduplicate\n                # by for backward compatibility keep doing deduplication here\n                modified = set(\n                    self.pathobj.joinpath(ut.PurePosixPath(p))\n                    for p in self.call_git_items_(\n                        # we must also look for deleted files, for the logic\n                        # below to work. Only from Git 2.31.0 would they be\n                        # included with `-m` alone\n                        ['ls-files', '-z', '-m', '-d'],\n                        # low-level code cannot handle pathobjs\n                        files=[str(p) for p in paths] if paths is not None else None,\n                        sep='\\0',\n                        read_only=True)\n                    if p)\n                _cache[key] = modified\n        else:\n            key = _get_cache_key('ci', paths, to)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(paths=paths, ref=to)\n                _cache[key] = to_state\n            # we do not need worktree modification detection in this case\n            modified = None\n        # origin state\n        key = _get_cache_key('ci', paths, fr)\n        if key in _cache:\n            from_state = _cache[key]\n        else:\n            if fr:\n                from_state = self.get_content_info(paths=paths, ref=fr)\n            else:\n                # no ref means from nothing\n                from_state = {}\n            _cache[key] = from_state\n\n        status = dict()\n        for f, to_state_r in to_state.items():\n            props = self._diffstatus_get_state_props(\n                f,\n                from_state.get(f, None),\n                to_state_r,\n                # are we comparing against a recorded commit or the worktree\n                to is not None,\n                # if we have worktree modification info, report if\n                # path is reported as modified in it\n                modified and f in modified,\n                eval_submodule_state)\n            # potential early exit in \"global\" eval mode\n            if eval_submodule_state == 'global' and \\\n                    props.get('state', None) not in ('clean', None):\n                # any modification means globally 'modified'\n                return 'modified'\n            status[f] = props\n\n        for f, from_state_r in from_state.items():\n            if f not in to_state:\n                # we new this, but now it is gone and Git is not complaining\n                # about it being missing -> properly deleted and deletion\n                # stages\n                status[f] = dict(\n                    state='deleted',\n                    type=from_state_r['type'],\n                    # report the shasum to distinguish from a plainly vanished\n                    # file\n                    gitshasum=from_state_r['gitshasum'],\n                )\n                if eval_submodule_state == 'global':\n                    return 'modified'\n\n        if to is not None or eval_submodule_state == 'no':\n            # if we have `to` we are specifically comparing against\n            # a recorded state, and this function only attempts\n            # to label the state of a subdataset, not investigate\n            # specifically what the changes in subdatasets are\n            # this is done by a high-level command like rev-diff\n            # so the comparison within this repo and the present\n            # `state` label are all we need, and they are done already\n            if eval_submodule_state == 'global':\n                return 'clean'\n            else:\n                return status\n\n        # loop over all subdatasets and look for additional modifications\n        for f, st in status.items():\n            f = str(f)\n            if 'state' in st or not st['type'] == 'dataset':\n                # no business here\n                continue\n            if not GitRepo.is_valid_repo(f):\n                # submodule is not present, no chance for a conflict\n                st['state'] = 'clean'\n                continue\n            # we have to recurse into the dataset and get its status\n            subrepo = repo_from_path(f)\n            # get the HEAD commit, or the one of the corresponding branch\n            # only that one counts re super-sub relationship\n            # save() syncs the corresponding branch each time\n            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())\n            st['gitshasum'] = subrepo_commit\n            # subdataset records must be labeled clean up to this point\n            # test if current commit in subdataset deviates from what is\n            # recorded in the dataset\n            st['state'] = 'modified' \\\n                if st['prev_gitshasum'] != subrepo_commit \\\n                else 'clean'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n            if eval_submodule_state == 'commit':\n                continue\n            # the recorded commit did not change, so we need to make\n            # a more expensive traversal\n            st['state'] = subrepo.diffstatus(\n                # we can use 'HEAD' because we know that the commit\n                # did not change. using 'HEAD' will facilitate\n                # caching the result\n                fr='HEAD',\n                to=None,\n                paths=None,\n                untracked=untracked,\n                eval_submodule_state='global',\n                _cache=_cache) if st['state'] == 'clean' else 'modified'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n\n        if eval_submodule_state == 'global':\n            return 'clean'\n        else:\n            return status\n\n    def _diffstatus_get_state_props(self, f, from_state, to_state,\n                                    against_commit,\n                                    modified_in_worktree,\n                                    eval_submodule_state):\n        \"\"\"Helper to determine diff properties for a single path\n\n        Parameters\n        ----------\n        f : Path\n        from_state : dict\n        to_state : dict\n        against_commit : bool\n          Flag whether `to_state` reflects a commit or the worktree.\n        modified_in_worktree : bool\n          Flag whether a worktree modification is reported. This is ignored\n          when `against_commit` is True.\n        eval_submodule_state : {'commit', 'no', ...}\n        \"\"\"\n        if against_commit:\n            # we can ignore any worktree modification reported when\n            # comparing against a commit\n            modified_in_worktree = False\n\n        props = {}\n        if 'type' in to_state:\n            props['type'] = to_state['type']\n\n        to_sha = to_state['gitshasum']\n        from_sha = from_state['gitshasum'] if from_state else None\n\n        # determine the state of `f` from from_state and to_state records, if\n        # it can be determined conclusively from it. If not, it will\n        # stay None for now\n        state = None\n        if not from_state:\n            # this is new, or rather not known to the previous state\n            state = 'added' if to_sha else 'untracked'\n        elif to_sha == from_sha and not modified_in_worktree:\n            # something that is seemingly unmodified, based on the info\n            # gathered so far\n            if to_state['type'] == 'dataset':\n                if against_commit or eval_submodule_state == 'commit':\n                    # we compare against a recorded state, just based on\n                    # the shas we can be confident, otherwise the state\n                    # of a subdataset isn't fully known yet, because\n                    # `modified_in_worktree` will only reflect changes\n                    # in the commit of a subdataset without looking into\n                    # it for uncommitted changes. Such tests are done\n                    # later and based on further conditionals for\n                    # performance reasons\n                    state = 'clean'\n            else:\n                # no change in git record, and no change on disk\n                # at this point we know that the reported object ids\n                # for this file are identical in the to and from\n                # records.  If to is None, we're comparing to the\n                # working tree and a deleted file will still have an\n                # identical id, so we need to check whether the file is\n                # gone before declaring it clean. This working tree\n                # check is irrelevant and wrong if to is a ref.\n                state = 'clean' \\\n                    if against_commit or (f.exists() or f.is_symlink()) \\\n                    else 'deleted'\n        else:\n            # change in git record, or on disk\n            # for subdatasets leave the 'modified' judgement to the caller\n            # for supporting corner cases, such as adjusted branch\n            # which require inspection of a subdataset\n            # TODO we could have a new file that is already staged\n            # but had subsequent modifications done to it that are\n            # unstaged. Such file would presently show up as 'added'\n            # ATM I think this is OK, but worth stating...\n            state = ('modified'\n                     if against_commit or to_state['type'] != 'dataset'\n                     else None\n                    ) if f.exists() or f.is_symlink() else 'deleted'\n            # TODO record before and after state for diff-like use\n            # cases\n\n        if state in ('clean', 'added', 'modified', None):\n            # assign present gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['gitshasum'] = to_sha\n            if 'bytesize' in to_state:\n                # if we got this cheap, report it\n                props['bytesize'] = to_state['bytesize']\n            elif state == 'clean' and 'bytesize' in from_state:\n                # no change, we can take this old size info\n                props['bytesize'] = from_state['bytesize']\n        if state in ('clean', 'modified', 'deleted', None):\n            # assign previous gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['prev_gitshasum'] = from_sha\n        if state:\n            # only report a state if we could determine any\n            # outside code tests for existence of the property\n            # and not (always) for the value\n            props['state'] = state\n        return props\n\n    def _save_pre(self, paths, _status, **kwargs):\n        # helper to get an actionable status report\n        if paths is not None and not paths and not _status:\n            return\n        if _status is None:\n            if 'untracked' not in kwargs:\n                kwargs['untracked'] = 'normal'\n            status = self.status(\n                paths=paths,\n                **{k: kwargs[k] for k in kwargs\n                   if k in ('untracked', 'eval_submodule_state')})\n        else:\n            # we want to be able to add items down the line\n            # make sure to detach from prev. owner\n            status = _status.copy()\n        return status\n\n    def get_staged_paths(self):\n        \"\"\"Returns a list of any stage repository path(s)\n\n        This is a rather fast call, as it will not depend on what is going on\n        in the worktree.\n        \"\"\"\n        try:\n            return list(self.call_git_items_(\n                ['diff', '--name-only', '--staged'],\n                expect_stderr=True))\n        except CommandError as e:\n            lgr.debug(CapturedException(e))\n            return []\n\n    def _save_post(self, message, files, partial_commit, amend=False,\n                   allow_empty=False):\n        # helper to commit changes reported in status\n\n        # TODO remove pathobj stringification when commit() can\n        # handle it\n        to_commit = [str(f.relative_to(self.pathobj))\n                     for f in files] \\\n                    if partial_commit else None\n        if not partial_commit or to_commit or allow_empty or \\\n                (amend and message):\n            # we directly call GitRepo.commit() to avoid a whole slew\n            # if direct-mode safeguards and workarounds in the AnnexRepo\n            # implementation (which also run an additional dry-run commit\n            GitRepo.commit(\n                self,\n                files=to_commit,\n                msg=message,\n                options=to_options(amend=amend, allow_empty=allow_empty),\n                # do not raise on empty commit\n                # it could be that the `add` in this save-cycle has already\n                # brought back a 'modified' file into a clean state\n                careless=True,\n            )\n\n    def save(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Save dataset content.\n\n        Parameters\n        ----------\n        message : str or None\n          A message to accompany the changeset in the log. If None,\n          a default message is used.\n        paths : list or None\n          Any content with path matching any of the paths given in this\n          list will be saved. Matching will be performed against the\n          dataset status (GitRepo.status()), or a custom status provided\n          via `_status`. If no paths are provided, ALL non-clean paths\n          present in the repo status or `_status` will be saved.\n        _status : dict or None\n          If None, Repo.status() will be queried for the given `ds`. If\n          a dict is given, its content will be used as a constraint.\n          For example, to save only modified content, but no untracked\n          content, set `paths` to None and provide a `_status` that has\n          no entries for untracked content.\n        **kwargs :\n          Additional arguments that are passed to underlying Repo methods.\n          Supported:\n\n          - git : bool (passed to Repo.add()\n          - eval_submodule_state : {'full', 'commit', 'no'}\n            passed to Repo.status()\n          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()\n          - amend : bool (passed to GitRepo.commit)\n        \"\"\"\n        return list(\n            self.save_(\n                message=message,\n                paths=paths,\n                _status=_status,\n                **kwargs\n            )\n        )\n\n    def save_(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Like `save()` but working as a generator.\"\"\"\n        from datalad.interface.results import get_status_dict\n\n        status_state = _get_save_status_state(\n            self._save_pre(paths, _status, **kwargs) or {}\n        )\n        amend = kwargs.get('amend', False)\n\n        # TODO: check on those None's -- may be those are also \"nothing to worry about\"\n        # and we could just return?\n        if not any(status_state.values()) and not (message and amend):\n            # all clean, nothing todo\n            lgr.debug('Nothing to save in %r, exiting early', self)\n            return\n\n        # three things are to be done:\n        # - remove (deleted if not already staged)\n        # - add (modified/untracked)\n        # - commit (with all paths that have been touched, to bypass\n        #   potential pre-staged bits)\n\n        staged_paths = self.get_staged_paths()\n        need_partial_commit = bool(staged_paths)\n        if need_partial_commit and hasattr(self, \"call_annex\"):\n            # so we have some staged content. let's check which ones\n            # are symlinks -- those could be annex key links that\n            # are broken after a `git-mv` operation\n            # https://github.com/datalad/datalad/issues/4967\n            # call `git-annex pre-commit` on them to rectify this before\n            # saving the wrong symlinks\n            added = status_state['added']\n            tofix = [\n                sp for sp in staged_paths\n                if added.get(self.pathobj / sp, {}).get(\"type\") == \"symlink\"\n            ]\n            if tofix:\n                self.call_annex(['pre-commit'], files=tofix)\n\n        submodule_change = False\n\n        if status_state['deleted']:\n            vanished_subds = [\n                str(f.relative_to(self.pathobj))\n                for f, props in status_state['deleted'].items()\n                if props.get('type') == 'dataset'\n            ]\n            if vanished_subds:\n                # we submodule removal we use `git-rm`, because the clean-up\n                # is more complex than just an index update -- make no\n                # sense to have a duplicate implementation.\n                # we do not yield here, but only altogether below -- we are just\n                # processing gone components, should always be quick.\n                self._call_git(['rm', '-q'], files=vanished_subds)\n                submodule_change = True\n            # remove anything from the index that was found to be gone\n            self._call_git(\n                ['update-index', '--remove'],\n                files=[\n                    str(f.relative_to(self.pathobj))\n                    for f, props in status_state['deleted'].items()\n                    # do not update the index, if there is already\n                    # something staged for this path (e.g.,\n                    # a directory was removed and a file staged\n                    # in its place)\n                    if not props.get('gitshasum')\n                    # we already did the submodules\n                    and props.get('type') != 'dataset'\n                ]\n            )\n            # now yield all deletions\n            for p, props in status_state['deleted'].items():\n                yield get_status_dict(\n                    action='delete',\n                    refds=self.pathobj,\n                    type=props.get('type'),\n                    path=p,\n                    status='ok',\n                    logger=lgr)\n\n        # TODO this additional query should not be, based on status as given\n        # if anyhow possible, however, when paths are given, status may\n        # not contain all required information. In case of path=None AND\n        # _status=None, we should be able to avoid this, because\n        # status should have the full info already\n        # looks for contained repositories\n        untracked_dirs = [\n            f.relative_to(self.pathobj)\n            for f, props in status_state['untracked'].items()\n            if props.get('type', None) == 'directory']\n        to_add_submodules = []\n        if untracked_dirs:\n            to_add_submodules = [\n                sm for sm, sm_props in\n                self.get_content_info(\n                    untracked_dirs,\n                    ref=None,\n                    # request exhaustive list, so that everything that is\n                    # still reported as a directory must be its own repository\n                    untracked='all').items()\n                if sm_props.get('type', None) == 'directory']\n            to_add_submodules = _prune_deeper_repos(to_add_submodules)\n\n        to_stage_submodules = {\n            f: props\n            for f, props in status_state['modified_or_untracked'].items()\n            if props.get('type', None) == 'dataset'}\n        if to_stage_submodules:\n            lgr.debug(\n                '%i submodule path(s) to stage in %r %s',\n                len(to_stage_submodules), self,\n                to_stage_submodules\n                if len(to_stage_submodules) < 10 else '')\n            to_add_submodules += list(to_stage_submodules)\n\n        if to_add_submodules:\n            for r in self._save_add_submodules(to_add_submodules):\n                if r.get('status', None) == 'ok':\n                    submodule_change = True\n                yield r\n\n        if submodule_change:\n            # this will alter the config, reload\n            self.config.reload()\n            # need to include .gitmodules in what needs committing\n            f = self.pathobj.joinpath('.gitmodules')\n            status_state['modified_or_untracked'][f] = \\\n                status_state['modified'][f] = \\\n                dict(type='file', state='modified')\n            # now stage .gitmodules\n            self._call_git(['update-index', '--add'], files=['.gitmodules'])\n            # and report on it\n            yield get_status_dict(\n                action='add',\n                refds=self.pathobj,\n                type='file',\n                path=f,\n                status='ok',\n                logger=lgr)\n\n        to_add = {\n            # TODO remove pathobj stringification when add() can\n            # handle it\n            str(f.relative_to(self.pathobj)): props\n            for f, props in status_state['modified_or_untracked'].items()\n            if not (f in to_add_submodules or f in to_stage_submodules)}\n        if to_add:\n            compat_config = \\\n                self.config.obtain(\"datalad.save.windows-compat-warning\")\n            to_add, problems = self._check_for_win_compat(to_add, compat_config)\n            lgr.debug(\n                '%i path(s) to add to %s %s',\n                len(to_add), self, to_add if len(to_add) < 10 else '')\n\n            if to_add:\n                yield from self._save_add(\n                        to_add,\n                        git_opts=None,\n                        **{k: kwargs[k] for k in kwargs\n                           if k in (('git',) if hasattr(self, 'uuid')\n                                    else tuple())})\n            if problems:\n                from datalad.interface.results import get_status_dict\n                msg = \\\n                    'Incompatible name for Windows systems; disable with ' \\\n                    'datalad.save.windows-compat-warning.',\n                for path in problems:\n                    yield get_status_dict(\n                        action='save',\n                        refds=self.pathobj,\n                        type='file',\n                        path=(self.pathobj / ut.PurePosixPath(path)),\n                        status='impossible',\n                        message=msg,\n                        logger=lgr)\n\n\n        # https://github.com/datalad/datalad/issues/6558\n        # file could have become a directory. Unfortunately git\n        # would then mistakenly refuse to commit if that old path is also\n        # given to commit, so we better filter it out\n        if status_state['deleted'] and status_state['added']:\n            # check if any \"deleted\" is a directory now. Then for those\n            # there should be some other path under that directory in 'added'\n            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:\n                # this could potentially be expensive if lots of files become\n                # directories, but it is unlikely to happen often\n                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish\n                # path_is_subpath faster, also if comparing to \"in f.parents\"\n                f_str = str(f)\n                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):\n                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post\n\n        # Note, that allow_empty is always ok when we amend. Required when we\n        # amend an empty commit while the amendment is empty, too (though\n        # possibly different message). If an empty commit was okay before, it's\n        # okay now.\n        status_state.pop('modified_or_untracked')  # pop the hybrid state\n        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,\n                        allow_empty=amend)\n        # TODO yield result for commit, prev helper checked hexsha pre\n        # and post...\n\n    def _check_for_win_compat(self, files, config):\n        \"\"\"Check file names for illegal characters or reserved names on Windows\n\n        In the case that a non-Windows-compatible file is detected, warn users\n        about potential interoperability issues.\n\n        Parameters\n        ----------\n        files\n          list of files to add\n        config\n          value of self.config.obtain(\"datalad.save.windows-compat-warning\"),\n          used to choose appropriate behavior. \"none\" performs no check,\n          \"warning\" warns in case of incompatibilities, and \"error\" results in\n          an error result in case of incompatibilities\n        \"\"\"\n        # don't perform any check when the configuration is set to 'none'\n        if config == 'none':\n            return files, None\n\n        from collections import defaultdict\n        problems = defaultdict(list)\n        for file in files:\n            for part in Path(file).parts:\n                # check every component of the path for incompatibilities\n                if Path(part).stem.upper() in RESERVED_NAMES_WIN:\n                    problems['Elements using a reserved filename:'].append(part)\n                    problems['paths'].append(file)\n                if re.search(ILLEGAL_CHARS_WIN, part):\n                    problems['Elements with illegal characters:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith('.'):\n                    problems['Elements ending with a dot:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith(' '):\n                    problems['Elements ending with a space:'].append(part)\n                    problems['paths'].append(file)\n        if not problems:\n            return files, None\n        msg = \\\n            \"Some elements of your dataset are not compatible with \" \\\n            \"Windows systems. Disable this check by changing \" \\\n            \"datalad.save.windows-compat-warning or consider renaming \" \\\n            \"the following elements: \"\n        for k, v in problems.items():\n            # use the key as an explanation, and report filenames only once\n            msg += f\"\\n{k} {[*{*v}]}\" if k != 'paths' else ''\n        if config == 'warning':\n            lgr.warning(msg)\n            return files, None\n\n        elif config == 'error':\n            # take the problematic files out of to_add\n            for path in [*{*problems['paths']}]:\n                files.pop(path)\n            return files, [*{*problems['paths']}]\n\n    def _save_add(self, files, git_opts=None):\n        \"\"\"Simple helper to add files in save()\"\"\"\n        from datalad.interface.results import get_status_dict\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.largefiles to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.largefiles=nothing', 'add'] +\n                ensure_list(git_opts) + ['--verbose'],\n                files=list(files.keys()),\n                pathspec_from_file=True,\n            )\n            # get all the entries\n            for r in self._process_git_get_output(*add_out):\n                yield get_status_dict(\n                    action=r.get('command', 'add'),\n                    refds=self.pathobj,\n                    type='file',\n                    path=(self.pathobj / ut.PurePosixPath(r['file']))\n                    if 'file' in r else None,\n                    status='ok' if r.get('success', None) else 'error',\n                    key=r.get('key', None),\n                    # while there is no git-annex underneath here, we\n                    # tend to fake its behavior, so we can also support\n                    # this type of messaging\n                    message='\\n'.join(r['error-messages'])\n                    if 'error-messages' in r else None,\n                    logger=lgr)\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n    def _save_add_submodules(self, paths):\n        \"\"\"Add new submodules, or updates records of existing ones\n\n        This method does not use `git submodule add`, but aims to be more\n        efficient by limiting the scope to mere in-place registration of\n        multiple already present repositories.\n\n        Parameters\n        ----------\n        paths : list(Path)\n\n        Yields\n        ------\n        dict\n          Result records\n        \"\"\"\n        from datalad.interface.results import get_status_dict\n\n        # first gather info from all datasets in read-only fashion, and then\n        # update index, .gitmodules and .git/config at once\n        info = []\n        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843\n        # We must not add already known submodules explicitly since \"untracked\"\n        # can be assigned even for known ones (TODO: add issue, might have been closed)?\n        # Not sure if operating on relative paths would provide any speed up so use full\n        known_sub_paths = {s['path'] for s in self.get_submodules_()}\n        for path in paths:\n            already_known = path in known_sub_paths\n            rpath = str(path.relative_to(self.pathobj).as_posix())\n            subm = repo_from_path(path)\n            # if there is a corresponding branch, we want to record it's state.\n            # we rely on the corresponding branch being synced already.\n            # `save` should do that each time it runs.\n            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())\n            if not subm_commit:\n                yield get_status_dict(\n                    action='add_submodule',\n                    ds=self,\n                    path=path,\n                    status='error',\n                    message=('cannot add subdataset %s with no commits', subm),\n                    logger=lgr)\n                continue\n            # make an attempt to configure a submodule source URL based on the\n            # discovered remote configuration\n            remote, branch = subm.get_tracking_branch()\n            url = subm.get_remote_url(remote) if remote else None\n            if url is None:\n                url = './{}'.format(rpath)\n            subm_id = subm.config.get('datalad.dataset.id', None)\n            info.append(\n                dict(\n                     # if we have additional information on this path, pass it on.\n                     # if not, treat it as an untracked directory\n                     paths[path] if isinstance(paths, dict)\n                     else dict(type='directory', state='untracked'),\n                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,\n                     url=url, known=already_known))\n\n        # bypass any convenience or safe-manipulator for speed reasons\n        # use case: saving many new subdatasets in a single run\n        with (self.pathobj / '.gitmodules').open('a') as gmf, \\\n             (self.pathobj / '.git' / 'config').open('a') as gcf:\n            for i in info:\n                # we update the subproject commit unconditionally\n                self.call_git([\n                    'update-index', '--add', '--replace', '--cacheinfo', '160000',\n                    i['commit'], i['rpath']\n                ])\n                # only write the .gitmodules/.config changes when this is not yet\n                # a subdataset and not yet already known\n                # TODO: we could update the URL, and branch info at this point,\n                # even for previously registered subdatasets\n                if not i['known'] and (i['type'] != 'dataset' or (\n                        i['type'] == 'dataset' and i['state'] == 'untracked')):\n                    gmprops = dict(path=i['rpath'], url=i['url'])\n                    if i['id']:\n                        gmprops['datalad-id'] = i['id']\n                    write_config_section(\n                        gmf, 'submodule', i['rpath'], gmprops)\n                    write_config_section(\n                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))\n\n                # This mirrors the result structure yielded for\n                # to_stage_submodules below.\n                yield get_status_dict(\n                    action='add',\n                    refds=self.pathobj,\n                    type='dataset',\n                    key=None,\n                    path=i['path'],\n                    status='ok',\n                    logger=lgr)\n\n\ndef _get_save_status_state(status):\n    \"\"\"\n    Returns\n    -------\n    dict\n      By status category by file path, mapped to status properties.\n    \"\"\"\n    # Sort status into status by state with explicit list of states\n    # (excluding clean we do not care about) we expect to be present\n    # and which we know of (unless None), and modified_or_untracked hybrid\n    # since it is used below\n    status_state = {\n        k: {}\n        for k in (None,  # not cared of explicitly here\n                  'added',  # not cared of explicitly here\n                  # 'clean'  # not even wanted since nothing to do about those\n                  'deleted',\n                  'modified',\n                  'untracked',\n                  'modified_or_untracked',  # hybrid group created here\n                  )}\n    for f, props in status.items():\n        state = props.get('state', None)\n        if state == 'clean':\n            # we don't care about clean\n            continue\n        if state == 'modified' and props.get('gitshasum') \\\n                and props.get('gitshasum') == props.get('prev_gitshasum'):\n            # reported as modified, but with identical shasums -> typechange\n            # a subdataset maybe? do increasingly expensive tests for\n            # speed reasons\n            if props.get('type') != 'dataset' and f.is_dir() \\\n                    and GitRepo.is_valid_repo(f):\n                # it was not a dataset, but now there is one.\n                # we declare it untracked to engage the discovery tooling.\n                state = 'untracked'\n                props = dict(type='dataset', state='untracked')\n        status_state[state][f] = props\n        # The hybrid one to retain the same order as in original status\n        if state in ('modified', 'untracked'):\n            status_state['modified_or_untracked'][f] = props\n    return status_state\n\n\n# used in in the get command and GitRepo.add_submodule(), the\n# latter is not used outside the tests\ndef _fixup_submodule_dotgit_setup(ds, relativepath):\n    \"\"\"Implementation of our current of .git in a subdataset\n\n    Each subdataset/module has its own .git directory where a standalone\n    repository would have it. No gitdir files, no symlinks.\n    \"\"\"\n    # move .git to superrepo's .git/modules, remove .git, create\n    # .git-file\n    path = opj(ds.path, relativepath)\n    subds_dotgit = opj(path, \".git\")\n\n    repo = GitRepo(path, create=False)\n    if repo.dot_git.parent == repo.pathobj:\n        # this is what we want\n        return\n\n    # first we want to remove any conflicting worktree setup\n    # done by git to find the checkout at the mountpoint of the\n    # submodule, if we keep that, any git command will fail\n    # after we move .git\n    # Ben: Shouldn't we re-setup a possible worktree afterwards?\n    repo.config.unset('core.worktree', scope='local')\n    # what we have here is some kind of reference, remove and\n    # replace by the target\n    os.remove(subds_dotgit)\n    # make absolute\n    src_dotgit = str(repo.dot_git)\n    # move .git\n    from os import (\n        listdir,\n        rename,\n        rmdir,\n    )\n    ensure_dir(subds_dotgit)\n    for dot_git_entry in listdir(src_dotgit):\n        rename(opj(src_dotgit, dot_git_entry),\n               opj(subds_dotgit, dot_git_entry))\n    assert not listdir(src_dotgit)\n    rmdir(src_dotgit)\n\n\n# try retro-fitting GitRepo with deprecated functionality\n# must be done last in this file\ntry:\n    from datalad_deprecated.gitrepo import DeprecatedGitRepoMethods\n    for symbol in dir(DeprecatedGitRepoMethods):\n        if symbol.startswith('__'):\n            # ignore Python internals\n            continue\n        if hasattr(GitRepo, symbol):\n            lgr.debug(\n                'Not retro-fitted GitRepo with deprecated %s, '\n                'name-space conflict', symbol)\n            # do not override existing symbols\n            continue\n        # assign deprecated symbol to GitRepo\n        setattr(GitRepo, symbol, getattr(DeprecatedGitRepoMethods, symbol))\n        lgr.debug('Retro-fitted GitRepo with deprecated %s', symbol)\nexcept ImportError as e:\n    ce = CapturedException(e)\n    lgr.debug(\n        'Not retro-fitting GitRepo with deprecated symbols, '\n        'datalad-deprecated package not found')\n",
            "new_file_content": "# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-\n# ex: set sts=4 ts=4 sw=4 et:\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n#\n#   See COPYING file distributed along with the datalad package for the\n#   copyright and license terms.\n#\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n\"\"\"Internal low-level interface to Git repositories\n\n\"\"\"\n\nimport logging\nimport os\nimport os.path as op\nimport posixpath\nimport re\nimport warnings\nfrom collections.abc import Mapping\nfrom functools import wraps\nfrom itertools import chain\nfrom os import linesep\nfrom os.path import (\n    commonprefix,\n    curdir,\n    dirname,\n    exists,\n    isabs,\n)\nfrom os.path import join as opj\nfrom os.path import (\n    pardir,\n    relpath,\n    sep,\n)\n\nimport datalad.utils as ut\nfrom datalad import ssh_manager\nfrom datalad.cmd import (\n    BatchedCommand,\n    GitWitlessRunner,\n    NoCapture,\n    StdOutErrCapture,\n    WitlessProtocol,\n)\nfrom datalad.config import (\n    parse_gitconfig_dump,\n    write_config_section,\n)\nfrom datalad.consts import (\n    ILLEGAL_CHARS_WIN,\n    RESERVED_NAMES_WIN,\n)\nfrom datalad.core.local.repo import repo_from_path\nfrom datalad.dataset.gitrepo import GitRepo as CoreGitRepo\nfrom datalad.dataset.gitrepo import (\n    _get_dot_git,\n    path_based_str_repr,\n)\nfrom datalad.log import log_progress\nfrom datalad.support.due import (\n    Doi,\n    due,\n)\nfrom datalad.utils import (\n    Path,\n    PurePosixPath,\n    ensure_dir,\n    ensure_list,\n    ensure_unicode,\n    generate_file_chunks,\n    getpwd,\n    is_interactive,\n    on_windows,\n    optional_args,\n    path_is_subpath,\n    posix_relpath,\n)\n\nfrom .exceptions import (\n    CapturedException,\n    CommandError,\n    FileNotInRepositoryError,\n    InvalidGitReferenceError,\n    InvalidGitRepositoryError,\n    NoSuchPathError,\n)\n# imports from same module:\nfrom .external_versions import external_versions\nfrom .network import (\n    RI,\n    PathRI,\n    is_ssh,\n)\nfrom .path import get_parent_paths, get_filtered_paths_\n\n# shortcuts\n_curdirsep = curdir + sep\n_pardirsep = pardir + sep\n\n\nlgr = logging.getLogger('datalad.gitrepo')\n\n\n# outside the repo base classes only used in ConfigManager\ndef to_options(split_single_char_options=True, **kwargs):\n    \"\"\"Transform keyword arguments into a list of cmdline options\n\n    Imported from GitPython.\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n\n    Parameters\n    ----------\n    split_single_char_options: bool\n\n    kwargs:\n\n    Returns\n    -------\n    list\n    \"\"\"\n    def dashify(string):\n        return string.replace('_', '-')\n\n    def transform_kwarg(name, value, split_single_char_options):\n        if len(name) == 1:\n            if value is True:\n                return [\"-%s\" % name]\n            elif value not in (False, None):\n                if split_single_char_options:\n                    return [\"-%s\" % name, \"%s\" % value]\n                else:\n                    return [\"-%s%s\" % (name, value)]\n        else:\n            if value is True:\n                return [\"--%s\" % dashify(name)]\n            elif value is not False and value is not None:\n                return [\"--%s=%s\" % (dashify(name), value)]\n        return []\n\n    args = []\n    kwargs = dict(sorted(kwargs.items(), key=lambda x: x[0]))\n    for k, v in kwargs.items():\n        if isinstance(v, (list, tuple)):\n            for value in v:\n                args += transform_kwarg(k, value, split_single_char_options)\n        else:\n            args += transform_kwarg(k, v, split_single_char_options)\n    return args\n\n\ndef _normalize_path(base_dir, path):\n    \"\"\"Helper to check paths passed to methods of this class.\n\n    Checks whether `path` is beneath `base_dir` and normalizes it.\n    Additionally paths are converted into relative paths with respect to\n    `base_dir`, considering PWD in case of relative paths. This\n    is intended to be used in repository classes, which means that\n    `base_dir` usually will be the repository's base directory.\n\n    Parameters\n    ----------\n    base_dir: str\n        directory to serve as base to normalized, relative paths\n    path: str\n        path to be normalized\n\n    Returns\n    -------\n    str:\n        path, that is a relative path with respect to `base_dir`\n    \"\"\"\n    if not path:\n        return path\n    pathobj = Path(path)\n\n    # do absolute() in addition to always get an absolute path\n    # even with non-existing base_dirs on windows\n    base_dir = str(Path(base_dir).resolve().absolute())  # realpath OK\n\n    # path = normpath(path)\n    # Note: disabled normpath, because it may break paths containing symlinks;\n    # But we don't want to realpath relative paths, in case cwd isn't the\n    # correct base.\n\n    if pathobj.is_absolute():\n        # path might already be a symlink pointing to annex etc,\n        # so realpath only its directory, to get \"inline\" with\n        # realpath(base_dir) above\n        path = str(pathobj.parent.resolve() / pathobj.name)  # realpath OK\n    # Executive decision was made to not do this kind of magic!\n    #\n    # elif commonprefix([realpath(getpwd()), base_dir]) == base_dir:\n    #     # If we are inside repository, rebuilt relative paths.\n    #     path = opj(realpath(getpwd()), path)\n    #\n    # BUT with relative curdir/pardir start it would assume relative to curdir\n    #\n    elif path.startswith(_curdirsep) or path.startswith(_pardirsep):\n        path = str(Path(getpwd()).resolve() / pathobj)  # realpath OK\n    else:\n        # We were called from outside the repo. Therefore relative paths\n        # are interpreted as being relative to self.path already.\n        return path\n\n    if commonprefix([path, base_dir]) != base_dir:\n        raise FileNotInRepositoryError(msg=\"Path outside repository: %s\"\n                                           % base_dir, filename=path)\n\n    return relpath(path, start=base_dir)\n\n\n@optional_args\ndef normalize_path(func):\n    \"\"\"Decorator to provide unified path conversion for a single file\n\n    Unlike normalize_paths, intended to be used for functions dealing with a\n    single filename at a time\n\n    Note\n    ----\n    This is intended to be used within the repository classes and therefore\n    returns a class method!\n\n    The decorated function is expected to take a path at\n    first positional argument (after 'self'). Additionally the class `func`\n    is a member of, is expected to have an attribute 'path'.\n    \"\"\"\n\n    @wraps(func)\n    def  _wrap_normalize_path(self, file_, *args, **kwargs):\n        file_new = _normalize_path(self.path, file_)\n        return func(self, file_new, *args, **kwargs)\n\n    return  _wrap_normalize_path\n\n\n@optional_args\ndef normalize_paths(func, match_return_type=True, map_filenames_back=False,\n                    serialize=False):\n    \"\"\"Decorator to provide unified path conversions.\n\n    Note\n    ----\n    This is intended to be used within the repository classes and therefore\n    returns a class method!\n\n    The decorated function is expected to take a path or a list of paths at\n    first positional argument (after 'self'). Additionally the class `func`\n    is a member of, is expected to have an attribute 'path'.\n\n    Accepts either a list of paths or a single path in a str. Passes a list\n    to decorated function either way, but would return based on the value of\n    match_return_type and possibly input argument.\n\n    If a call to the wrapped function includes normalize_path and it is False\n    no normalization happens for that function call (used for calls to wrapped\n    functions within wrapped functions, while possible CWD is within a\n    repository)\n\n    Parameters\n    ----------\n    match_return_type : bool, optional\n      If True, and a single string was passed in, it would return the first\n      element of the output (after verifying that it is a list of length 1).\n      It makes easier to work with single files input.\n    map_filenames_back : bool, optional\n      If True and returned value is a dictionary, it assumes to carry entries\n      one per file, and then filenames are mapped back to as provided from the\n      normalized (from the root of the repo) paths\n    serialize : bool, optional\n      Loop through files giving only a single one to the function one at a time.\n      This allows to simplify implementation and interface to annex commands\n      which do not take multiple args in the same call (e.g. checkpresentkey)\n    \"\"\"\n\n    @wraps(func)\n    def  _wrap_normalize_paths(self, files, *args, **kwargs):\n\n        normalize = _normalize_path if kwargs.pop('normalize_paths', True) \\\n            else lambda rpath, filepath: filepath\n\n        if files:\n            if isinstance(files, str) or not files:\n                files_new = [normalize(self.path, files)]\n                single_file = True\n            elif isinstance(files, list):\n                files_new = [normalize(self.path, path) for path in files]\n                single_file = False\n            else:\n                raise ValueError(\"_files_decorator: Don't know how to handle \"\n                                 \"instance of %s.\" % type(files))\n        else:\n            single_file = None\n            files_new = []\n\n        if map_filenames_back:\n            def remap_filenames(out):\n                \"\"\"Helper to map files back to non-normalized paths\"\"\"\n                if isinstance(out, dict):\n                    assert(len(out) == len(files_new))\n                    files_ = [files] if single_file else files\n                    mapped = out.__class__()\n                    for fin, fout in zip(files_, files_new):\n                        mapped[fin] = out[fout]\n                    return mapped\n                else:\n                    return out\n        else:\n            remap_filenames = lambda x: x\n\n        if serialize:  # and not single_file:\n            result = [\n                func(self, f, *args, **kwargs)\n                for f in files_new\n            ]\n        else:\n            result = func(self, files_new, *args, **kwargs)\n\n        if single_file is None:\n            # no files were provided, nothing we can do really\n            return result\n        elif (result is None) or not match_return_type or not single_file:\n            # If function doesn't return anything or no denormalization\n            # was requested or it was not a single file\n            return remap_filenames(result)\n        elif single_file:\n            if len(result) != 1:\n                # Magic doesn't apply\n                return remap_filenames(result)\n            elif isinstance(result, (list, tuple)):\n                return result[0]\n            elif isinstance(result, dict) and tuple(result)[0] == files_new[0]:\n                # assume that returned dictionary has files as keys.\n                return tuple(result.values())[0]\n            else:\n                # no magic can apply\n                return remap_filenames(result)\n        else:\n            return RuntimeError(\"should have not got here... check logic\")\n\n    return  _wrap_normalize_paths\n\n\nif \"2.24.0\" <= external_versions[\"cmd:git\"] < \"2.25.0\":\n    # An unintentional change in Git 2.24.0 led to `ls-files -o` traversing\n    # into untracked submodules when multiple pathspecs are given, returning\n    # repositories that are deeper than the first level. This helper filters\n    # these deeper levels out so that save_() doesn't fail trying to add them.\n    #\n    # This regression fixed with upstream's 072a231016 (2019-12-10).\n    def _prune_deeper_repos(repos):\n        firstlevel_repos = []\n        prev = None\n        for repo in sorted(repos):\n            if not (prev and str(repo).startswith(prev)):\n                prev = str(repo)\n                firstlevel_repos.append(repo)\n        return firstlevel_repos\nelse:\n    def _prune_deeper_repos(repos):\n        return repos\n\n\nclass GitProgress(WitlessProtocol):\n    \"\"\"Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n    # inform super-class to capture stderr\n    proc_err = True\n\n    _num_op_codes = 10\n    BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT, ENUMERATING = \\\n        [1 << x for x in range(_num_op_codes)]\n    STAGE_MASK = BEGIN | END\n    OP_MASK = ~STAGE_MASK\n\n    DONE_TOKEN = 'done.'\n    TOKEN_SEPARATOR = ', '\n\n    _known_ops = {\n        COUNTING: (\"Counting\", \"Objects\"),\n        ENUMERATING: (\"Enumerating\", \"Objects\"),\n        COMPRESSING: (\"Compressing\", \"Objects\"),\n        WRITING: (\"Writing\", \"Objects\"),\n        RECEIVING: (\"Receiving\", \"Objects\"),\n        RESOLVING: (\"Resolving\", \"Deltas\"),\n        FINDING_SOURCES: (\"Finding\", \"Sources\"),\n        CHECKING_OUT: (\"Check out\", \"Things\"),\n    }\n\n    __slots__ = ('_seen_ops', '_pbars', '_encoding')\n\n    re_op_absolute = re.compile(r\"(remote: )?([\\w\\s]+):\\s+()(\\d+)()(.*)\")\n    re_op_relative = re.compile(r\"(remote: )?([\\w\\s]+):\\s+(\\d+)% \\((\\d+)/(\\d+)\\)(.*)\")\n\n    def __init__(self, *args):\n        super().__init__(*args)\n        self._unprocessed = None\n\n    def connection_made(self, transport):\n        super().connection_made(transport)\n        self._seen_ops = []\n        self._pbars = set()\n\n    def process_exited(self):\n        # take down any progress bars that were not closed orderly\n        for pbar_id in self._pbars:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Finished',\n            )\n        super().process_exited()\n\n    def pipe_data_received(self, fd, byts):\n        # progress reports only come from stderr\n        if fd != 2:\n            # let the base class decide what to do with it\n            super().pipe_data_received(fd, byts)\n            return\n        for line in byts.splitlines(keepends=True):\n            # put any unprocessed content back in front\n            line = self._unprocessed + line if self._unprocessed else line\n            self._unprocessed = None\n            if not self._parse_progress_line(line):\n                # anything that doesn't look like a progress report\n                # is retained and returned\n                # in case of partial progress lines, this can lead to\n                # leakage of progress info into the output, but\n                # it is better to enable better (maybe more expensive)\n                # subsequent filtering than hiding lines with\n                # unknown, potentially important info\n                lgr.debug('Non-progress stderr: %s', line)\n                if line.endswith((b'\\r', b'\\n')):\n                    # complete non-progress line, pass on\n                    super().pipe_data_received(fd, line)\n                else:\n                    # an incomplete line, maybe the next batch completes\n                    # it to become a recognizable progress report\n                    self._unprocessed = line\n\n    def _parse_progress_line(self, line):\n        \"\"\"Process a single line\n\n        Parameters\n        ----------\n        line : bytes\n\n        Returns\n        -------\n        bool\n          Flag whether the line was recognized as a Git progress report.\n        \"\"\"\n        # handle\n        # Counting objects: 4, done.\n        # Compressing objects:  50% (1/2)\n        # Compressing objects: 100% (2/2)\n        # Compressing objects: 100% (2/2), done.\n        line = line.decode(self.encoding) if isinstance(line, bytes) else line\n        if line.startswith(('warning:', 'error:', 'fatal:')):\n            return False\n\n        # find escape characters and cut them away - regex will not work with\n        # them as they are non-ascii. As git might expect a tty, it will send them\n        last_valid_index = None\n        for i, c in enumerate(reversed(line)):\n            if ord(c) < 32:\n                # its a slice index\n                last_valid_index = -i - 1\n            # END character was non-ascii\n        # END for each character in line\n        if last_valid_index is not None:\n            line = line[:last_valid_index]\n        # END cut away invalid part\n        line = line.rstrip()\n\n        cur_count, max_count = None, None\n        match = self.re_op_relative.match(line)\n        if match is None:\n            match = self.re_op_absolute.match(line)\n\n        if not match:\n            return False\n        # END could not get match\n\n        op_code = 0\n        _remote, op_name, _percent, cur_count, max_count, message = match.groups()\n\n        # get operation id\n        if op_name == \"Counting objects\":\n            op_code |= self.COUNTING\n        elif op_name == \"Compressing objects\":\n            op_code |= self.COMPRESSING\n        elif op_name == \"Writing objects\":\n            op_code |= self.WRITING\n        elif op_name == 'Receiving objects':\n            op_code |= self.RECEIVING\n        elif op_name == 'Resolving deltas':\n            op_code |= self.RESOLVING\n        elif op_name == 'Finding sources':\n            op_code |= self.FINDING_SOURCES\n        elif op_name == 'Checking out files':\n            op_code |= self.CHECKING_OUT\n        elif op_name == 'Enumerating objects':\n            op_code |= self.ENUMERATING\n        else:\n            # Note: On windows it can happen that partial lines are sent\n            # Hence we get something like \"CompreReceiving objects\", which is\n            # a blend of \"Compressing objects\" and \"Receiving objects\".\n            # This can't really be prevented.\n            lgr.debug(\n                'Output line matched a progress report of an unknown type: %s',\n                line)\n            # TODO investigate if there is any chance that we might swallow\n            # important info -- until them do not flag this line\n            # as progress\n            return False\n        # END handle op code\n\n        pbar_id = 'gitprogress-{}-{}'.format(id(self), op_code)\n\n        op_props = self._known_ops[op_code]\n\n        # figure out stage\n        if op_code not in self._seen_ops:\n            self._seen_ops.append(op_code)\n            op_code |= self.BEGIN\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start {} {}'.format(\n                    op_props[0].lower(),\n                    op_props[1].lower(),\n                ),\n                label=op_props[0],\n                unit=' {}'.format(op_props[1]),\n                total=float(max_count) if max_count else None,\n            )\n            self._pbars.add(pbar_id)\n        # END begin opcode\n\n        if message is None:\n            message = ''\n        # END message handling\n\n        done_progress = False\n        message = message.strip()\n        if message.endswith(self.DONE_TOKEN):\n            op_code |= self.END\n            message = message[:-len(self.DONE_TOKEN)]\n            done_progress = True\n        # END end message handling\n        message = message.strip(self.TOKEN_SEPARATOR)\n\n        if cur_count and max_count:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                line,\n                update=float(cur_count),\n                noninteractive_level=logging.DEBUG,\n            )\n\n        if done_progress:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Finished {} {}'.format(\n                    op_props[0].lower(),\n                    op_props[1].lower(),\n                ),\n                noninteractive_level=logging.DEBUG,\n            )\n            self._pbars.discard(pbar_id)\n        return True\n\n\nclass StdOutCaptureWithGitProgress(GitProgress):\n    proc_out = True\n\n\nclass FetchInfo(dict):\n    \"\"\"\n    dict that carries results of a fetch operation of a single head\n\n    Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n\n    NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \\\n        FAST_FORWARD, ERROR = [1 << x for x in range(8)]\n\n    _re_fetch_result = re.compile(r'^\\s*(.) (\\[?[\\w\\s\\.$@]+\\]?)\\s+(.+) [-> ]+ ([^\\s]+)(    \\(.*\\)?$)?')\n\n    _flag_map = {\n        '!': ERROR,\n        '+': FORCED_UPDATE,\n        '*': 0,\n        '=': HEAD_UPTODATE,\n        ' ': FAST_FORWARD,\n        '-': TAG_UPDATE,\n    }\n    _operation_map = {\n        NEW_TAG: 'new-tag',\n        NEW_HEAD: 'new-branch',\n        HEAD_UPTODATE: 'uptodate',\n        TAG_UPDATE: 'tag-update',\n        REJECTED: 'rejected',\n        FORCED_UPDATE: 'forced-update',\n        FAST_FORWARD: 'fast-forward',\n        ERROR: 'error',\n    }\n\n    @classmethod\n    def _from_line(cls, line):\n        \"\"\"Parse information from the given line as returned by git-fetch -v\n        and return a new FetchInfo object representing this information.\n        \"\"\"\n        match = cls._re_fetch_result.match(line)\n        if match is None:\n            raise ValueError(\"Failed to parse line: %r\" % line)\n\n        # parse lines\n        control_character, operation, local_remote_ref, remote_local_ref, note = \\\n            match.groups()\n\n        # parse flags from control_character\n        flags = 0\n        try:\n            flags |= cls._flag_map[control_character]\n        except KeyError:\n            raise ValueError(\n                \"Control character %r unknown as parsed from line %r\"\n                % (control_character, line))\n        # END control char exception handling\n\n        # parse operation string for more info - makes no sense for symbolic refs,\n        # but we parse it anyway\n        old_commit = None\n        if 'rejected' in operation:\n            flags |= cls.REJECTED\n        if 'new tag' in operation:\n            flags |= cls.NEW_TAG\n        if 'tag update' in operation:\n            flags |= cls.TAG_UPDATE\n        if 'new branch' in operation:\n            flags |= cls.NEW_HEAD\n        if '...' in operation or '..' in operation:\n            split_token = '...'\n            if control_character == ' ':\n                split_token = split_token[:-1]\n            old_commit = operation.split(split_token)[0]\n        # END handle refspec\n\n        return cls(\n            ref=remote_local_ref.strip(),\n            local_ref=local_remote_ref.strip(),\n            # convert flag int into a list of operation labels\n            operations=[\n                cls._operation_map[o]\n                for o in cls._operation_map.keys()\n                if flags & o\n            ],\n            note=note,\n            old_commit=old_commit,\n        )\n\n\nclass PushInfo(dict):\n    \"\"\"dict that carries results of a push operation of a single head\n\n    Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n    NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \\\n        FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)]\n\n    _flag_map = {'X': NO_MATCH,\n                 '-': DELETED,\n                 '*': 0,\n                 '+': FORCED_UPDATE,\n                 ' ': FAST_FORWARD,\n                 '=': UP_TO_DATE,\n                 '!': ERROR}\n\n    _operation_map = {\n        NEW_TAG: 'new-tag',\n        NEW_HEAD: 'new-branch',\n        NO_MATCH: 'no-match',\n        REJECTED: 'rejected',\n        REMOTE_REJECTED: 'remote-rejected',\n        REMOTE_FAILURE: 'remote-failure',\n        DELETED: 'deleted',\n        FORCED_UPDATE: 'forced-update',\n        FAST_FORWARD: 'fast-forward',\n        UP_TO_DATE: 'uptodate',\n        ERROR: 'error',\n    }\n\n    @classmethod\n    def _from_line(cls, line):\n        \"\"\"Create a new PushInfo instance as parsed from line which is expected to be like\n            refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes\"\"\"\n        control_character, from_to, summary = line.split('\\t', 3)\n        flags = 0\n\n        # control character handling\n        try:\n            flags |= cls._flag_map[control_character]\n        except KeyError:\n            raise ValueError(\"Control character %r unknown as parsed from line %r\" % (control_character, line))\n        # END handle control character\n\n        # from_to handling\n        from_ref_string, to_ref_string = from_to.split(':')\n\n        # commit handling, could be message or commit info\n        old_commit = None\n        if summary.startswith('['):\n            if \"[rejected]\" in summary:\n                flags |= cls.REJECTED\n            elif \"[remote rejected]\" in summary:\n                flags |= cls.REMOTE_REJECTED\n            elif \"[remote failure]\" in summary:\n                flags |= cls.REMOTE_FAILURE\n            elif \"[no match]\" in summary:\n                flags |= cls.ERROR\n            elif \"[new tag]\" in summary:\n                flags |= cls.NEW_TAG\n            elif \"[new branch]\" in summary:\n                flags |= cls.NEW_HEAD\n            # up-to-date encoded in control character\n        else:\n            # fast-forward or forced update - was encoded in control character,\n            # but we parse the old and new commit\n            split_token = \"...\"\n            if control_character == \" \":\n                split_token = \"..\"\n            old_sha, _new_sha = summary.split(' ')[0].split(split_token)\n            # have to use constructor here as the sha usually is abbreviated\n            old_commit = old_sha\n        # END message handling\n\n        return cls(\n            from_ref=from_ref_string.strip(),\n            to_ref=to_ref_string.strip(),\n            # convert flag int into a list of operation labels\n            operations=[\n                cls._operation_map[o]\n                for o in cls._operation_map.keys()\n                if flags & o\n            ],\n            note=summary.strip(),\n            old_commit=old_commit,\n        )\n\n\n@path_based_str_repr\nclass GitRepo(CoreGitRepo):\n    \"\"\"Representation of a git repository\n\n    \"\"\"\n    # We must check git config to have name and email set, but\n    # should do it once\n    _config_checked = False\n\n    GIT_MIN_VERSION = \"2.19.1\"\n    git_version = None\n\n    @classmethod\n    def _check_git_version(cls):\n        external_versions.check(\"cmd:git\", min_version=cls.GIT_MIN_VERSION)\n        cls.git_version = external_versions['cmd:git']\n\n    # This is the least common denominator to claim that a user\n    # used DataLad.\n    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262\n    @due.dcite(Doi(\"10.21105/joss.03262\"),\n               # override path since there is no need ATM for such details\n               path=\"datalad\",\n               description=\"DataLad - Data management and distribution platform\")\n    def __init__(self, path, runner=None, create=True,\n                 git_opts=None, repo=None, fake_dates=False,\n                 create_sanity_checks=True,\n                 **kwargs):\n        \"\"\"Creates representation of git repository at `path`.\n\n        Can also be used to create a git repository at `path`.\n\n        Parameters\n        ----------\n        path: str\n          path to the git repository; In case it's not an absolute path,\n          it's relative to PWD\n        create: bool, optional\n          if true, creates a git repository at `path` if there is none. Also\n          creates `path`, if it doesn't exist.\n          If set to false, an exception is raised in case `path` doesn't exist\n          or doesn't contain a git repository.\n        repo: git.Repo, optional\n          This argument is ignored.\n        create_sanity_checks: bool, optional\n          Whether to perform sanity checks during initialization (when\n          `create=True` and target path is not a valid repo already), such as\n          that new repository is not created in the directory where git already\n          tracks some files.\n        kwargs:\n          keyword arguments serving as additional options to the git-init\n          command. Therefore, it makes sense only if called with `create`.\n\n          Generally, this way of passing options to the git executable is\n          (or will be) used a lot in this class. It's a transformation of\n          python-style keyword arguments (or a `dict`) to command line arguments,\n          provided by GitPython.\n\n          A single character keyword will be prefixed by '-', multiple characters\n          by '--'. An underscore in the keyword becomes a dash. The value of the\n          keyword argument is used as the value for the corresponding command\n          line argument. Assigning a boolean creates a flag.\n\n          Examples:\n          no_commit=True => --no-commit\n          C='/my/path'   => -C /my/path\n\n        \"\"\"\n        # this will set up .pathobj and .dot_git\n        super().__init__(path)\n\n        if self.git_version is None:\n            self._check_git_version()\n\n        # BEGIN Repo validity test\n        # We want to fail early for tests, that would be performed a lot. In\n        # particular this is about GitRepo.is_valid_repo. We would use the\n        # latter to decide whether or not to call GitRepo() only for __init__ to\n        # then test the same things again. If we fail early we can save the\n        # additional test from outer scope.\n        self.path = path\n\n        # Note, that the following three path objects are used often and\n        # therefore are stored for performance. Path object creation comes with\n        # a cost. Most notably, this is used for validity checking of the\n        # repository.\n        _valid_repo = self.is_valid_git()\n\n        do_create = False\n        if create and not _valid_repo:\n            if repo is not None:\n                # `repo` passed with `create`, which doesn't make sense\n                raise TypeError(\"argument 'repo' must not be used with 'create'\")\n            do_create = True\n        else:\n            # Note: We used to call gitpy.Repo(path) here, which potentially\n            # raised NoSuchPathError or InvalidGitRepositoryError. This is\n            # used by callers of GitRepo.__init__() to detect whether we have a\n            # valid repo at `path`. Now, with switching to lazy loading property\n            # `repo`, we detect those cases without instantiating a\n            # gitpy.Repo().\n\n            if not exists(path):\n                raise NoSuchPathError(path)\n            if not _valid_repo:\n                raise InvalidGitRepositoryError(path)\n        # END Repo validity test\n\n        # So that we \"share\" control paths with git/git-annex\n        if ssh_manager:\n            ssh_manager.ensure_initialized()\n\n        # note: we may also want to distinguish between a path to the worktree\n        # and the actual repository\n\n        if git_opts is None:\n            git_opts = {}\n        if kwargs:\n            git_opts.update(kwargs)\n\n        self._cfg = None\n\n        if do_create:  # we figured it out earlier\n            from_cmdline = git_opts.pop('_from_cmdline_', [])\n            self.init(\n                sanity_checks=create_sanity_checks,\n                init_options=from_cmdline + to_options(**git_opts),\n            )\n\n        # with DryRunProtocol path might still not exist\n        if exists(self.path):\n            self.inode = os.stat(self.path).st_ino\n        else:\n            self.inode = None\n\n        if fake_dates:\n            self.configure_fake_dates()\n\n    @property\n    def bare(self):\n        \"\"\"Returns a bool indicating whether the repository is bare\n\n        Importantly, this is not reporting the configuration value\n        of 'core.bare', in order to be usable at a stage where a\n        Repo instance is not yet equipped with a ConfigManager.\n        Instead, it is testing whether the repository path and its\n        \"dot_git\" are identical. The value of 'core.bare' can be query\n        from the ConfigManager in a fully initialized instance.\n        \"\"\"\n        return self.pathobj == self.dot_git\n\n    @classmethod\n    def clone(cls, url, path, *args, clone_options=None, **kwargs):\n        \"\"\"Clone url into path\n\n        Provides workarounds for known issues (e.g.\n        https://github.com/datalad/datalad/issues/785)\n\n        Parameters\n        ----------\n        url : str\n        path : str\n        clone_options : dict or list\n          Arbitrary options that will be passed on to the underlying call to\n          `git-clone`. This may be a list of plain options or key-value pairs\n          that will be converted to a list of plain options with `to_options`.\n        expect_fail : bool\n          Whether expect that command might fail, so error should be logged then\n          at DEBUG level instead of ERROR\n        kwargs:\n          Passed to the Repo class constructor.\n        \"\"\"\n\n        if 'repo' in kwargs:\n            raise TypeError(\"argument 'repo' conflicts with cloning\")\n            # TODO: what about 'create'?\n\n        expect_fail = kwargs.pop('expect_fail', False)\n        # fail early on non-empty target:\n        from os import listdir\n        if exists(path) and listdir(path):\n            raise ValueError(\n                \"destination path '%s' already exists and is not an \"\n                \"empty directory.\" % path)\n        else:\n            # protect against cloning into existing and obviously dangling\n            # instance for that location\n            try:\n                del cls._unique_instances[path]\n            except KeyError:\n                # didn't exist - all fine\n                pass\n\n        # Massage URL\n        url_ri = RI(url) if not isinstance(url, RI) else url\n        if on_windows:\n            # When we're cloning from a local path on Windows, the URL at\n            # this point is platform-specific (e.g., \"..\\\\origin\"). According \n            # to Git clone's manpage, clone urls can't have backslashes.\n            # While Git does manage to clone a URL with backslashes, \n            # in the case of subdatasets cloned from relative paths it nevertheless\n            # messed up the resulting remote url, resulting in a mix of\n            # front and backslashes (see also gh-7180): \n            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\\\origin' \n            # Therefore, we're turning it to Posix now.\n            if isinstance(url_ri, PathRI):\n                url = Path(url).as_posix()\n                url_ri = PathRI(url)\n\n        else:\n            # if we are on windows, the local path of a URL\n            # would not end up being a proper local path and cloning\n            # would fail. Don't try to be smart and just pass the\n            # URL along unmodified\n\n            # try to get a local path from `url`:\n            try:\n                url = url_ri.localpath\n                url_ri = RI(url)\n            except ValueError:\n                pass\n\n        if is_ssh(url_ri):\n            ssh_manager.get_connection(url).open()\n        else:\n            if isinstance(url_ri, PathRI):\n                # expand user, because execution not going through a shell\n                # doesn't work well otherwise\n                new_url = os.path.expanduser(url)\n                if url != new_url:\n                    lgr.info(\"Expanded source path to %s from %s\", new_url, url)\n                    url = new_url\n\n        cmd = cls._git_cmd_prefix + ['clone', '--progress']\n        if clone_options:\n            if isinstance(clone_options, Mapping):\n                clone_options = to_options(**clone_options)\n            cmd.extend(clone_options)\n        cmd.extend([url, path])\n\n        fix_annex = None\n        ntries = 5  # 3 is not enough for robust workaround\n        for trial in range(ntries):\n            try:\n                lgr.debug(\"Git clone from %s to %s\", url, path)\n\n                res = GitWitlessRunner().run(cmd, protocol=GitProgress)\n                # fish out non-critical warnings by git-clone\n                # (empty repo clone, etc.), all other content is logged\n                # by the progress helper to 'debug'\n                for errline in res['stderr'].splitlines():\n                    if errline.startswith('warning:'):\n                        lgr.warning(errline[8:].strip())\n                lgr.debug(\"Git clone completed\")\n                break\n            except CommandError as e:\n                # log here but let caller decide what to do\n                ce = CapturedException(e)\n                str_e = str(e)\n                # see https://github.com/datalad/datalad/issues/785\n                if re.search(\"Request for .*aborted.*Unable to find\", str_e,\n                             re.DOTALL) \\\n                        and trial < ntries - 1:\n                    lgr.info(\n                        \"Hit a known issue with Git (see GH#785). Trial #%d, \"\n                        \"retrying\",\n                        trial)\n                    continue\n                    #(lgr.debug if expect_fail else lgr.error)(e_str)\n\n                if \"Clone succeeded, but checkout failed.\" in str_e:\n                    fix_annex = ce\n                    break\n\n                raise\n\n        # get ourselves a repository instance\n        gr = cls(path, *args, **kwargs)\n        if fix_annex:\n            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then\n            # would need to import our own subclass\n            if hasattr(gr, 'is_valid_annex'):\n                lgr.warning(\"Experienced issues while cloning. \"\n                            \"Trying to fix it, using git-annex-fsck.\")\n                if not gr.is_initialized():\n                    gr._init()\n                gr.fsck()\n            else:\n                lgr.warning(\"Experienced issues while cloning: %s\", fix_annex)\n        # ensure that Git doesn't mangle relative paths into obscure absolute\n        # paths: https://github.com/datalad/datalad/issues/3538\n        if isinstance(url_ri, PathRI):\n            url_path = Path(url)\n            if not url_path.is_absolute():\n                # get git-created path\n                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'\n                git_url = gr.config.get(remote_url)\n                if Path(git_url).is_absolute():\n                    # Git created an absolute path from a relative URL.\n                    git_url = op.relpath(git_url, gr.path)\n                path = Path(git_url)\n                # always in POSIX even on Windows\n                path = path.as_posix()\n                gr.config.set(remote_url, path,\n                              scope='local', force=True)\n        return gr\n\n    # Note: __del__ shouldn't be needed anymore as we switched to\n    #       `weakref.finalize`.\n    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods\n    #\n    #       Keeping both methods and this comment around as a reminder to not\n    #       use __del__, if we figure there's a need for cleanup in the future.\n    #\n    # def __del__(self):\n    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird\n    #     # stalls etc\n    #     self._cfg = None\n\n    def is_valid_git(self):\n        \"\"\"Returns whether the underlying repository appears to be still valid\n\n        Note, that this almost identical to the classmethod is_valid_repo().\n        However, if we are testing an existing instance, we can save Path object\n        creations. Since this testing is done a lot, this is relevant. Creation\n        of the Path objects in is_valid_repo() takes nearly half the time of the\n        entire function.\n\n        Also note, that this method is bound to an instance but still\n        class-dependent, meaning that a subclass cannot simply overwrite it.\n        This is particularly important for the call from within __init__(),\n        which in turn is called by the subclasses' __init__. Using an overwrite\n        would lead to the wrong thing being called.\n        \"\"\"\n        return self.is_valid()\n\n    @classmethod\n    def is_valid_repo(cls, path):\n        \"\"\"Returns if a given path points to a git repository\"\"\"\n        return cls.is_valid(path)\n\n    @staticmethod\n    def get_git_dir(repo):\n        \"\"\"figure out a repo's gitdir\n\n        '.git' might be a  directory, a symlink or a file\n\n        Note\n        ----\n        This method is likely to get deprecated, please use GitRepo.dot_git instead!\n        That one's not static, but it's cheaper and you should avoid\n        not having an instance of a repo you're working on anyway.\n        Note, that the property in opposition to this method returns an absolute path.\n\n\n        Parameters\n        ----------\n        repo: path or Repo instance\n          currently expected to be the repos base dir\n\n        Returns\n        -------\n        str\n          relative path to the repo's git dir; So, default would be \".git\"\n        \"\"\"\n        if isinstance(repo, GitRepo):\n            return str(repo.dot_git)\n        pathobj = Path(repo)\n        dot_git = _get_dot_git(pathobj, ok_missing=False)\n        try:\n            dot_git = dot_git.relative_to(pathobj)\n        except ValueError:\n            # is not a subpath, return as is\n            lgr.debug(\"Path %r is not subpath of %r\", dot_git, pathobj)\n        return str(dot_git)\n\n    @property\n    def config(self):\n        # just proxy the core repo APIs property for backward-compatibility\n        return self.cfg\n\n    def is_with_annex(self):\n        \"\"\"Report if GitRepo (assumed) has (remotes with) a git-annex branch\n        \"\"\"\n        return any(\n            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])\n        )\n\n    @classmethod\n    def get_toppath(cls, path, follow_up=True, git_options=None):\n        \"\"\"Return top-level of a repository given the path.\n\n        Parameters\n        -----------\n        follow_up : bool\n          If path has symlinks -- they get resolved by git.  If follow_up is\n          True, we will follow original path up until we hit the same resolved\n          path.  If no such path found, resolved one would be returned.\n        git_options: list of str\n          options to be passed to the git rev-parse call\n\n        Return None if no parent directory contains a git repository.\n        \"\"\"\n        cmd = ['git']\n        if git_options:\n            cmd.extend(git_options)\n        cmd += [\"rev-parse\", \"--show-toplevel\"]\n        try:\n            out = GitWitlessRunner(cwd=path).run(\n                cmd, protocol=StdOutErrCapture)\n            toppath = out['stdout'].rstrip('\\n\\r')\n        except CommandError:\n            return None\n        except OSError:\n            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,\n                                          git_options=git_options)\n\n        # normalize the report, because, e.g. on windows it can come out\n        # with improper directory separators (C:/Users/datalad)\n        toppath = str(Path(toppath))\n\n        if follow_up:\n            path_ = path\n            path_prev = \"\"\n            while path_ and path_ != path_prev:  # on top /.. = /\n                if str(Path(path_).resolve()) == toppath:\n                    toppath = path_\n                    break\n                path_prev = path_\n                path_ = dirname(path_)\n\n        return toppath\n\n    @normalize_paths\n    def add(self, files, git=True, git_options=None, update=False):\n        \"\"\"Adds file(s) to the repository.\n\n        Parameters\n        ----------\n        files: list\n          list of paths to add\n        git: bool\n          somewhat ugly construction to be compatible with AnnexRepo.add();\n          has to be always true.\n        update: bool\n          --update option for git-add. From git's manpage:\n           Update the index just where it already has an entry matching\n           <pathspec>. This removes as well as modifies index entries to match\n           the working tree, but adds no new files.\n\n           If no <pathspec> is given when --update option is used, all tracked\n           files in the entire working tree are updated (old versions of Git\n           used to limit the update to the current directory and its\n           subdirectories).\n\n        Returns\n        -------\n        list\n          Of status dicts.\n        \"\"\"\n        # under all circumstances call this class' add_ (otherwise\n        # AnnexRepo.add would go into a loop\n        return list(GitRepo.add_(self, files, git=git, git_options=git_options,\n                    update=update))\n\n    def add_(self, files, git=True, git_options=None, update=False):\n        \"\"\"Like `add`, but returns a generator\"\"\"\n        # TODO: git_options is used as options for the git-add here,\n        # instead of options to the git executable => rename for consistency\n\n        if not git:\n            lgr.warning(\n                'GitRepo.add() called with git=%s, this should not happen',\n                git)\n            git = True\n\n        # there is no other way then to collect all files into a list\n        # at this point, because we need to pass them at once to a single\n        # `git add` call\n        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]\n\n        if not (files or git_options or update):\n            # wondering why just a warning? in cmdline this is also not an error\n            lgr.warning(\"add was called with empty file list and no options.\")\n            return\n\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.gitaddtoannex to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.gitaddtoannex=false', 'add'] +\n                ensure_list(git_options) +\n                to_options(update=update) + ['--verbose'],\n                files=files,\n                pathspec_from_file=True,\n                read_only=False,\n            )\n            # get all the entries\n            for o in self._process_git_get_output(*add_out):\n                yield o\n            # Note: as opposed to git cmdline, force is True by default in\n            #       gitpython, which would lead to add things, that are\n            #       ignored or excluded otherwise\n            # 2. Note: There is an issue with globbing (like adding '.'),\n            #       which apparently doesn't care for 'force' and therefore\n            #       adds '.git/...'. May be it's expanded at the wrong\n            #       point in time or sth. like that.\n            # For now, use direct call to git add.\n            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,\n            #                      force=False)\n            # TODO: May be make use of 'fprogress'-option to indicate\n            # progress\n            # But then, we don't have it for git-annex add, anyway.\n            #\n            # TODO: Is write=True a reasonable way to do it?\n            # May be should not write until success of operation is\n            # confirmed?\n            # What's best in case of a list of files?\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n        # Make sure return value from GitRepo is consistent with AnnexRepo\n        # currently simulating similar return value, assuming success\n        # for all files:\n        # TODO: Make return values consistent across both *Repo classes!\n        return\n\n    @staticmethod\n    def _process_git_get_output(stdout, stderr=None):\n        \"\"\"Given both outputs (stderr is ignored atm) of git add - process it\n\n        Primarily to centralize handling in both indirect annex and direct\n        modes when ran through proxy\n        \"\"\"\n        return [{u'file': f, u'success': True}\n                for f in re.findall(\"'(.*)'[\\n$]\", ensure_unicode(stdout))]\n\n    @normalize_paths(match_return_type=False)\n    def remove(self, files, recursive=False, **kwargs):\n        \"\"\"Remove files.\n\n        Calls git-rm.\n\n        Parameters\n        ----------\n        files: list of str\n          list of paths to remove\n        recursive: False\n          whether to allow recursive removal from subdirectories\n        kwargs:\n          see `__init__`\n\n        Returns\n        -------\n        [str]\n          list of successfully removed files.\n        \"\"\"\n        if recursive:\n            kwargs['r'] = True\n\n        # the name is chosen badly, but the purpose is to make sure that\n        # any pending operations actually manifest themselves in the Git repo\n        # on disk (in case of an AnnexRepo, it could be pending batch\n        # processes that need closing)\n        self.precommit()\n\n        # output per removed file is expected to be \"rm 'PATH'\":\n        return [\n            line.strip()[4:-1]\n            for line in self.call_git_items_(\n                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)\n        ]\n\n    def precommit(self):\n        \"\"\"Perform pre-commit maintenance tasks\n        \"\"\"\n        # we used to clean up GitPython here\n        pass\n\n    @staticmethod\n    def _get_prefixed_commit_msg(msg):\n        DATALAD_PREFIX = \"[DATALAD]\"\n        return DATALAD_PREFIX if not msg else \"%s %s\" % (DATALAD_PREFIX, msg)\n\n    def configure_fake_dates(self):\n        \"\"\"Configure repository to use fake dates.\n        \"\"\"\n        lgr.debug(\"Enabling fake dates\")\n        self.config.set(\"datalad.fake-dates\", \"true\")\n\n    @property\n    def fake_dates_enabled(self):\n        \"\"\"Is the repository configured to use fake dates?\n        \"\"\"\n        # this turned into a private property of the CoreGitRepo\n        return self._fake_dates_enabled\n\n    def add_fake_dates(self, env):\n        # was renamed in CoreGitRepo\n        return self.add_fake_dates_to_env(env)\n\n    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,\n               files=None, date=None, index_file=None):\n        \"\"\"Commit changes to git.\n\n        Parameters\n        ----------\n        msg: str, optional\n          commit-message\n        options: list of str, optional\n          cmdline options for git-commit\n        _datalad_msg: bool, optional\n          To signal that commit is automated commit by datalad, so\n          it would carry the [DATALAD] prefix\n        careless: bool, optional\n          if False, raise when there's nothing actually committed;\n          if True, don't care\n        files: list of str, optional\n          path(s) to commit\n        date: str, optional\n          Date in one of the formats git understands\n        index_file: str, optional\n          An alternative index to use\n        \"\"\"\n\n        self.precommit()\n\n        # assemble commandline\n        cmd = ['commit']\n        options = ensure_list(options)\n\n        if date:\n            options += [\"--date\", date]\n\n        orig_msg = msg\n        if not msg:\n            if '--amend' in options:\n                if '--no-edit' not in options:\n                    # don't overwrite old commit message with our default\n                    # message by default, but re-use old one. In other words:\n                    # Make --no-edit the default:\n                    options += [\"--no-edit\"]\n            else:\n                msg = 'Recorded changes'\n                _datalad_msg = True\n\n        if _datalad_msg:\n            msg = self._get_prefixed_commit_msg(msg)\n\n        if msg:\n            options += [\"-m\", msg]\n        cmd.extend(options)\n\n        # set up env for commit\n        env = self.add_fake_dates(None) \\\n            if self.fake_dates_enabled else os.environ.copy()\n        if index_file:\n            env['GIT_INDEX_FILE'] = index_file\n\n        lgr.debug(\"Committing via direct call of git: %s\", cmd)\n\n        prev_sha = self.get_hexsha()\n\n        # Old code was doing clever --amend'ing of chunked series of commits manually\n        # here, but with pathspec_from_file it is no longer needed.\n        # store pre-commit state to be able to check if anything was committed\n        try:\n            # Note: call_git operates via joining call_git_items_ and that one wipes out\n            # .stdout from exception and collects/repopulates stderr only. Let's use\n            # _call_git which returns both outputs and collects/re-populates both stdout\n            # **and** stderr\n            _ = self._call_git(\n                    cmd,\n                    files=files,\n                    env=env,\n                    pathspec_from_file=True,\n            )\n        except CommandError as e:\n            # real errors first\n            if \"did not match any file(s) known to git\" in e.stderr:\n                raise FileNotInRepositoryError(\n                    cmd=e.cmd,\n                    msg=\"File(s) unknown to git\",\n                    code=e.code,\n                    filename=linesep.join([\n                        l for l in e.stderr.splitlines()\n                        if l.startswith(\"error: pathspec\")\n                    ])\n                )\n            # behavior choices now\n            elif not careless:\n                # not willing to compromise at all\n                raise\n            elif 'nothing to commit' in e.stdout:\n                lgr.debug(\"nothing to commit in %s. Ignored.\", self)\n            elif 'no changes added to commit' in e.stdout or \\\n                    'nothing added to commit' in e.stdout:\n                lgr.debug(\"no changes added to commit in %s. Ignored.\", self)\n            else:\n                raise\n        if orig_msg \\\n                or '--dry-run' in cmd \\\n                or prev_sha == self.get_hexsha() \\\n                or ('--amend' in cmd and '--no-edit' in cmd) \\\n                or (not is_interactive()) \\\n                or self.config.obtain('datalad.save.no-message') != 'interactive':\n            # we had a message given, or nothing was committed, or prev. commit\n            # was amended, or we are not connected to a terminal, or no\n            # interactive message input is desired:\n            # we can go home\n            return\n\n        # handle interactive message entry by running another `git-commit`\n        self._git_runner.run(\n            cmd + ['--amend', '--edit'],\n            protocol=NoCapture,\n            stdin=None,\n            env=env,\n        )\n\n    # TODO usage is only in the tests, consider making a test helper and\n    # remove from GitRepo API\n    def get_indexed_files(self):\n        \"\"\"Get a list of files in git's index\n\n        Returns\n        -------\n        list\n            list of paths rooting in git's base dir\n        \"\"\"\n\n        return [\n            str(r.relative_to(self.pathobj))\n            for r in self.get_content_info(\n                paths=None, ref=None, untracked='no')\n        ]\n\n    def format_commit(self, fmt, commitish=None):\n        \"\"\"Return `git show` output for `commitish`.\n\n        Parameters\n        ----------\n        fmt : str\n            A format string accepted by `git show`.\n        commitish: str, optional\n          Any commit identifier (defaults to \"HEAD\").\n\n        Returns\n        -------\n        str or, if there are not commits yet, None.\n        \"\"\"\n        # use git-log and not git-show due to faster performance with\n        # complex commits (e.g. octopus merges)\n        # https://github.com/datalad/datalad/issues/4801\n        cmd = ['log', '-1', '-z', '--format=' + fmt]\n        if commitish is not None:\n            cmd.append(commitish + \"^{commit}\")\n        # make sure Git takes our argument as a revision\n        cmd.append('--')\n        try:\n            stdout = self.call_git(\n                cmd, expect_stderr=True, expect_fail=True,\n                read_only=True)\n        except CommandError as e:\n            if 'bad revision' in e.stderr:\n                raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n            elif 'does not have any commits yet' in e.stderr:\n                return None\n            else:\n                raise e\n        # This trailing null is coming from the -z above, which avoids the\n        # newline that Git would append to the output. We could drop -z and\n        # strip the newline directly, but then we'd have to worry about\n        # compatibility across platforms.\n        return stdout.rsplit(\"\\0\", 1)[0]\n\n    def get_hexsha(self, commitish=None, short=False):\n        \"\"\"Return a hexsha for a given commitish.\n\n        Parameters\n        ----------\n        commitish : str, optional\n          Any identifier that refers to a commit (defaults to \"HEAD\").\n        short : bool, optional\n          Return the abbreviated form of the hexsha.\n\n        Returns\n        -------\n        str or, if no commitish was given and there are no commits yet, None.\n\n        Raises\n        ------\n        ValueError\n          If a commitish was given, but no corresponding commit could be\n          determined.\n        \"\"\"\n        # use --quiet because the 'Needed a single revision' error message\n        # that is the result of running this in a repo with no commits\n        # isn't useful to report\n        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(\n            commitish if commitish else 'HEAD')\n        ]\n        if short:\n            cmd.append('--short')\n        try:\n            return self.call_git_oneline(cmd, read_only=True)\n        except CommandError as e:\n            if commitish is None:\n                return None\n            raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n\n    @normalize_paths(match_return_type=False)\n    def get_last_commit_hexsha(self, files):\n        \"\"\"Return the hash of the last commit the modified any of the given\n        paths\"\"\"\n        try:\n            commit = self.call_git(\n                ['rev-list', '-n1', 'HEAD'],\n                files=files,\n                expect_fail=True,\n                read_only=True,\n            )\n            commit = commit.strip()\n            return commit if commit else None\n        except CommandError:\n            if self.get_hexsha() is None:\n                # unborn branch, don't freak out\n                return None\n            raise\n\n    def get_revisions(self, revrange=None, fmt=\"%H\", options=None):\n        \"\"\"Return list of revisions in `revrange`.\n\n        Parameters\n        ----------\n        revrange : str or list of str or None, optional\n            Revisions or revision ranges to walk. If None, revision defaults to\n            HEAD unless a revision-modifying option like `--all` or\n            `--branches` is included in `options`.\n        fmt : string, optional\n            Format accepted by `--format` option of `git log`. This should not\n            contain new lines because the output is split on new lines.\n        options : list of str, optional\n            Options to pass to `git log`.  This should not include `--format`.\n\n        Returns\n        -------\n        List of revisions (str), formatted according to `fmt`.\n        \"\"\"\n        if revrange is None:\n            revrange = []\n        elif isinstance(revrange, str):\n            revrange = [revrange]\n\n        cmd = [\"log\", \"--format={}\".format(fmt)]\n        cmd.extend((options or []) + revrange + [\"--\"])\n        try:\n            stdout = self.call_git(cmd, expect_fail=True, read_only=True)\n        except CommandError as e:\n            if \"does not have any commits\" in e.stderr:\n                return []\n            raise\n        return stdout.splitlines()\n\n    def commit_exists(self, commitish):\n        \"\"\"Does `commitish` exist in the repo?\n\n        Parameters\n        ----------\n        commitish : str\n            A commit or an object that can be dereferenced to one.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        # Note: The peeling operator \"^{commit}\" is required so that rev-parse\n        # doesn't succeed if passed a full hexsha that is valid but doesn't\n        # exist.\n        return self.call_git_success(\n            [\"rev-parse\", \"--verify\", commitish + \"^{commit}\"],\n            read_only=True,\n        )\n\n    def get_merge_base(self, commitishes):\n        \"\"\"Get a merge base hexsha\n\n        Parameters\n        ----------\n        commitishes: str or list of str\n          List of commitishes (branches, hexshas, etc) to determine the merge\n          base of. If a single value provided, returns merge_base with the\n          current branch.\n\n        Returns\n        -------\n        str or None\n          If no merge-base for given commits, or specified treeish doesn't\n          exist, None returned\n        \"\"\"\n        if isinstance(commitishes, str):\n            commitishes = [commitishes]\n        if not commitishes:\n            raise ValueError(\"Provide at least a single value\")\n        elif len(commitishes) == 1:\n            commitishes = commitishes + [self.get_active_branch()]\n\n        try:\n            base = self.call_git_oneline(['merge-base'] + commitishes,\n                                         read_only=True)\n        except CommandError as exc:\n            if exc.code == 1 and not (exc.stdout or exc.stderr):\n                # No merge base was found (unrelated commits).\n                return None\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                return None\n            raise\n\n        return base\n\n    def is_ancestor(self, reva, revb):\n        \"\"\"Is `reva` an ancestor of `revb`?\n\n        Parameters\n        ----------\n        reva, revb : str\n            Revisions.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        return self.call_git_success(\n            [\"merge-base\", \"--is-ancestor\", reva, revb],\n            read_only=True)\n\n    def get_commit_date(self, branch=None, date='authored'):\n        \"\"\"Get the date stamp of the last commit (in a branch or head otherwise)\n\n        Parameters\n        ----------\n        date: {'authored', 'committed'}\n          Which date to return.  \"authored\" will be the date shown by \"git show\"\n          and the one possibly specified via --date to `git commit`\n\n        Returns\n        -------\n        int or None\n          None if no commit\n        \"\"\"\n        if date == 'committed':\n            format = '%ct'\n        elif date == 'authored':\n            format = '%at'\n        else:\n            raise ValueError('unknow date type: {}'.format(date))\n        d = self.format_commit(format, commitish=branch)\n        return int(d) if d else None\n\n    def get_active_branch(self):\n        \"\"\"Get the name of the active branch\n\n        Returns\n        -------\n        str or None\n          Returns None if there is no active branch, i.e. detached HEAD,\n          and the branch name otherwise.\n        \"\"\"\n        try:\n            out = self.call_git([\"symbolic-ref\", \"HEAD\"], expect_fail=True,\n                                read_only=True)\n        except CommandError as e:\n            if 'HEAD is not a symbolic ref' in e.stderr:\n                lgr.debug(\"detached HEAD in %s\", self)\n                return None\n            else:\n                raise e\n        return out.strip()[11:]  # strip refs/heads/\n\n    def get_corresponding_branch(self, branch=None):\n        \"\"\"Always returns None, a plain GitRepo has no managed branches\"\"\"\n        return None\n\n    def get_branches(self):\n        \"\"\"Get all branches of the repo.\n\n        Returns\n        -------\n        [str]\n            Names of all branches of this repository.\n        \"\"\"\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')\n        ]\n\n    def get_remote_branches(self):\n        \"\"\"Get all branches of all remotes of the repo.\n\n        Returns\n        -----------\n        [str]\n            Names of all remote branches.\n        \"\"\"\n        # TODO: Reconsider melting with get_branches()\n\n        # TODO: treat entries like this: origin/HEAD -> origin/master'\n        # currently this is done in collection\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')\n        ]\n\n    def get_remotes(self, with_urls_only=False):\n        \"\"\"Get known remotes of the repository\n\n        Parameters\n        ----------\n        with_urls_only : bool, optional\n          return only remotes which have urls\n\n        Returns\n        -------\n        remotes : list of str\n          List of names of the remotes\n        \"\"\"\n        from datalad.utils import unique\n\n        self.config.reload()\n        remotes = unique([x[7:] for x in self.config.sections()\n                          if x.startswith(\"remote.\")])\n\n        if with_urls_only:\n            remotes = [\n                r for r in remotes\n                if self.config.get('remote.%s.url' % r)\n            ]\n        return remotes\n\n    # TODO this is practically unused outside the tests, consider turning\n    # into a test helper and trim from the API\n    def get_files(self, branch=None):\n        \"\"\"Get a list of files in git.\n\n        Lists the files in the (remote) branch.\n\n        Parameters\n        ----------\n        branch: str\n          Name of the branch to query. Default: active branch.\n\n        Returns\n        -------\n        [str]\n          list of files.\n        \"\"\"\n        return [\n            str(p.relative_to(self.pathobj))\n            for p in self.get_content_info(\n                paths=None, ref=branch, untracked='no')\n            ]\n\n    def add_remote(self, name, url, options=None):\n        \"\"\"Register remote pointing to a url\n        \"\"\"\n        cmd = ['remote', 'add']\n        if options:\n            cmd += options\n        cmd += [name, url]\n\n        # for historical reasons this method returns stdout and\n        # stderr, keeping that for now\n        result = self._call_git(cmd)\n        self.config.reload()\n        return result\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n\n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n                raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\n            else:\n                raise e\n\n        # config.reload necessary, because the associated remote config\n        # will vanish\n        self.config.reload()\n        return\n\n    def _maybe_open_ssh_connection(self, remote, prefer_push=True):\n        \"\"\"Open connection if `remote` has an SSH URL.\n\n        Doing so enables SSH caching, preventing datalad-sshrun subprocesses\n        from opening (and then closing) their own.\n\n        Parameters\n        ----------\n        remote : str\n        prefer_push : bool, optional\n            Use `remote.<remote>.pushurl` if there is one, falling back to\n            `remote.<remote>.url`.\n        \"\"\"\n        if remote:\n            url = None\n            if prefer_push:\n                url = self.get_remote_url(remote, push=True)\n            url = url or self.get_remote_url(remote)\n            if url and is_ssh(url):\n                ssh_manager.get_connection(url).open()\n\n    def update_remote(self, name=None, verbose=False):\n        \"\"\"\n        \"\"\"\n        options = [\"-v\"] if verbose else []\n        self._maybe_open_ssh_connection(name)\n        name = [name] if name else []\n        self.call_git(\n            ['remote'] + name + ['update'] + options,\n            expect_stderr=True\n        )\n\n    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,\n              **kwargs):\n        \"\"\"Fetches changes from a remote (or all remotes).\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to fetch from. If no remote is given and\n          `all_` is not set, the tracking branch is fetched.\n        refspec : str or list, optional\n          refspec(s) to fetch.\n        all_ : bool, optional\n          fetch all remotes (and all of their branches).\n          Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-fetch.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-fetch.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        return list(\n            self.fetch_(\n                remote=remote,\n                refspec=refspec,\n                all_=all_,\n                git_options=git_options,\n            )\n        )\n\n    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `fetch`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],\n            action='fetch',\n            urlvars=('remote.{}.url', 'remote.{}.url'),\n            protocol=GitProgress,\n            info_cls=FetchInfo,\n            info_from='stderr',\n            add_remote=False,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def push(self, remote=None, refspec=None, all_remotes=False,\n             all_=False, git_options=None, **kwargs):\n        \"\"\"Push changes to a remote (or all remotes).\n\n        If remote and refspec are specified, and remote has\n        `remote.{remote}.datalad-push-default-first` configuration variable\n        set (e.g. by `create-sibling-github`), we will first push the first\n        refspec separately to possibly ensure that the first refspec is chosen\n        by remote as the \"default branch\".\n        See https://github.com/datalad/datalad/issues/4997\n        Upon successful push if this variable was set in the local git config,\n        we unset it, so subsequent pushes would proceed normally.\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to push to. If no remote is given and\n          `all_` is not set, the tracking branch is pushed.\n        refspec : str or list, optional\n          refspec(s) to push.\n        all_ : bool, optional\n          push to all remotes. Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-push.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-push.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        if all_remotes:\n            # be nice to the elderly\n            all_ = True\n\n        push_refspecs = [refspec]\n        cfg = self.config  # shortcut\n        cfg_push_var = \"remote.{}.datalad-push-default-first\".format(remote)\n        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):\n            refspec = ensure_list(refspec)\n            lgr.debug(\"As indicated by %s pushing first refspec %s separately first\",\n                      cfg_push_var, refspec[0])\n            push_refspecs = [[refspec[0]], refspec[1:]]\n\n        push_res = []\n        for refspecs in push_refspecs:\n            push_res.extend(\n                self.push_(\n                    remote=remote,\n                    refspec=refspecs,\n                    all_=all_,\n                    git_options=git_options,\n                )\n            )\n        # note: above push_ should raise exception if errors out\n        if '--dry-run' not in git_options \\\n            and cfg.get_from_source('local', cfg_push_var) is not None:\n            lgr.debug(\"Removing %s variable from local git config after successful push\", cfg_push_var)\n            cfg.unset(cfg_push_var, 'local')\n        return push_res\n\n    def push_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `push`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],\n            action='push',\n            urlvars=('remote.{}.pushurl', 'remote.{}.url'),\n            protocol=StdOutCaptureWithGitProgress,\n            info_cls=PushInfo,\n            info_from='stdout',\n            add_remote=True,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def _fetch_push_helper(\n            self,\n            base_cmd,     # arg list\n            action,       # label fetch|push\n            urlvars,      # variables to query for URLs\n            protocol,     # processor for output\n            info_cls,     # Push|FetchInfo\n            info_from,    # stdout, stderr\n            add_remote,   # whether to add a 'remote' field to the info dict\n            remote=None, refspec=None, all_=False, git_options=None):\n\n        git_options = ensure_list(git_options)\n\n        cmd = base_cmd + git_options\n\n        if remote is None:\n            if refspec:\n                # conflicts with using tracking branch or push all remotes\n                # For now: Just fail.\n                # TODO: May be check whether it fits to tracking branch\n                raise ValueError(\n                    \"refspec specified without a remote. ({})\".format(refspec))\n            if all_:\n                remotes_to_process = self.get_remotes(with_urls_only=True)\n            else:\n                # No explicit remote to fetch.\n                # => get tracking branch:\n                tb_remote, refspec = self.get_tracking_branch()\n                if tb_remote is not None:\n                    remotes_to_process = [tb_remote]\n                else:\n                    # No remote, no tracking branch\n                    # => fail\n                    raise ValueError(\n                        \"Neither a remote is specified to {} \"\n                        \"from nor a tracking branch is set up.\".format(action))\n        else:\n            if all_:\n                raise ValueError(\n                    \"Option 'all_' conflicts with specified remote \"\n                    \"'{}'.\".format(remote))\n            remotes_to_process = [remote]\n\n        if refspec:\n            # prep for appending to cmd\n            refspec = ensure_list(refspec)\n\n        # no need for progress report, when there is just one remote\n        log_remote_progress = len(remotes_to_process) > 1\n        if log_remote_progress:\n            pbar_id = '{}remotes-{}'.format(action, id(self))\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start %sing remotes for %s', action, self,\n                total=len(remotes_to_process),\n                label=action.capitalize(),\n                unit=' Remotes',\n            )\n        try:\n            for remote in remotes_to_process:\n                r_cmd = cmd + [remote]\n                if refspec:\n                    r_cmd += refspec\n\n                if log_remote_progress:\n                    log_progress(\n                        lgr.info,\n                        pbar_id,\n                        '{}ing remote %s'.format(action.capitalize()),\n                        remote,\n                        update=1,\n                        increment=True,\n                    )\n                # best effort to enable SSH connection caching\n                url = self.config.get(\n                    # make two attempts to get a URL\n                    urlvars[0].format(remote),\n                    self.config.get(\n                        urlvars[1].format(remote),\n                        None)\n                )\n                if url and is_ssh(url):\n                    ssh_manager.get_connection(url).open()\n                try:\n                    out = self._git_runner.run(\n                        r_cmd,\n                        protocol=protocol,\n                    )\n                    output = out[info_from] or ''\n                except CommandError as e:\n                    output = None\n                    # intercept some errors that we express as an error report\n                    # in the info dicts\n                    if re.match(\n                            '.*^error: failed to (push|fetch) some refs',\n                            e.stderr,\n                            re.DOTALL | re.MULTILINE):\n                        output = getattr(e, info_from)\n                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()\n                                          if l.startswith('hint: ')])\n                        if output is None:\n                            output = ''\n                    if not output:\n                        raise\n\n                for line in output.splitlines():\n                    try:\n                        # push info doesn't identify a remote, add it here\n                        pi = info_cls._from_line(line)\n                        if add_remote:\n                            pi['remote'] = remote\n                        # There were errors, but Git provided hints\n                        if 'error' in pi['operations']:\n                            pi['hints'] = hints or None\n                        yield pi\n                    except Exception:\n                        # it is not progress and no push info\n                        # don't hide it completely\n                        lgr.debug('git-%s reported: %s', action, line)\n        finally:\n            if log_remote_progress:\n                log_progress(\n                    lgr.info,\n                    pbar_id,\n                    'Finished %sing remotes for %s', action, self,\n                )\n\n    def get_remote_url(self, name, push=False):\n        \"\"\"Get the url of a remote.\n\n        Reads the configuration of remote `name` and returns its url or None,\n        if there is no url configured.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        push: bool\n          if True, get the pushurl instead of the fetch url.\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        return self.config.get(var, None)\n\n    def set_remote_url(self, name, url, push=False):\n        \"\"\"Set the URL a remote is pointing to\n\n        Sets the URL of the remote `name`. Requires the remote to already exist.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        url: str\n        push: bool\n          if True, set the push URL, otherwise the fetch URL\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        self.config.set(var, url, scope='local', reload=True)\n\n    def get_branch_commits_(self, branch=None, limit=None, stop=None):\n        \"\"\"Return commit hexshas for a branch\n\n        Parameters\n        ----------\n        branch: str, optional\n          If not provided, assumes current branch\n        limit: None | 'left-only', optional\n          Limit which commits to report.  If None -- all commits (merged or not),\n          if 'left-only' -- only the commits from the left side of the tree upon\n          merges\n        stop: str, optional\n          hexsha of the commit at which stop reporting (matched one is not\n          reported either)\n\n        Yields\n        ------\n        str\n        \"\"\"\n        cmd = ['rev-list']\n        if limit == 'left-only':\n            cmd.append('--left-only')\n        if not branch:\n            branch = self.get_active_branch()\n        cmd.append(branch)\n        # and trailing -- marker to make sure that Git never confused the branch\n        # with a potentially existing directory of the same name\n        cmd.append('--')\n        for r in self.call_git_items_(cmd):\n            if stop and stop == r:\n                return\n            yield r\n\n    def checkout(self, name, options=None):\n        \"\"\"\n        \"\"\"\n        # TODO: May be check for the need of -b options herein?\n        cmd = ['checkout']\n        if options:\n            cmd += options\n        cmd += [str(name)]\n\n        self.call_git(cmd, expect_stderr=True)\n        # checkout can change committed config, or create branch config\n        self.config.reload()\n\n    # TODO: Before implementing annex merge, find usages and check for a needed\n    # change to call super().merge\n    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):\n        if options is None:\n            options = []\n        if msg:\n            options = options + [\"-m\", msg]\n        if allow_unrelated:\n            options += ['--allow-unrelated-histories']\n        self.call_git(\n            ['merge'] + options + [name],\n            **kwargs\n        )\n\n    def remove_branch(self, branch):\n        self.call_git(['branch', '-D', branch])\n\n    def cherry_pick(self, commit):\n        \"\"\"Cherry pick `commit` to the current branch.\n\n        Parameters\n        ----------\n        commit : str\n            A single commit.\n        \"\"\"\n        self.call_git([\"cherry-pick\", commit])\n\n    @property\n    def dirty(self):\n        \"\"\"Is the repository dirty?\n\n        Note: This provides a quick answer when you simply want to know if\n        there are any untracked changes or modifications in this repository or\n        its submodules. For finer-grained control and more detailed reporting,\n        use status() instead.\n        \"\"\"\n        stdout = self.call_git(\n            [\"status\", \"--porcelain\",\n             # Ensure the result isn't influenced by status.showUntrackedFiles.\n             \"--untracked-files=normal\",\n             # Ensure the result isn't influenced by diff.ignoreSubmodules.\n             \"--ignore-submodules=none\"])\n        if bool(stdout.strip()):\n            # The quick `git status`-based check can give a different answer\n            # than `datalad status` for submodules on an adjusted branch.\n            st = self.diffstatus(fr=\"HEAD\" if self.get_hexsha() else None,\n                                 to=None, untracked=\"normal\")\n            return any(r.get(\"state\") != \"clean\" for r in st.values())\n        return False\n\n    @property\n    def untracked_files(self):\n        \"\"\"Legacy interface, do not use! Use the status() method instead.\n\n        Despite its name, it also reports on untracked datasets, and\n        yields their names with trailing path separators.\n        \"\"\"\n        return [\n            '{}{}'.format(\n                str(p.relative_to(self.pathobj)),\n                os.sep if props['type'] != 'file' else ''\n            )\n            for p, props in self.status(\n                    untracked='all', eval_submodule_state='no').items()\n            if props.get('state', None) == 'untracked'\n        ]\n\n    def gc(self, allow_background=False, auto=False):\n        \"\"\"Perform house keeping (garbage collection, repacking)\"\"\"\n        cmd_options = []\n        if not allow_background:\n            cmd_options += ['-c', 'gc.autodetach=0']\n        cmd_options += ['gc', '--aggressive']\n        if auto:\n            cmd_options += ['--auto']\n        self.call_git(cmd_options)\n\n    def _parse_gitmodules(self):\n        # TODO read .gitconfig from Git blob?\n        gitmodules = self.pathobj / '.gitmodules'\n        if not gitmodules.exists():\n            return {}\n        # pull out file content\n        out = self.call_git(\n            ['config', '-z', '-l', '--file', '.gitmodules'],\n            read_only=True)\n        # abuse our config parser\n        # disable multi-value report, because we could not deal with them\n        # anyways, and they should not appear in a normal .gitmodules file\n        # but could easily appear when duplicates are included. In this case,\n        # we better not crash\n        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)\n        mods = {}\n        for k, v in db.items():\n            if not k.startswith('submodule.'):\n                # we don't know what this is\n                lgr.warning(\"Skip unrecognized .gitmodule specification: %s=%s\", k, v)\n                continue\n            k_l = k.split('.')\n            # module name is everything after 'submodule.' that is not the variable\n            # name\n            mod_name = '.'.join(k_l[1:-1])\n            mod = mods.get(mod_name, {})\n            # variable name is the last 'dot-free' segment in the key\n            mod[k_l[-1]] = v\n            mods[mod_name] = mod\n\n        out = {}\n        # bring into traditional shape\n        for name, props in mods.items():\n            if 'path' not in props:\n                lgr.warning(\"Failed to get '%s.path', skipping this submodule\", name)\n                continue\n            modprops = {'gitmodule_{}'.format(k): v\n                        for k, v in props.items()\n                        if not k.startswith('__')}\n            # Keep as PurePosixPath for possible normalization of / in the path etc\n            modpath = PurePosixPath(props['path'])\n            modprops['gitmodule_name'] = name\n            out[modpath] = modprops\n        return out\n\n    def get_submodules_(self, paths=None):\n        \"\"\"Yield submodules in this repository.\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`. Paths must be relative\n            to the resolved repository root, and must be normed to match the\n            reporting done by Git, i.e. no parent dir components\n            (ala \"some/../this\").\n\n        Returns\n        -------\n        A generator that yields a dictionary with information for each\n        submodule.\n        \"\"\"\n        if not (self.pathobj / \".gitmodules\").exists():\n            return\n\n        modinfo = self._parse_gitmodules()\n        if not modinfo:\n            # we exit early, if there is nothing on record (even though\n            # a .gitmodules file exists).\n            # without this conditional exit, we would be able to discover\n            # subprojects even when they are not recorded in .gitmodules,\n            # but at the cost of running an unconstrained ls-files call\n            # below\n            return\n\n        posix_mod_paths = [m.as_posix() for m in modinfo]\n        if paths:\n            # harmonize them into relative to the repository\n            posix_paths = []\n            for path in paths:\n                path = ut.PurePath(path)\n                if path.is_absolute():\n                    try:\n                        path = path.relative_to(self.pathobj)\n                    except ValueError as exc:\n                        lgr.debug(\n                            \"Path %s it not underneath %s, skipping since nothing should match it: %s\",\n                            path, self.pathobj, CapturedException(exc)\n                        )\n                        continue\n                posix_paths.append(path.as_posix())\n\n            # constrain the report by the given paths, make sure all paths are POSIX\n            posix_mod_paths = list(get_filtered_paths_(\n                posix_mod_paths,\n                posix_paths,\n                include_within_path=True,\n            ))\n\n        for r in self.call_git_items_(\n            ['ls-files', '--stage', '-z'],\n            sep='\\0',\n            files=posix_mod_paths,\n            read_only=True,\n            keep_ends=True,\n        ):\n            if not r.startswith('160000'):\n                # make sure this method never talks about non-dataset\n                # content\n                continue\n            props, rpath = r.split('\\t')\n            mode, gitsha, stage = props.split(' ')\n            if stage not in ('0', '2'):\n                # we either have non-merge situation, or a simple merge\n                # situation (i.e. stage=0). the reported gitsha always\n                # matches what we have locally.\n                # or we are in a three-way merge, in which case stage=2\n                # is what we want to report, because it matches the\n                # current HEAD (see git-read-tree manpage).\n                # there is either a stage 2 or stage 0, never both\n                continue\n            # remove the expected line separator from the path\n            rpath = rpath[:-1]\n            path = PurePosixPath(rpath)\n            yield dict(\n                path=self.pathobj / rpath, # full path returned here\n                type='dataset',\n                gitshasum=gitsha,\n                **modinfo.get(path, {})\n            )\n\n    def get_submodules(self, sorted_=True, paths=None):\n        \"\"\"Return list of submodules.\n\n        Parameters\n        ----------\n        sorted_ : bool, optional\n            Sort submodules by path name.\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`.\n\n        Returns\n        -------\n        List of submodule namedtuples if `compat` is true or otherwise a list\n        of dictionaries as returned by `get_submodules_`.\n        \"\"\"\n        xs = self.get_submodules_(paths=paths)\n\n        if sorted_:\n            xs = sorted(xs, key=lambda x: x[\"path\"])\n        return list(xs)\n\n    def update_ref(self, ref, value, oldvalue=None, symbolic=False):\n        \"\"\"Update the object name stored in a ref \"safely\".\n\n        Just a shim for `git update-ref` call if not symbolic, and\n        `git symbolic-ref` if symbolic\n\n        Parameters\n        ----------\n        ref : str\n          Reference, such as `ref/heads/BRANCHNAME` or HEAD.\n        value : str\n          Value to update to, e.g. hexsha of a commit when updating for a\n          branch ref, or branch ref if updating HEAD\n        oldvalue: str\n          Value to update from. Safeguard to be verified by git. This is only\n          valid if `symbolic` is not True.\n        symbolic : None\n          To instruct if ref is symbolic, e.g. should be used in case of\n          ref=HEAD\n        \"\"\"\n        if symbolic:\n            if oldvalue:\n                raise ValueError(\"oldvalue and symbolic must not be given both\")\n            cmd = ['symbolic-ref', ref, value]\n        else:\n            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])\n        self.call_git(cmd)\n\n    def tag(self, tag, message=None, commit=None, options=None):\n        \"\"\"Tag a commit\n\n        Parameters\n        ----------\n        tag : str\n          Custom tag label. Must be a valid tag name.\n        message : str, optional\n          If provided, adds ['-m', <message>] to the list of `git tag`\n          arguments.\n        commit : str, optional\n          If provided, will be appended as last argument to the `git tag` call,\n          and can be used to identify the commit that shall be tagged, if\n          not HEAD.\n        options : list, optional\n          Additional command options, inserted prior a potential `commit`\n          argument.\n        \"\"\"\n        # TODO: call in save.py complains about extensive logging. When does it\n        # happen in what way? Figure out, whether to just silence it or raise or\n        # whatever else.\n        args = ['tag']\n        if message:\n            args += ['-m', message]\n        if options is not None:\n            args.extend(options)\n        args.append(tag)\n        if commit:\n            args.append(commit)\n        self.call_git(args)\n\n    def get_tags(self, output=None):\n        \"\"\"Get list of tags\n\n        Parameters\n        ----------\n        output : str, optional\n          If given, limit the return value to a list of values matching that\n          particular key of the tag properties.\n\n        Returns\n        -------\n        list\n          Each item is a dictionary with information on a tag. At present\n          this includes 'hexsha', and 'name', where the latter is the string\n          label of the tag, and the former the hexsha of the object the tag\n          is attached to. The list is sorted by the creator date (committer\n          date for lightweight tags and tagger date for annotated tags), with\n          the most recent commit being the last element.\n        \"\"\"\n        tags = [\n            dict(\n                name=t['refname:strip=2'],\n                hexsha=t['object'] if t['object'] else t['objectname'],\n            )\n            for t in self.for_each_ref_(\n                fields=['refname:strip=2', 'objectname', 'object'],\n                pattern='refs/tags',\n                sort='creatordate')\n        ]\n        if output:\n            return [t[output] for t in tags]\n        else:\n            return tags\n\n    def describe(self, commitish=None, **kwargs):\n        \"\"\" Quick and dirty implementation to call git-describe\n\n        Parameters\n        ----------\n        kwargs:\n            transformed to cmdline options for git-describe;\n            see __init__ for description of the transformation\n        \"\"\"\n        # TODO: be more precise what failure to expect when and raise actual\n        # errors\n        cmd = ['describe'] + to_options(**kwargs)\n        if commitish is not None:\n            cmd.append(commitish)\n        try:\n            describe = self.call_git(cmd, expect_fail=True)\n            return describe.strip()\n        # TODO: WTF \"catch everything\"?\n        except:\n            return None\n\n    def get_tracking_branch(self, branch=None, remote_only=False):\n        \"\"\"Get the tracking branch for `branch` if there is any.\n\n        Parameters\n        ----------\n        branch: str\n            local branch to look up. If none is given, active branch is used.\n        remote_only : bool\n            Don't return a value if the upstream remote is set to \".\" (meaning\n            this repository).\n\n        Returns\n        -------\n        tuple\n            (remote or None, refspec or None) of the tracking branch\n        \"\"\"\n        if branch is None:\n            branch = self.get_corresponding_branch() or self.get_active_branch()\n            if branch is None:\n                return None, None\n\n        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)\n        if remote_only and track_remote == \".\":\n            return None, None\n        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)\n        return track_remote, track_branch\n\n    @property\n    def count_objects(self):\n        \"\"\"return dictionary with count, size(in KiB) information of git objects\n        \"\"\"\n\n        count_cmd = ['count-objects', '-v']\n        count_str = self.call_git(count_cmd)\n        count = {key: int(value)\n                 for key, value in [item.split(': ')\n                                    for item in count_str.split('\\n')\n                                    if len(item.split(': ')) == 2]}\n        return count\n\n    def get_git_attributes(self):\n        \"\"\"Query gitattributes which apply to top level directory\n\n        It is a thin compatibility/shortcut wrapper around more versatile\n        get_gitattributes which operates on a list of paths and returns\n        a dictionary per each path\n\n        Returns\n        -------\n        dict:\n          a dictionary with attribute name and value items relevant for the\n          top ('.') directory of the repository, and thus most likely the\n          default ones (if not overwritten with more rules) for all files within\n          repo.\n        \"\"\"\n        return self.get_gitattributes('.')['.']\n\n    def get_gitattributes(self, path, index_only=False):\n        \"\"\"Query gitattributes for one or more paths\n\n        Parameters\n        ----------\n        path: path or list\n          Path(s) to query. Paths may be relative or absolute.\n        index_only: bool\n          Flag whether to consider only gitattribute setting that are reflected\n          in the repository index, not just in the work tree content.\n\n        Returns\n        -------\n        dict:\n          Each key is a queried path (always relative to the repository root),\n          each value is a dictionary with attribute\n          name and value items. Attribute values are either True or False,\n          for set and unset attributes, or are the literal attribute value.\n        \"\"\"\n        path = ensure_list(path)\n        cmd = [\"check-attr\", \"-z\", \"--all\"]\n        if index_only:\n            cmd.append('--cached')\n        # make sure we have one entry for each query path to\n        # simplify work with the result\n        attributes = {p: {} for p in path}\n        attr = []\n        for item in self.call_git_items_(cmd, files=path, sep='\\0',\n                                         read_only=True):\n            attr.append(item)\n            if len(attr) < 3:\n                continue\n            # we have a full record\n            p, name, value = attr\n            attrs = attributes[p]\n            attrs[name] = \\\n                True if value == 'set' else False if value == 'unset' else value\n            # done, reset item\n            attr = []\n        return {relpath(k, self.path) if isabs(k) else k: v\n                for k, v in attributes.items()}\n\n    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):\n        \"\"\"Set gitattributes\n\n        By default appends additional lines to `attrfile`. Note, that later\n        lines in `attrfile` overrule earlier ones, which may or may not be\n        what you want. Set `mode` to 'w' to replace the entire file by\n        what you provided in `attrs`.\n\n        Parameters\n        ----------\n        attrs : list\n          Each item is a 2-tuple, where the first element is a path pattern,\n          and the second element is a dictionary with attribute key/value\n          pairs. The attribute dictionary must use the same semantics as those\n          returned by `get_gitattributes()`. Path patterns can use absolute paths,\n          in which case they will be normalized relative to the directory\n          that contains the target .gitattributes file (see `attrfile`).\n        attrfile: path\n          Path relative to the repository root of the .gitattributes file the\n          attributes shall be set in.\n        mode: str\n          'a' to append .gitattributes, 'w' to replace it\n        \"\"\"\n\n        git_attributes_file = op.join(self.path, attrfile)\n        attrdir = op.dirname(git_attributes_file)\n        if not op.exists(attrdir):\n            os.makedirs(attrdir)\n\n        with open(git_attributes_file, mode + '+') as f:\n            # for append, fix existing files that do not end with \\n\n            if mode == 'a' and f.tell():\n                f.seek(max(0, f.tell() - len(os.linesep)))\n                if not f.read().endswith('\\n'):\n                    f.write('\\n')\n\n            for pattern, attr in sorted(attrs, key=lambda x: x[0]):\n                # normalize the pattern relative to the target .gitattributes file\n                npath = _normalize_path(\n                    op.join(self.path, op.dirname(attrfile)), pattern)\n                # paths in gitattributes always have to be POSIX\n                npath = Path(npath).as_posix()\n                attrline = u''\n                if npath.count(' '):\n                    # quote patterns with spaces\n                    attrline += u'\"{}\"'.format(npath.replace('\"', '\\\\\"'))\n                else:\n                    attrline += npath\n                for a in sorted(attr):\n                    val = attr[a]\n                    if val is True:\n                        attrline += ' {}'.format(a)\n                    elif val is False:\n                        attrline += ' -{}'.format(a)\n                    else:\n                        attrline += ' {}={}'.format(a, val)\n                f.write('{}\\n'.format(attrline))\n\n    def get_content_info(self, paths=None, ref=None, untracked='all'):\n        \"\"\"Get identifier and type information from repository content.\n\n        This is simplified front-end for `git ls-files/tree`.\n\n        Both commands differ in their behavior when queried about subdataset\n        paths. ls-files will not report anything, ls-tree will report on the\n        subdataset record. This function uniformly follows the behavior of\n        ls-tree (report on the respective subdataset mount).\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath) or None\n          Specific paths, relative to the resolved repository root, to query\n          info for. Paths must be normed to match the reporting done by Git,\n          i.e. no parent dir components (ala \"some/../this\").\n          If `None`, info is reported for all content.\n        ref : gitref or None\n          If given, content information is retrieved for this Git reference\n          (via ls-tree), otherwise content information is produced for the\n          present work tree (via ls-files). With a given reference, the\n          reported content properties also contain a 'bytesize' record,\n          stating the size of a file in bytes.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when no `ref` was given:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n\n          `gitshasum`\n            SHASUM of the item as tracked by Git, or None, if not\n            tracked. This could be different from the SHASUM of the file\n            in the worktree, if it was modified.\n\n        Raises\n        ------\n        ValueError\n          In case of an invalid Git reference (e.g. 'HEAD' in an empty\n          repository)\n        \"\"\"\n        lgr.debug('%s.get_content_info(...)', self)\n        # TODO limit by file type to replace code in subdatasets command\n        info = dict()\n\n        if paths:  # is not None separate after\n            # path matching will happen against what Git reports\n            # and Git always reports POSIX paths\n            # any incoming path has to be relative already, so we can simply\n            # convert unconditionally\n            # note: will be list-ified below\n            posix_paths = [ut.PurePath(p).as_posix() for p in paths]\n        elif paths is not None:\n            return info\n        else:\n            posix_paths = None\n\n        if posix_paths and (not ref or external_versions[\"cmd:git\"] >= \"2.29.0\"):\n            # If a path points within a submodule, we need to map it to the\n            # containing submodule before feeding it to ls-files or ls-tree.\n            #\n            # Before Git 2.29.0, ls-tree and ls-files differed in how they\n            # reported paths within submodules: ls-files provided no output,\n            # and ls-tree listed the submodule. Now they both return no output.\n            submodules = [s[\"path\"].relative_to(self.pathobj).as_posix()\n                          for s in self.get_submodules_()]\n            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well\n            posix_paths = get_parent_paths(posix_paths, submodules)\n\n        # this will not work in direct mode, but everything else should be\n        # just fine\n        if not ref:\n            # make sure no operations are pending before we figure things\n            # out in the worktree\n            self.precommit()\n\n            # --exclude-standard will make sure to honor and standard way\n            # git can be instructed to ignore content, and will prevent\n            # crap from contaminating untracked file reports\n            cmd = ['ls-files', '--stage', '-z']\n            # untracked report mode, using labels from `git diff` option style\n            if untracked == 'all':\n                cmd += ['--exclude-standard', '-o']\n            elif untracked == 'normal':\n                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']\n            elif untracked == 'no':\n                pass\n            else:\n                raise ValueError(\n                    'unknown value for `untracked`: {}'.format(untracked))\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\\t(?P<fname>.*)$')\n        else:\n            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\\s]*(?P<size>[0-9-]+)\\t(?P<fname>.*)$')\n\n        lgr.debug('Query repo: %s', cmd)\n        try:\n            stdout = self.call_git(\n                cmd,\n                files=posix_paths,\n                expect_fail=True,\n                read_only=True)\n        except CommandError as exc:\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                raise InvalidGitReferenceError(ref)\n            raise\n        lgr.debug('Done query repo: %s', cmd)\n\n        self._get_content_info_line_helper(\n            ref,\n            info,\n            stdout.split('\\0'),\n            props_re)\n\n        lgr.debug('Done %s.get_content_info(...)', self)\n        return info\n\n    def _get_content_info_line_helper(self, ref, info, lines, props_re):\n        \"\"\"Internal helper of get_content_info() to parse Git output\"\"\"\n        mode_type_map = {\n            '100644': 'file',\n            '100755': 'file',\n            '120000': 'symlink',\n            '160000': 'dataset',\n        }\n        for line in lines:\n            if not line:\n                continue\n            inf = {}\n            props = props_re.match(line)\n            if not props:\n                # Kludge: Filter out paths starting with .git/ to work around\n                # an `ls-files -o` bug that was fixed in Git 2.25.\n                #\n                # TODO: Drop this condition when GIT_MIN_VERSION is at least\n                # 2.25.\n                if line.startswith(\".git/\"):\n                    lgr.debug(\"Filtering out .git/ file: %s\", line)\n                    continue\n                # not known to Git, but Git always reports POSIX\n                path = ut.PurePosixPath(line)\n                inf['gitshasum'] = None\n            else:\n                # again Git reports always in POSIX\n                path = ut.PurePosixPath(props.group('fname'))\n\n            # revisit the file props after this path has not been rejected\n            if props:\n                inf['gitshasum'] = props.group('sha')\n                inf['type'] = mode_type_map.get(\n                    props.group('type'), props.group('type'))\n\n                if ref and inf['type'] == 'file':\n                    inf['bytesize'] = int(props.group('size'))\n\n            # join item path with repo path to get a universally useful\n            # path representation with auto-conversion and tons of other\n            # stuff\n            path = self.pathobj.joinpath(path)\n            if 'type' not in inf:\n                # be nice and assign types for untracked content\n                inf['type'] = 'symlink' if path.is_symlink() \\\n                    else 'directory' if path.is_dir() else 'file'\n            info[path] = inf\n\n    def status(self, paths=None, untracked='all', eval_submodule_state='full'):\n        \"\"\"Simplified `git status` equivalent.\n\n        Parameters\n        ----------\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list. If a query path points\n          into a subdataset, a report is made on the subdataset record\n          within the queried dataset only (no recursion).\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        lgr.debug('Query status of %r for %s paths',\n                  self, len(paths) if paths is not None else 'all')\n        return self.diffstatus(\n            fr='HEAD' if self.get_hexsha() else None,\n            to=None,\n            paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state)\n\n    def diff(self, fr, to, paths=None, untracked='all',\n             eval_submodule_state='full'):\n        \"\"\"Like status(), but reports changes between to arbitrary revisions\n\n        Parameters\n        ----------\n        fr : str or None\n          Revision specification (anything that Git understands). Passing\n          `None` considers anything in the target state as new.\n        to : str or None\n          Revision specification (anything that Git understands), or None\n          to compare to the state of the work tree.\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when `to` is None:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        return {k: v for k, v in self.diffstatus(\n            fr=fr, to=to, paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state).items()\n            if v.get('state', None) != 'clean'}\n\n    def diffstatus(self, fr, to, paths=None, untracked='all',\n                   eval_submodule_state='full', _cache=None):\n        \"\"\"Like diff(), but reports the status of 'clean' content too.\n\n        It supports an additional submodule evaluation state 'global'.\n        If given, it will return a single 'modified'\n        (vs. 'clean') state label for the entire repository, as soon as\n        it can.\n        \"\"\"\n        def _get_cache_key(label, paths, ref, untracked=None):\n            return self.path, label, tuple(paths) if paths else None, \\\n                ref, untracked\n\n        if _cache is None:\n            _cache = {}\n\n        if paths is not None:\n            # at this point we must normalize paths to the form that\n            # Git would report them, to easy matching later on\n            paths = map(ut.Path, paths)\n            paths = [\n                p.relative_to(self.pathobj) if p.is_absolute() else p\n                for p in paths\n            ]\n\n        # TODO report more info from get_content_info() calls in return\n        # value, those are cheap and possibly useful to a consumer\n        # we need (at most) three calls to git\n        if to is None:\n            # everything we know about the worktree, including os.stat\n            # for each file\n            key = _get_cache_key('ci', paths, None, untracked)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(\n                    paths=paths, ref=None, untracked=untracked)\n                _cache[key] = to_state\n            # we want Git to tell us what it considers modified and avoid\n            # reimplementing logic ourselves\n            key = _get_cache_key('mod', paths, None)\n            if key in _cache:\n                modified = _cache[key]\n            else:\n                # from Git 2.31.0 onwards ls-files has --deduplicate\n                # by for backward compatibility keep doing deduplication here\n                modified = set(\n                    self.pathobj.joinpath(ut.PurePosixPath(p))\n                    for p in self.call_git_items_(\n                        # we must also look for deleted files, for the logic\n                        # below to work. Only from Git 2.31.0 would they be\n                        # included with `-m` alone\n                        ['ls-files', '-z', '-m', '-d'],\n                        # low-level code cannot handle pathobjs\n                        files=[str(p) for p in paths] if paths is not None else None,\n                        sep='\\0',\n                        read_only=True)\n                    if p)\n                _cache[key] = modified\n        else:\n            key = _get_cache_key('ci', paths, to)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(paths=paths, ref=to)\n                _cache[key] = to_state\n            # we do not need worktree modification detection in this case\n            modified = None\n        # origin state\n        key = _get_cache_key('ci', paths, fr)\n        if key in _cache:\n            from_state = _cache[key]\n        else:\n            if fr:\n                from_state = self.get_content_info(paths=paths, ref=fr)\n            else:\n                # no ref means from nothing\n                from_state = {}\n            _cache[key] = from_state\n\n        status = dict()\n        for f, to_state_r in to_state.items():\n            props = self._diffstatus_get_state_props(\n                f,\n                from_state.get(f, None),\n                to_state_r,\n                # are we comparing against a recorded commit or the worktree\n                to is not None,\n                # if we have worktree modification info, report if\n                # path is reported as modified in it\n                modified and f in modified,\n                eval_submodule_state)\n            # potential early exit in \"global\" eval mode\n            if eval_submodule_state == 'global' and \\\n                    props.get('state', None) not in ('clean', None):\n                # any modification means globally 'modified'\n                return 'modified'\n            status[f] = props\n\n        for f, from_state_r in from_state.items():\n            if f not in to_state:\n                # we new this, but now it is gone and Git is not complaining\n                # about it being missing -> properly deleted and deletion\n                # stages\n                status[f] = dict(\n                    state='deleted',\n                    type=from_state_r['type'],\n                    # report the shasum to distinguish from a plainly vanished\n                    # file\n                    gitshasum=from_state_r['gitshasum'],\n                )\n                if eval_submodule_state == 'global':\n                    return 'modified'\n\n        if to is not None or eval_submodule_state == 'no':\n            # if we have `to` we are specifically comparing against\n            # a recorded state, and this function only attempts\n            # to label the state of a subdataset, not investigate\n            # specifically what the changes in subdatasets are\n            # this is done by a high-level command like rev-diff\n            # so the comparison within this repo and the present\n            # `state` label are all we need, and they are done already\n            if eval_submodule_state == 'global':\n                return 'clean'\n            else:\n                return status\n\n        # loop over all subdatasets and look for additional modifications\n        for f, st in status.items():\n            f = str(f)\n            if 'state' in st or not st['type'] == 'dataset':\n                # no business here\n                continue\n            if not GitRepo.is_valid_repo(f):\n                # submodule is not present, no chance for a conflict\n                st['state'] = 'clean'\n                continue\n            # we have to recurse into the dataset and get its status\n            subrepo = repo_from_path(f)\n            # get the HEAD commit, or the one of the corresponding branch\n            # only that one counts re super-sub relationship\n            # save() syncs the corresponding branch each time\n            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())\n            st['gitshasum'] = subrepo_commit\n            # subdataset records must be labeled clean up to this point\n            # test if current commit in subdataset deviates from what is\n            # recorded in the dataset\n            st['state'] = 'modified' \\\n                if st['prev_gitshasum'] != subrepo_commit \\\n                else 'clean'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n            if eval_submodule_state == 'commit':\n                continue\n            # the recorded commit did not change, so we need to make\n            # a more expensive traversal\n            st['state'] = subrepo.diffstatus(\n                # we can use 'HEAD' because we know that the commit\n                # did not change. using 'HEAD' will facilitate\n                # caching the result\n                fr='HEAD',\n                to=None,\n                paths=None,\n                untracked=untracked,\n                eval_submodule_state='global',\n                _cache=_cache) if st['state'] == 'clean' else 'modified'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n\n        if eval_submodule_state == 'global':\n            return 'clean'\n        else:\n            return status\n\n    def _diffstatus_get_state_props(self, f, from_state, to_state,\n                                    against_commit,\n                                    modified_in_worktree,\n                                    eval_submodule_state):\n        \"\"\"Helper to determine diff properties for a single path\n\n        Parameters\n        ----------\n        f : Path\n        from_state : dict\n        to_state : dict\n        against_commit : bool\n          Flag whether `to_state` reflects a commit or the worktree.\n        modified_in_worktree : bool\n          Flag whether a worktree modification is reported. This is ignored\n          when `against_commit` is True.\n        eval_submodule_state : {'commit', 'no', ...}\n        \"\"\"\n        if against_commit:\n            # we can ignore any worktree modification reported when\n            # comparing against a commit\n            modified_in_worktree = False\n\n        props = {}\n        if 'type' in to_state:\n            props['type'] = to_state['type']\n\n        to_sha = to_state['gitshasum']\n        from_sha = from_state['gitshasum'] if from_state else None\n\n        # determine the state of `f` from from_state and to_state records, if\n        # it can be determined conclusively from it. If not, it will\n        # stay None for now\n        state = None\n        if not from_state:\n            # this is new, or rather not known to the previous state\n            state = 'added' if to_sha else 'untracked'\n        elif to_sha == from_sha and not modified_in_worktree:\n            # something that is seemingly unmodified, based on the info\n            # gathered so far\n            if to_state['type'] == 'dataset':\n                if against_commit or eval_submodule_state == 'commit':\n                    # we compare against a recorded state, just based on\n                    # the shas we can be confident, otherwise the state\n                    # of a subdataset isn't fully known yet, because\n                    # `modified_in_worktree` will only reflect changes\n                    # in the commit of a subdataset without looking into\n                    # it for uncommitted changes. Such tests are done\n                    # later and based on further conditionals for\n                    # performance reasons\n                    state = 'clean'\n            else:\n                # no change in git record, and no change on disk\n                # at this point we know that the reported object ids\n                # for this file are identical in the to and from\n                # records.  If to is None, we're comparing to the\n                # working tree and a deleted file will still have an\n                # identical id, so we need to check whether the file is\n                # gone before declaring it clean. This working tree\n                # check is irrelevant and wrong if to is a ref.\n                state = 'clean' \\\n                    if against_commit or (f.exists() or f.is_symlink()) \\\n                    else 'deleted'\n        else:\n            # change in git record, or on disk\n            # for subdatasets leave the 'modified' judgement to the caller\n            # for supporting corner cases, such as adjusted branch\n            # which require inspection of a subdataset\n            # TODO we could have a new file that is already staged\n            # but had subsequent modifications done to it that are\n            # unstaged. Such file would presently show up as 'added'\n            # ATM I think this is OK, but worth stating...\n            state = ('modified'\n                     if against_commit or to_state['type'] != 'dataset'\n                     else None\n                    ) if f.exists() or f.is_symlink() else 'deleted'\n            # TODO record before and after state for diff-like use\n            # cases\n\n        if state in ('clean', 'added', 'modified', None):\n            # assign present gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['gitshasum'] = to_sha\n            if 'bytesize' in to_state:\n                # if we got this cheap, report it\n                props['bytesize'] = to_state['bytesize']\n            elif state == 'clean' and 'bytesize' in from_state:\n                # no change, we can take this old size info\n                props['bytesize'] = from_state['bytesize']\n        if state in ('clean', 'modified', 'deleted', None):\n            # assign previous gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['prev_gitshasum'] = from_sha\n        if state:\n            # only report a state if we could determine any\n            # outside code tests for existence of the property\n            # and not (always) for the value\n            props['state'] = state\n        return props\n\n    def _save_pre(self, paths, _status, **kwargs):\n        # helper to get an actionable status report\n        if paths is not None and not paths and not _status:\n            return\n        if _status is None:\n            if 'untracked' not in kwargs:\n                kwargs['untracked'] = 'normal'\n            status = self.status(\n                paths=paths,\n                **{k: kwargs[k] for k in kwargs\n                   if k in ('untracked', 'eval_submodule_state')})\n        else:\n            # we want to be able to add items down the line\n            # make sure to detach from prev. owner\n            status = _status.copy()\n        return status\n\n    def get_staged_paths(self):\n        \"\"\"Returns a list of any stage repository path(s)\n\n        This is a rather fast call, as it will not depend on what is going on\n        in the worktree.\n        \"\"\"\n        try:\n            return list(self.call_git_items_(\n                ['diff', '--name-only', '--staged'],\n                expect_stderr=True))\n        except CommandError as e:\n            lgr.debug(CapturedException(e))\n            return []\n\n    def _save_post(self, message, files, partial_commit, amend=False,\n                   allow_empty=False):\n        # helper to commit changes reported in status\n\n        # TODO remove pathobj stringification when commit() can\n        # handle it\n        to_commit = [str(f.relative_to(self.pathobj))\n                     for f in files] \\\n                    if partial_commit else None\n        if not partial_commit or to_commit or allow_empty or \\\n                (amend and message):\n            # we directly call GitRepo.commit() to avoid a whole slew\n            # if direct-mode safeguards and workarounds in the AnnexRepo\n            # implementation (which also run an additional dry-run commit\n            GitRepo.commit(\n                self,\n                files=to_commit,\n                msg=message,\n                options=to_options(amend=amend, allow_empty=allow_empty),\n                # do not raise on empty commit\n                # it could be that the `add` in this save-cycle has already\n                # brought back a 'modified' file into a clean state\n                careless=True,\n            )\n\n    def save(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Save dataset content.\n\n        Parameters\n        ----------\n        message : str or None\n          A message to accompany the changeset in the log. If None,\n          a default message is used.\n        paths : list or None\n          Any content with path matching any of the paths given in this\n          list will be saved. Matching will be performed against the\n          dataset status (GitRepo.status()), or a custom status provided\n          via `_status`. If no paths are provided, ALL non-clean paths\n          present in the repo status or `_status` will be saved.\n        _status : dict or None\n          If None, Repo.status() will be queried for the given `ds`. If\n          a dict is given, its content will be used as a constraint.\n          For example, to save only modified content, but no untracked\n          content, set `paths` to None and provide a `_status` that has\n          no entries for untracked content.\n        **kwargs :\n          Additional arguments that are passed to underlying Repo methods.\n          Supported:\n\n          - git : bool (passed to Repo.add()\n          - eval_submodule_state : {'full', 'commit', 'no'}\n            passed to Repo.status()\n          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()\n          - amend : bool (passed to GitRepo.commit)\n        \"\"\"\n        return list(\n            self.save_(\n                message=message,\n                paths=paths,\n                _status=_status,\n                **kwargs\n            )\n        )\n\n    def save_(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Like `save()` but working as a generator.\"\"\"\n        from datalad.interface.results import get_status_dict\n\n        status_state = _get_save_status_state(\n            self._save_pre(paths, _status, **kwargs) or {}\n        )\n        amend = kwargs.get('amend', False)\n\n        # TODO: check on those None's -- may be those are also \"nothing to worry about\"\n        # and we could just return?\n        if not any(status_state.values()) and not (message and amend):\n            # all clean, nothing todo\n            lgr.debug('Nothing to save in %r, exiting early', self)\n            return\n\n        # three things are to be done:\n        # - remove (deleted if not already staged)\n        # - add (modified/untracked)\n        # - commit (with all paths that have been touched, to bypass\n        #   potential pre-staged bits)\n\n        staged_paths = self.get_staged_paths()\n        need_partial_commit = bool(staged_paths)\n        if need_partial_commit and hasattr(self, \"call_annex\"):\n            # so we have some staged content. let's check which ones\n            # are symlinks -- those could be annex key links that\n            # are broken after a `git-mv` operation\n            # https://github.com/datalad/datalad/issues/4967\n            # call `git-annex pre-commit` on them to rectify this before\n            # saving the wrong symlinks\n            added = status_state['added']\n            tofix = [\n                sp for sp in staged_paths\n                if added.get(self.pathobj / sp, {}).get(\"type\") == \"symlink\"\n            ]\n            if tofix:\n                self.call_annex(['pre-commit'], files=tofix)\n\n        submodule_change = False\n\n        if status_state['deleted']:\n            vanished_subds = [\n                str(f.relative_to(self.pathobj))\n                for f, props in status_state['deleted'].items()\n                if props.get('type') == 'dataset'\n            ]\n            if vanished_subds:\n                # we submodule removal we use `git-rm`, because the clean-up\n                # is more complex than just an index update -- make no\n                # sense to have a duplicate implementation.\n                # we do not yield here, but only altogether below -- we are just\n                # processing gone components, should always be quick.\n                self._call_git(['rm', '-q'], files=vanished_subds)\n                submodule_change = True\n            # remove anything from the index that was found to be gone\n            self._call_git(\n                ['update-index', '--remove'],\n                files=[\n                    str(f.relative_to(self.pathobj))\n                    for f, props in status_state['deleted'].items()\n                    # do not update the index, if there is already\n                    # something staged for this path (e.g.,\n                    # a directory was removed and a file staged\n                    # in its place)\n                    if not props.get('gitshasum')\n                    # we already did the submodules\n                    and props.get('type') != 'dataset'\n                ]\n            )\n            # now yield all deletions\n            for p, props in status_state['deleted'].items():\n                yield get_status_dict(\n                    action='delete',\n                    refds=self.pathobj,\n                    type=props.get('type'),\n                    path=p,\n                    status='ok',\n                    logger=lgr)\n\n        # TODO this additional query should not be, based on status as given\n        # if anyhow possible, however, when paths are given, status may\n        # not contain all required information. In case of path=None AND\n        # _status=None, we should be able to avoid this, because\n        # status should have the full info already\n        # looks for contained repositories\n        untracked_dirs = [\n            f.relative_to(self.pathobj)\n            for f, props in status_state['untracked'].items()\n            if props.get('type', None) == 'directory']\n        to_add_submodules = []\n        if untracked_dirs:\n            to_add_submodules = [\n                sm for sm, sm_props in\n                self.get_content_info(\n                    untracked_dirs,\n                    ref=None,\n                    # request exhaustive list, so that everything that is\n                    # still reported as a directory must be its own repository\n                    untracked='all').items()\n                if sm_props.get('type', None) == 'directory']\n            to_add_submodules = _prune_deeper_repos(to_add_submodules)\n\n        to_stage_submodules = {\n            f: props\n            for f, props in status_state['modified_or_untracked'].items()\n            if props.get('type', None) == 'dataset'}\n        if to_stage_submodules:\n            lgr.debug(\n                '%i submodule path(s) to stage in %r %s',\n                len(to_stage_submodules), self,\n                to_stage_submodules\n                if len(to_stage_submodules) < 10 else '')\n            to_add_submodules += list(to_stage_submodules)\n\n        if to_add_submodules:\n            for r in self._save_add_submodules(to_add_submodules):\n                if r.get('status', None) == 'ok':\n                    submodule_change = True\n                yield r\n\n        if submodule_change:\n            # this will alter the config, reload\n            self.config.reload()\n            # need to include .gitmodules in what needs committing\n            f = self.pathobj.joinpath('.gitmodules')\n            status_state['modified_or_untracked'][f] = \\\n                status_state['modified'][f] = \\\n                dict(type='file', state='modified')\n            # now stage .gitmodules\n            self._call_git(['update-index', '--add'], files=['.gitmodules'])\n            # and report on it\n            yield get_status_dict(\n                action='add',\n                refds=self.pathobj,\n                type='file',\n                path=f,\n                status='ok',\n                logger=lgr)\n\n        to_add = {\n            # TODO remove pathobj stringification when add() can\n            # handle it\n            str(f.relative_to(self.pathobj)): props\n            for f, props in status_state['modified_or_untracked'].items()\n            if not (f in to_add_submodules or f in to_stage_submodules)}\n        if to_add:\n            compat_config = \\\n                self.config.obtain(\"datalad.save.windows-compat-warning\")\n            to_add, problems = self._check_for_win_compat(to_add, compat_config)\n            lgr.debug(\n                '%i path(s) to add to %s %s',\n                len(to_add), self, to_add if len(to_add) < 10 else '')\n\n            if to_add:\n                yield from self._save_add(\n                        to_add,\n                        git_opts=None,\n                        **{k: kwargs[k] for k in kwargs\n                           if k in (('git',) if hasattr(self, 'uuid')\n                                    else tuple())})\n            if problems:\n                from datalad.interface.results import get_status_dict\n                msg = \\\n                    'Incompatible name for Windows systems; disable with ' \\\n                    'datalad.save.windows-compat-warning.',\n                for path in problems:\n                    yield get_status_dict(\n                        action='save',\n                        refds=self.pathobj,\n                        type='file',\n                        path=(self.pathobj / ut.PurePosixPath(path)),\n                        status='impossible',\n                        message=msg,\n                        logger=lgr)\n\n\n        # https://github.com/datalad/datalad/issues/6558\n        # file could have become a directory. Unfortunately git\n        # would then mistakenly refuse to commit if that old path is also\n        # given to commit, so we better filter it out\n        if status_state['deleted'] and status_state['added']:\n            # check if any \"deleted\" is a directory now. Then for those\n            # there should be some other path under that directory in 'added'\n            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:\n                # this could potentially be expensive if lots of files become\n                # directories, but it is unlikely to happen often\n                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish\n                # path_is_subpath faster, also if comparing to \"in f.parents\"\n                f_str = str(f)\n                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):\n                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post\n\n        # Note, that allow_empty is always ok when we amend. Required when we\n        # amend an empty commit while the amendment is empty, too (though\n        # possibly different message). If an empty commit was okay before, it's\n        # okay now.\n        status_state.pop('modified_or_untracked')  # pop the hybrid state\n        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,\n                        allow_empty=amend)\n        # TODO yield result for commit, prev helper checked hexsha pre\n        # and post...\n\n    def _check_for_win_compat(self, files, config):\n        \"\"\"Check file names for illegal characters or reserved names on Windows\n\n        In the case that a non-Windows-compatible file is detected, warn users\n        about potential interoperability issues.\n\n        Parameters\n        ----------\n        files\n          list of files to add\n        config\n          value of self.config.obtain(\"datalad.save.windows-compat-warning\"),\n          used to choose appropriate behavior. \"none\" performs no check,\n          \"warning\" warns in case of incompatibilities, and \"error\" results in\n          an error result in case of incompatibilities\n        \"\"\"\n        # don't perform any check when the configuration is set to 'none'\n        if config == 'none':\n            return files, None\n\n        from collections import defaultdict\n        problems = defaultdict(list)\n        for file in files:\n            for part in Path(file).parts:\n                # check every component of the path for incompatibilities\n                if Path(part).stem.upper() in RESERVED_NAMES_WIN:\n                    problems['Elements using a reserved filename:'].append(part)\n                    problems['paths'].append(file)\n                if re.search(ILLEGAL_CHARS_WIN, part):\n                    problems['Elements with illegal characters:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith('.'):\n                    problems['Elements ending with a dot:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith(' '):\n                    problems['Elements ending with a space:'].append(part)\n                    problems['paths'].append(file)\n        if not problems:\n            return files, None\n        msg = \\\n            \"Some elements of your dataset are not compatible with \" \\\n            \"Windows systems. Disable this check by changing \" \\\n            \"datalad.save.windows-compat-warning or consider renaming \" \\\n            \"the following elements: \"\n        for k, v in problems.items():\n            # use the key as an explanation, and report filenames only once\n            msg += f\"\\n{k} {[*{*v}]}\" if k != 'paths' else ''\n        if config == 'warning':\n            lgr.warning(msg)\n            return files, None\n\n        elif config == 'error':\n            # take the problematic files out of to_add\n            for path in [*{*problems['paths']}]:\n                files.pop(path)\n            return files, [*{*problems['paths']}]\n\n    def _save_add(self, files, git_opts=None):\n        \"\"\"Simple helper to add files in save()\"\"\"\n        from datalad.interface.results import get_status_dict\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.largefiles to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.largefiles=nothing', 'add'] +\n                ensure_list(git_opts) + ['--verbose'],\n                files=list(files.keys()),\n                pathspec_from_file=True,\n            )\n            # get all the entries\n            for r in self._process_git_get_output(*add_out):\n                yield get_status_dict(\n                    action=r.get('command', 'add'),\n                    refds=self.pathobj,\n                    type='file',\n                    path=(self.pathobj / ut.PurePosixPath(r['file']))\n                    if 'file' in r else None,\n                    status='ok' if r.get('success', None) else 'error',\n                    key=r.get('key', None),\n                    # while there is no git-annex underneath here, we\n                    # tend to fake its behavior, so we can also support\n                    # this type of messaging\n                    message='\\n'.join(r['error-messages'])\n                    if 'error-messages' in r else None,\n                    logger=lgr)\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n    def _save_add_submodules(self, paths):\n        \"\"\"Add new submodules, or updates records of existing ones\n\n        This method does not use `git submodule add`, but aims to be more\n        efficient by limiting the scope to mere in-place registration of\n        multiple already present repositories.\n\n        Parameters\n        ----------\n        paths : list(Path)\n\n        Yields\n        ------\n        dict\n          Result records\n        \"\"\"\n        from datalad.interface.results import get_status_dict\n\n        # first gather info from all datasets in read-only fashion, and then\n        # update index, .gitmodules and .git/config at once\n        info = []\n        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843\n        # We must not add already known submodules explicitly since \"untracked\"\n        # can be assigned even for known ones (TODO: add issue, might have been closed)?\n        # Not sure if operating on relative paths would provide any speed up so use full\n        known_sub_paths = {s['path'] for s in self.get_submodules_()}\n        for path in paths:\n            already_known = path in known_sub_paths\n            rpath = str(path.relative_to(self.pathobj).as_posix())\n            subm = repo_from_path(path)\n            # if there is a corresponding branch, we want to record it's state.\n            # we rely on the corresponding branch being synced already.\n            # `save` should do that each time it runs.\n            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())\n            if not subm_commit:\n                yield get_status_dict(\n                    action='add_submodule',\n                    ds=self,\n                    path=path,\n                    status='error',\n                    message=('cannot add subdataset %s with no commits', subm),\n                    logger=lgr)\n                continue\n            # make an attempt to configure a submodule source URL based on the\n            # discovered remote configuration\n            remote, branch = subm.get_tracking_branch()\n            url = subm.get_remote_url(remote) if remote else None\n            if url is None:\n                url = './{}'.format(rpath)\n            subm_id = subm.config.get('datalad.dataset.id', None)\n            info.append(\n                dict(\n                     # if we have additional information on this path, pass it on.\n                     # if not, treat it as an untracked directory\n                     paths[path] if isinstance(paths, dict)\n                     else dict(type='directory', state='untracked'),\n                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,\n                     url=url, known=already_known))\n\n        # bypass any convenience or safe-manipulator for speed reasons\n        # use case: saving many new subdatasets in a single run\n        with (self.pathobj / '.gitmodules').open('a') as gmf, \\\n             (self.pathobj / '.git' / 'config').open('a') as gcf:\n            for i in info:\n                # we update the subproject commit unconditionally\n                self.call_git([\n                    'update-index', '--add', '--replace', '--cacheinfo', '160000',\n                    i['commit'], i['rpath']\n                ])\n                # only write the .gitmodules/.config changes when this is not yet\n                # a subdataset and not yet already known\n                # TODO: we could update the URL, and branch info at this point,\n                # even for previously registered subdatasets\n                if not i['known'] and (i['type'] != 'dataset' or (\n                        i['type'] == 'dataset' and i['state'] == 'untracked')):\n                    gmprops = dict(path=i['rpath'], url=i['url'])\n                    if i['id']:\n                        gmprops['datalad-id'] = i['id']\n                    write_config_section(\n                        gmf, 'submodule', i['rpath'], gmprops)\n                    write_config_section(\n                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))\n\n                # This mirrors the result structure yielded for\n                # to_stage_submodules below.\n                yield get_status_dict(\n                    action='add',\n                    refds=self.pathobj,\n                    type='dataset',\n                    key=None,\n                    path=i['path'],\n                    status='ok',\n                    logger=lgr)\n\n\ndef _get_save_status_state(status):\n    \"\"\"\n    Returns\n    -------\n    dict\n      By status category by file path, mapped to status properties.\n    \"\"\"\n    # Sort status into status by state with explicit list of states\n    # (excluding clean we do not care about) we expect to be present\n    # and which we know of (unless None), and modified_or_untracked hybrid\n    # since it is used below\n    status_state = {\n        k: {}\n        for k in (None,  # not cared of explicitly here\n                  'added',  # not cared of explicitly here\n                  # 'clean'  # not even wanted since nothing to do about those\n                  'deleted',\n                  'modified',\n                  'untracked',\n                  'modified_or_untracked',  # hybrid group created here\n                  )}\n    for f, props in status.items():\n        state = props.get('state', None)\n        if state == 'clean':\n            # we don't care about clean\n            continue\n        if state == 'modified' and props.get('gitshasum') \\\n                and props.get('gitshasum') == props.get('prev_gitshasum'):\n            # reported as modified, but with identical shasums -> typechange\n            # a subdataset maybe? do increasingly expensive tests for\n            # speed reasons\n            if props.get('type') != 'dataset' and f.is_dir() \\\n                    and GitRepo.is_valid_repo(f):\n                # it was not a dataset, but now there is one.\n                # we declare it untracked to engage the discovery tooling.\n                state = 'untracked'\n                props = dict(type='dataset', state='untracked')\n        status_state[state][f] = props\n        # The hybrid one to retain the same order as in original status\n        if state in ('modified', 'untracked'):\n            status_state['modified_or_untracked'][f] = props\n    return status_state\n\n\n# used in in the get command and GitRepo.add_submodule(), the\n# latter is not used outside the tests\ndef _fixup_submodule_dotgit_setup(ds, relativepath):\n    \"\"\"Implementation of our current of .git in a subdataset\n\n    Each subdataset/module has its own .git directory where a standalone\n    repository would have it. No gitdir files, no symlinks.\n    \"\"\"\n    # move .git to superrepo's .git/modules, remove .git, create\n    # .git-file\n    path = opj(ds.path, relativepath)\n    subds_dotgit = opj(path, \".git\")\n\n    repo = GitRepo(path, create=False)\n    if repo.dot_git.parent == repo.pathobj:\n        # this is what we want\n        return\n\n    # first we want to remove any conflicting worktree setup\n    # done by git to find the checkout at the mountpoint of the\n    # submodule, if we keep that, any git command will fail\n    # after we move .git\n    # Ben: Shouldn't we re-setup a possible worktree afterwards?\n    repo.config.unset('core.worktree', scope='local')\n    # what we have here is some kind of reference, remove and\n    # replace by the target\n    os.remove(subds_dotgit)\n    # make absolute\n    src_dotgit = str(repo.dot_git)\n    # move .git\n    from os import (\n        listdir,\n        rename,\n        rmdir,\n    )\n    ensure_dir(subds_dotgit)\n    for dot_git_entry in listdir(src_dotgit):\n        rename(opj(src_dotgit, dot_git_entry),\n               opj(subds_dotgit, dot_git_entry))\n    assert not listdir(src_dotgit)\n    rmdir(src_dotgit)\n\n\n# try retro-fitting GitRepo with deprecated functionality\n# must be done last in this file\ntry:\n    from datalad_deprecated.gitrepo import DeprecatedGitRepoMethods\n    for symbol in dir(DeprecatedGitRepoMethods):\n        if symbol.startswith('__'):\n            # ignore Python internals\n            continue\n        if hasattr(GitRepo, symbol):\n            lgr.debug(\n                'Not retro-fitted GitRepo with deprecated %s, '\n                'name-space conflict', symbol)\n            # do not override existing symbols\n            continue\n        # assign deprecated symbol to GitRepo\n        setattr(GitRepo, symbol, getattr(DeprecatedGitRepoMethods, symbol))\n        lgr.debug('Retro-fitted GitRepo with deprecated %s', symbol)\nexcept ImportError as e:\n    ce = CapturedException(e)\n    lgr.debug(\n        'Not retro-fitting GitRepo with deprecated symbols, '\n        'datalad-deprecated package not found')\n",
            "header": {
                "file": {
                    "path": "datalad/support/gitrepo.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "26fa9f076",
                "new_commit_hash": "bd3b04fe1",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/datalad/support/gitrepo.py"
            },
            "plus_file": {
                "path": "b/datalad/support/gitrepo.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 2246,
                            "length": 7
                        },
                        "new_range": {
                            "start": 2246,
                            "length": 8
                        },
                        "section": "class GitRepo(CoreGitRepo):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            options = []",
                                "type": "context"
                            },
                            {
                                "content": "        if msg:",
                                "type": "context"
                            },
                            {
                                "content": "            options = options + [\"-m\", msg]",
                                "type": "context"
                            },
                            {
                                "content": "        options += ['--allow-unrelated-histories']",
                                "type": "deleted"
                            },
                            {
                                "content": "        if allow_unrelated:",
                                "type": "added"
                            },
                            {
                                "content": "            options += ['--allow-unrelated-histories']",
                                "type": "added"
                            },
                            {
                                "content": "        self.call_git(",
                                "type": "context"
                            },
                            {
                                "content": "            ['merge'] + options + [name],",
                                "type": "context"
                            },
                            {
                                "content": "            **kwargs",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "datalad/support/gitrepo.py",
                            "type": "method",
                            "name": "GitRepo.merge",
                            "content": "class GitRepo(CoreGitRepo):\n    \"\"\"Representation of a git repository\n\n    \"\"\"\n    # We must check git config to have name and email set, but\n    # should do it once\n    _config_checked = False\n\n    GIT_MIN_VERSION = \"2.19.1\"\n    git_version = None\n\n    @classmethod\n    def _check_git_version(cls):\n        external_versions.check(\"cmd:git\", min_version=cls.GIT_MIN_VERSION)\n        cls.git_version = external_versions['cmd:git']\n\n    # This is the least common denominator to claim that a user\n    # used DataLad.\n    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262\n    @due.dcite(Doi(\"10.21105/joss.03262\"),\n               # override path since there is no need ATM for such details\n               path=\"datalad\",\n               description=\"DataLad - Data management and distribution platform\")\n    def __init__(self, path, runner=None, create=True,\n                 git_opts=None, repo=None, fake_dates=False,\n                 create_sanity_checks=True,\n                 **kwargs):\n        \"\"\"Creates representation of git repository at `path`.\n\n        Can also be used to create a git repository at `path`.\n\n        Parameters\n        ----------\n        path: str\n          path to the git repository; In case it's not an absolute path,\n          it's relative to PWD\n        create: bool, optional\n          if true, creates a git repository at `path` if there is none. Also\n          creates `path`, if it doesn't exist.\n          If set to false, an exception is raised in case `path` doesn't exist\n          or doesn't contain a git repository.\n        repo: git.Repo, optional\n          This argument is ignored.\n        create_sanity_checks: bool, optional\n          Whether to perform sanity checks during initialization (when\n          `create=True` and target path is not a valid repo already), such as\n          that new repository is not created in the directory where git already\n          tracks some files.\n        kwargs:\n          keyword arguments serving as additional options to the git-init\n          command. Therefore, it makes sense only if called with `create`.\n\n          Generally, this way of passing options to the git executable is\n          (or will be) used a lot in this class. It's a transformation of\n          python-style keyword arguments (or a `dict`) to command line arguments,\n          provided by GitPython.\n\n          A single character keyword will be prefixed by '-', multiple characters\n          by '--'. An underscore in the keyword becomes a dash. The value of the\n          keyword argument is used as the value for the corresponding command\n          line argument. Assigning a boolean creates a flag.\n\n          Examples:\n          no_commit=True => --no-commit\n          C='/my/path'   => -C /my/path\n\n        \"\"\"\n        # this will set up .pathobj and .dot_git\n        super().__init__(path)\n\n        if self.git_version is None:\n            self._check_git_version()\n\n        # BEGIN Repo validity test\n        # We want to fail early for tests, that would be performed a lot. In\n        # particular this is about GitRepo.is_valid_repo. We would use the\n        # latter to decide whether or not to call GitRepo() only for __init__ to\n        # then test the same things again. If we fail early we can save the\n        # additional test from outer scope.\n        self.path = path\n\n        # Note, that the following three path objects are used often and\n        # therefore are stored for performance. Path object creation comes with\n        # a cost. Most notably, this is used for validity checking of the\n        # repository.\n        _valid_repo = self.is_valid_git()\n\n        do_create = False\n        if create and not _valid_repo:\n            if repo is not None:\n                # `repo` passed with `create`, which doesn't make sense\n                raise TypeError(\"argument 'repo' must not be used with 'create'\")\n            do_create = True\n        else:\n            # Note: We used to call gitpy.Repo(path) here, which potentially\n            # raised NoSuchPathError or InvalidGitRepositoryError. This is\n            # used by callers of GitRepo.__init__() to detect whether we have a\n            # valid repo at `path`. Now, with switching to lazy loading property\n            # `repo`, we detect those cases without instantiating a\n            # gitpy.Repo().\n\n            if not exists(path):\n                raise NoSuchPathError(path)\n            if not _valid_repo:\n                raise InvalidGitRepositoryError(path)\n        # END Repo validity test\n\n        # So that we \"share\" control paths with git/git-annex\n        if ssh_manager:\n            ssh_manager.ensure_initialized()\n\n        # note: we may also want to distinguish between a path to the worktree\n        # and the actual repository\n\n        if git_opts is None:\n            git_opts = {}\n        if kwargs:\n            git_opts.update(kwargs)\n\n        self._cfg = None\n\n        if do_create:  # we figured it out earlier\n            from_cmdline = git_opts.pop('_from_cmdline_', [])\n            self.init(\n                sanity_checks=create_sanity_checks,\n                init_options=from_cmdline + to_options(**git_opts),\n            )\n\n        # with DryRunProtocol path might still not exist\n        if exists(self.path):\n            self.inode = os.stat(self.path).st_ino\n        else:\n            self.inode = None\n\n        if fake_dates:\n            self.configure_fake_dates()\n\n    @property\n    def bare(self):\n        \"\"\"Returns a bool indicating whether the repository is bare\n\n        Importantly, this is not reporting the configuration value\n        of 'core.bare', in order to be usable at a stage where a\n        Repo instance is not yet equipped with a ConfigManager.\n        Instead, it is testing whether the repository path and its\n        \"dot_git\" are identical. The value of 'core.bare' can be query\n        from the ConfigManager in a fully initialized instance.\n        \"\"\"\n        return self.pathobj == self.dot_git\n\n    @classmethod\n    def clone(cls, url, path, *args, clone_options=None, **kwargs):\n        \"\"\"Clone url into path\n\n        Provides workarounds for known issues (e.g.\n        https://github.com/datalad/datalad/issues/785)\n\n        Parameters\n        ----------\n        url : str\n        path : str\n        clone_options : dict or list\n          Arbitrary options that will be passed on to the underlying call to\n          `git-clone`. This may be a list of plain options or key-value pairs\n          that will be converted to a list of plain options with `to_options`.\n        expect_fail : bool\n          Whether expect that command might fail, so error should be logged then\n          at DEBUG level instead of ERROR\n        kwargs:\n          Passed to the Repo class constructor.\n        \"\"\"\n\n        if 'repo' in kwargs:\n            raise TypeError(\"argument 'repo' conflicts with cloning\")\n            # TODO: what about 'create'?\n\n        expect_fail = kwargs.pop('expect_fail', False)\n        # fail early on non-empty target:\n        from os import listdir\n        if exists(path) and listdir(path):\n            raise ValueError(\n                \"destination path '%s' already exists and is not an \"\n                \"empty directory.\" % path)\n        else:\n            # protect against cloning into existing and obviously dangling\n            # instance for that location\n            try:\n                del cls._unique_instances[path]\n            except KeyError:\n                # didn't exist - all fine\n                pass\n\n        # Massage URL\n        url_ri = RI(url) if not isinstance(url, RI) else url\n        if on_windows:\n            # When we're cloning from a local path on Windows, the URL at\n            # this point is platform-specific (e.g., \"..\\\\origin\"). According \n            # to Git clone's manpage, clone urls can't have backslashes.\n            # While Git does manage to clone a URL with backslashes, \n            # in the case of subdatasets cloned from relative paths it nevertheless\n            # messed up the resulting remote url, resulting in a mix of\n            # front and backslashes (see also gh-7180): \n            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\\\origin' \n            # Therefore, we're turning it to Posix now.\n            if isinstance(url_ri, PathRI):\n                url = Path(url).as_posix()\n                url_ri = PathRI(url)\n\n        else:\n            # if we are on windows, the local path of a URL\n            # would not end up being a proper local path and cloning\n            # would fail. Don't try to be smart and just pass the\n            # URL along unmodified\n\n            # try to get a local path from `url`:\n            try:\n                url = url_ri.localpath\n                url_ri = RI(url)\n            except ValueError:\n                pass\n\n        if is_ssh(url_ri):\n            ssh_manager.get_connection(url).open()\n        else:\n            if isinstance(url_ri, PathRI):\n                # expand user, because execution not going through a shell\n                # doesn't work well otherwise\n                new_url = os.path.expanduser(url)\n                if url != new_url:\n                    lgr.info(\"Expanded source path to %s from %s\", new_url, url)\n                    url = new_url\n\n        cmd = cls._git_cmd_prefix + ['clone', '--progress']\n        if clone_options:\n            if isinstance(clone_options, Mapping):\n                clone_options = to_options(**clone_options)\n            cmd.extend(clone_options)\n        cmd.extend([url, path])\n\n        fix_annex = None\n        ntries = 5  # 3 is not enough for robust workaround\n        for trial in range(ntries):\n            try:\n                lgr.debug(\"Git clone from %s to %s\", url, path)\n\n                res = GitWitlessRunner().run(cmd, protocol=GitProgress)\n                # fish out non-critical warnings by git-clone\n                # (empty repo clone, etc.), all other content is logged\n                # by the progress helper to 'debug'\n                for errline in res['stderr'].splitlines():\n                    if errline.startswith('warning:'):\n                        lgr.warning(errline[8:].strip())\n                lgr.debug(\"Git clone completed\")\n                break\n            except CommandError as e:\n                # log here but let caller decide what to do\n                ce = CapturedException(e)\n                str_e = str(e)\n                # see https://github.com/datalad/datalad/issues/785\n                if re.search(\"Request for .*aborted.*Unable to find\", str_e,\n                             re.DOTALL) \\\n                        and trial < ntries - 1:\n                    lgr.info(\n                        \"Hit a known issue with Git (see GH#785). Trial #%d, \"\n                        \"retrying\",\n                        trial)\n                    continue\n                    #(lgr.debug if expect_fail else lgr.error)(e_str)\n\n                if \"Clone succeeded, but checkout failed.\" in str_e:\n                    fix_annex = ce\n                    break\n\n                raise\n\n        # get ourselves a repository instance\n        gr = cls(path, *args, **kwargs)\n        if fix_annex:\n            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then\n            # would need to import our own subclass\n            if hasattr(gr, 'is_valid_annex'):\n                lgr.warning(\"Experienced issues while cloning. \"\n                            \"Trying to fix it, using git-annex-fsck.\")\n                if not gr.is_initialized():\n                    gr._init()\n                gr.fsck()\n            else:\n                lgr.warning(\"Experienced issues while cloning: %s\", fix_annex)\n        # ensure that Git doesn't mangle relative paths into obscure absolute\n        # paths: https://github.com/datalad/datalad/issues/3538\n        if isinstance(url_ri, PathRI):\n            url_path = Path(url)\n            if not url_path.is_absolute():\n                # get git-created path\n                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'\n                git_url = gr.config.get(remote_url)\n                if Path(git_url).is_absolute():\n                    # Git created an absolute path from a relative URL.\n                    git_url = op.relpath(git_url, gr.path)\n                path = Path(git_url)\n                # always in POSIX even on Windows\n                path = path.as_posix()\n                gr.config.set(remote_url, path,\n                              scope='local', force=True)\n        return gr\n\n    # Note: __del__ shouldn't be needed anymore as we switched to\n    #       `weakref.finalize`.\n    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods\n    #\n    #       Keeping both methods and this comment around as a reminder to not\n    #       use __del__, if we figure there's a need for cleanup in the future.\n    #\n    # def __del__(self):\n    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird\n    #     # stalls etc\n    #     self._cfg = None\n\n    def is_valid_git(self):\n        \"\"\"Returns whether the underlying repository appears to be still valid\n\n        Note, that this almost identical to the classmethod is_valid_repo().\n        However, if we are testing an existing instance, we can save Path object\n        creations. Since this testing is done a lot, this is relevant. Creation\n        of the Path objects in is_valid_repo() takes nearly half the time of the\n        entire function.\n\n        Also note, that this method is bound to an instance but still\n        class-dependent, meaning that a subclass cannot simply overwrite it.\n        This is particularly important for the call from within __init__(),\n        which in turn is called by the subclasses' __init__. Using an overwrite\n        would lead to the wrong thing being called.\n        \"\"\"\n        return self.is_valid()\n\n    @classmethod\n    def is_valid_repo(cls, path):\n        \"\"\"Returns if a given path points to a git repository\"\"\"\n        return cls.is_valid(path)\n\n    @staticmethod\n    def get_git_dir(repo):\n        \"\"\"figure out a repo's gitdir\n\n        '.git' might be a  directory, a symlink or a file\n\n        Note\n        ----\n        This method is likely to get deprecated, please use GitRepo.dot_git instead!\n        That one's not static, but it's cheaper and you should avoid\n        not having an instance of a repo you're working on anyway.\n        Note, that the property in opposition to this method returns an absolute path.\n\n\n        Parameters\n        ----------\n        repo: path or Repo instance\n          currently expected to be the repos base dir\n\n        Returns\n        -------\n        str\n          relative path to the repo's git dir; So, default would be \".git\"\n        \"\"\"\n        if isinstance(repo, GitRepo):\n            return str(repo.dot_git)\n        pathobj = Path(repo)\n        dot_git = _get_dot_git(pathobj, ok_missing=False)\n        try:\n            dot_git = dot_git.relative_to(pathobj)\n        except ValueError:\n            # is not a subpath, return as is\n            lgr.debug(\"Path %r is not subpath of %r\", dot_git, pathobj)\n        return str(dot_git)\n\n    @property\n    def config(self):\n        # just proxy the core repo APIs property for backward-compatibility\n        return self.cfg\n\n    def is_with_annex(self):\n        \"\"\"Report if GitRepo (assumed) has (remotes with) a git-annex branch\n        \"\"\"\n        return any(\n            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])\n        )\n\n    @classmethod\n    def get_toppath(cls, path, follow_up=True, git_options=None):\n        \"\"\"Return top-level of a repository given the path.\n\n        Parameters\n        -----------\n        follow_up : bool\n          If path has symlinks -- they get resolved by git.  If follow_up is\n          True, we will follow original path up until we hit the same resolved\n          path.  If no such path found, resolved one would be returned.\n        git_options: list of str\n          options to be passed to the git rev-parse call\n\n        Return None if no parent directory contains a git repository.\n        \"\"\"\n        cmd = ['git']\n        if git_options:\n            cmd.extend(git_options)\n        cmd += [\"rev-parse\", \"--show-toplevel\"]\n        try:\n            out = GitWitlessRunner(cwd=path).run(\n                cmd, protocol=StdOutErrCapture)\n            toppath = out['stdout'].rstrip('\\n\\r')\n        except CommandError:\n            return None\n        except OSError:\n            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,\n                                          git_options=git_options)\n\n        # normalize the report, because, e.g. on windows it can come out\n        # with improper directory separators (C:/Users/datalad)\n        toppath = str(Path(toppath))\n\n        if follow_up:\n            path_ = path\n            path_prev = \"\"\n            while path_ and path_ != path_prev:  # on top /.. = /\n                if str(Path(path_).resolve()) == toppath:\n                    toppath = path_\n                    break\n                path_prev = path_\n                path_ = dirname(path_)\n\n        return toppath\n\n    @normalize_paths\n    def add(self, files, git=True, git_options=None, update=False):\n        \"\"\"Adds file(s) to the repository.\n\n        Parameters\n        ----------\n        files: list\n          list of paths to add\n        git: bool\n          somewhat ugly construction to be compatible with AnnexRepo.add();\n          has to be always true.\n        update: bool\n          --update option for git-add. From git's manpage:\n           Update the index just where it already has an entry matching\n           <pathspec>. This removes as well as modifies index entries to match\n           the working tree, but adds no new files.\n\n           If no <pathspec> is given when --update option is used, all tracked\n           files in the entire working tree are updated (old versions of Git\n           used to limit the update to the current directory and its\n           subdirectories).\n\n        Returns\n        -------\n        list\n          Of status dicts.\n        \"\"\"\n        # under all circumstances call this class' add_ (otherwise\n        # AnnexRepo.add would go into a loop\n        return list(GitRepo.add_(self, files, git=git, git_options=git_options,\n                    update=update))\n\n    def add_(self, files, git=True, git_options=None, update=False):\n        \"\"\"Like `add`, but returns a generator\"\"\"\n        # TODO: git_options is used as options for the git-add here,\n        # instead of options to the git executable => rename for consistency\n\n        if not git:\n            lgr.warning(\n                'GitRepo.add() called with git=%s, this should not happen',\n                git)\n            git = True\n\n        # there is no other way then to collect all files into a list\n        # at this point, because we need to pass them at once to a single\n        # `git add` call\n        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]\n\n        if not (files or git_options or update):\n            # wondering why just a warning? in cmdline this is also not an error\n            lgr.warning(\"add was called with empty file list and no options.\")\n            return\n\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.gitaddtoannex to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.gitaddtoannex=false', 'add'] +\n                ensure_list(git_options) +\n                to_options(update=update) + ['--verbose'],\n                files=files,\n                pathspec_from_file=True,\n                read_only=False,\n            )\n            # get all the entries\n            for o in self._process_git_get_output(*add_out):\n                yield o\n            # Note: as opposed to git cmdline, force is True by default in\n            #       gitpython, which would lead to add things, that are\n            #       ignored or excluded otherwise\n            # 2. Note: There is an issue with globbing (like adding '.'),\n            #       which apparently doesn't care for 'force' and therefore\n            #       adds '.git/...'. May be it's expanded at the wrong\n            #       point in time or sth. like that.\n            # For now, use direct call to git add.\n            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,\n            #                      force=False)\n            # TODO: May be make use of 'fprogress'-option to indicate\n            # progress\n            # But then, we don't have it for git-annex add, anyway.\n            #\n            # TODO: Is write=True a reasonable way to do it?\n            # May be should not write until success of operation is\n            # confirmed?\n            # What's best in case of a list of files?\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n        # Make sure return value from GitRepo is consistent with AnnexRepo\n        # currently simulating similar return value, assuming success\n        # for all files:\n        # TODO: Make return values consistent across both *Repo classes!\n        return\n\n    @staticmethod\n    def _process_git_get_output(stdout, stderr=None):\n        \"\"\"Given both outputs (stderr is ignored atm) of git add - process it\n\n        Primarily to centralize handling in both indirect annex and direct\n        modes when ran through proxy\n        \"\"\"\n        return [{u'file': f, u'success': True}\n                for f in re.findall(\"'(.*)'[\\n$]\", ensure_unicode(stdout))]\n\n    @normalize_paths(match_return_type=False)\n    def remove(self, files, recursive=False, **kwargs):\n        \"\"\"Remove files.\n\n        Calls git-rm.\n\n        Parameters\n        ----------\n        files: list of str\n          list of paths to remove\n        recursive: False\n          whether to allow recursive removal from subdirectories\n        kwargs:\n          see `__init__`\n\n        Returns\n        -------\n        [str]\n          list of successfully removed files.\n        \"\"\"\n        if recursive:\n            kwargs['r'] = True\n\n        # the name is chosen badly, but the purpose is to make sure that\n        # any pending operations actually manifest themselves in the Git repo\n        # on disk (in case of an AnnexRepo, it could be pending batch\n        # processes that need closing)\n        self.precommit()\n\n        # output per removed file is expected to be \"rm 'PATH'\":\n        return [\n            line.strip()[4:-1]\n            for line in self.call_git_items_(\n                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)\n        ]\n\n    def precommit(self):\n        \"\"\"Perform pre-commit maintenance tasks\n        \"\"\"\n        # we used to clean up GitPython here\n        pass\n\n    @staticmethod\n    def _get_prefixed_commit_msg(msg):\n        DATALAD_PREFIX = \"[DATALAD]\"\n        return DATALAD_PREFIX if not msg else \"%s %s\" % (DATALAD_PREFIX, msg)\n\n    def configure_fake_dates(self):\n        \"\"\"Configure repository to use fake dates.\n        \"\"\"\n        lgr.debug(\"Enabling fake dates\")\n        self.config.set(\"datalad.fake-dates\", \"true\")\n\n    @property\n    def fake_dates_enabled(self):\n        \"\"\"Is the repository configured to use fake dates?\n        \"\"\"\n        # this turned into a private property of the CoreGitRepo\n        return self._fake_dates_enabled\n\n    def add_fake_dates(self, env):\n        # was renamed in CoreGitRepo\n        return self.add_fake_dates_to_env(env)\n\n    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,\n               files=None, date=None, index_file=None):\n        \"\"\"Commit changes to git.\n\n        Parameters\n        ----------\n        msg: str, optional\n          commit-message\n        options: list of str, optional\n          cmdline options for git-commit\n        _datalad_msg: bool, optional\n          To signal that commit is automated commit by datalad, so\n          it would carry the [DATALAD] prefix\n        careless: bool, optional\n          if False, raise when there's nothing actually committed;\n          if True, don't care\n        files: list of str, optional\n          path(s) to commit\n        date: str, optional\n          Date in one of the formats git understands\n        index_file: str, optional\n          An alternative index to use\n        \"\"\"\n\n        self.precommit()\n\n        # assemble commandline\n        cmd = ['commit']\n        options = ensure_list(options)\n\n        if date:\n            options += [\"--date\", date]\n\n        orig_msg = msg\n        if not msg:\n            if '--amend' in options:\n                if '--no-edit' not in options:\n                    # don't overwrite old commit message with our default\n                    # message by default, but re-use old one. In other words:\n                    # Make --no-edit the default:\n                    options += [\"--no-edit\"]\n            else:\n                msg = 'Recorded changes'\n                _datalad_msg = True\n\n        if _datalad_msg:\n            msg = self._get_prefixed_commit_msg(msg)\n\n        if msg:\n            options += [\"-m\", msg]\n        cmd.extend(options)\n\n        # set up env for commit\n        env = self.add_fake_dates(None) \\\n            if self.fake_dates_enabled else os.environ.copy()\n        if index_file:\n            env['GIT_INDEX_FILE'] = index_file\n\n        lgr.debug(\"Committing via direct call of git: %s\", cmd)\n\n        prev_sha = self.get_hexsha()\n\n        # Old code was doing clever --amend'ing of chunked series of commits manually\n        # here, but with pathspec_from_file it is no longer needed.\n        # store pre-commit state to be able to check if anything was committed\n        try:\n            # Note: call_git operates via joining call_git_items_ and that one wipes out\n            # .stdout from exception and collects/repopulates stderr only. Let's use\n            # _call_git which returns both outputs and collects/re-populates both stdout\n            # **and** stderr\n            _ = self._call_git(\n                    cmd,\n                    files=files,\n                    env=env,\n                    pathspec_from_file=True,\n            )\n        except CommandError as e:\n            # real errors first\n            if \"did not match any file(s) known to git\" in e.stderr:\n                raise FileNotInRepositoryError(\n                    cmd=e.cmd,\n                    msg=\"File(s) unknown to git\",\n                    code=e.code,\n                    filename=linesep.join([\n                        l for l in e.stderr.splitlines()\n                        if l.startswith(\"error: pathspec\")\n                    ])\n                )\n            # behavior choices now\n            elif not careless:\n                # not willing to compromise at all\n                raise\n            elif 'nothing to commit' in e.stdout:\n                lgr.debug(\"nothing to commit in %s. Ignored.\", self)\n            elif 'no changes added to commit' in e.stdout or \\\n                    'nothing added to commit' in e.stdout:\n                lgr.debug(\"no changes added to commit in %s. Ignored.\", self)\n            else:\n                raise\n        if orig_msg \\\n                or '--dry-run' in cmd \\\n                or prev_sha == self.get_hexsha() \\\n                or ('--amend' in cmd and '--no-edit' in cmd) \\\n                or (not is_interactive()) \\\n                or self.config.obtain('datalad.save.no-message') != 'interactive':\n            # we had a message given, or nothing was committed, or prev. commit\n            # was amended, or we are not connected to a terminal, or no\n            # interactive message input is desired:\n            # we can go home\n            return\n\n        # handle interactive message entry by running another `git-commit`\n        self._git_runner.run(\n            cmd + ['--amend', '--edit'],\n            protocol=NoCapture,\n            stdin=None,\n            env=env,\n        )\n\n    # TODO usage is only in the tests, consider making a test helper and\n    # remove from GitRepo API\n    def get_indexed_files(self):\n        \"\"\"Get a list of files in git's index\n\n        Returns\n        -------\n        list\n            list of paths rooting in git's base dir\n        \"\"\"\n\n        return [\n            str(r.relative_to(self.pathobj))\n            for r in self.get_content_info(\n                paths=None, ref=None, untracked='no')\n        ]\n\n    def format_commit(self, fmt, commitish=None):\n        \"\"\"Return `git show` output for `commitish`.\n\n        Parameters\n        ----------\n        fmt : str\n            A format string accepted by `git show`.\n        commitish: str, optional\n          Any commit identifier (defaults to \"HEAD\").\n\n        Returns\n        -------\n        str or, if there are not commits yet, None.\n        \"\"\"\n        # use git-log and not git-show due to faster performance with\n        # complex commits (e.g. octopus merges)\n        # https://github.com/datalad/datalad/issues/4801\n        cmd = ['log', '-1', '-z', '--format=' + fmt]\n        if commitish is not None:\n            cmd.append(commitish + \"^{commit}\")\n        # make sure Git takes our argument as a revision\n        cmd.append('--')\n        try:\n            stdout = self.call_git(\n                cmd, expect_stderr=True, expect_fail=True,\n                read_only=True)\n        except CommandError as e:\n            if 'bad revision' in e.stderr:\n                raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n            elif 'does not have any commits yet' in e.stderr:\n                return None\n            else:\n                raise e\n        # This trailing null is coming from the -z above, which avoids the\n        # newline that Git would append to the output. We could drop -z and\n        # strip the newline directly, but then we'd have to worry about\n        # compatibility across platforms.\n        return stdout.rsplit(\"\\0\", 1)[0]\n\n    def get_hexsha(self, commitish=None, short=False):\n        \"\"\"Return a hexsha for a given commitish.\n\n        Parameters\n        ----------\n        commitish : str, optional\n          Any identifier that refers to a commit (defaults to \"HEAD\").\n        short : bool, optional\n          Return the abbreviated form of the hexsha.\n\n        Returns\n        -------\n        str or, if no commitish was given and there are no commits yet, None.\n\n        Raises\n        ------\n        ValueError\n          If a commitish was given, but no corresponding commit could be\n          determined.\n        \"\"\"\n        # use --quiet because the 'Needed a single revision' error message\n        # that is the result of running this in a repo with no commits\n        # isn't useful to report\n        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(\n            commitish if commitish else 'HEAD')\n        ]\n        if short:\n            cmd.append('--short')\n        try:\n            return self.call_git_oneline(cmd, read_only=True)\n        except CommandError as e:\n            if commitish is None:\n                return None\n            raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n\n    @normalize_paths(match_return_type=False)\n    def get_last_commit_hexsha(self, files):\n        \"\"\"Return the hash of the last commit the modified any of the given\n        paths\"\"\"\n        try:\n            commit = self.call_git(\n                ['rev-list', '-n1', 'HEAD'],\n                files=files,\n                expect_fail=True,\n                read_only=True,\n            )\n            commit = commit.strip()\n            return commit if commit else None\n        except CommandError:\n            if self.get_hexsha() is None:\n                # unborn branch, don't freak out\n                return None\n            raise\n\n    def get_revisions(self, revrange=None, fmt=\"%H\", options=None):\n        \"\"\"Return list of revisions in `revrange`.\n\n        Parameters\n        ----------\n        revrange : str or list of str or None, optional\n            Revisions or revision ranges to walk. If None, revision defaults to\n            HEAD unless a revision-modifying option like `--all` or\n            `--branches` is included in `options`.\n        fmt : string, optional\n            Format accepted by `--format` option of `git log`. This should not\n            contain new lines because the output is split on new lines.\n        options : list of str, optional\n            Options to pass to `git log`.  This should not include `--format`.\n\n        Returns\n        -------\n        List of revisions (str), formatted according to `fmt`.\n        \"\"\"\n        if revrange is None:\n            revrange = []\n        elif isinstance(revrange, str):\n            revrange = [revrange]\n\n        cmd = [\"log\", \"--format={}\".format(fmt)]\n        cmd.extend((options or []) + revrange + [\"--\"])\n        try:\n            stdout = self.call_git(cmd, expect_fail=True, read_only=True)\n        except CommandError as e:\n            if \"does not have any commits\" in e.stderr:\n                return []\n            raise\n        return stdout.splitlines()\n\n    def commit_exists(self, commitish):\n        \"\"\"Does `commitish` exist in the repo?\n\n        Parameters\n        ----------\n        commitish : str\n            A commit or an object that can be dereferenced to one.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        # Note: The peeling operator \"^{commit}\" is required so that rev-parse\n        # doesn't succeed if passed a full hexsha that is valid but doesn't\n        # exist.\n        return self.call_git_success(\n            [\"rev-parse\", \"--verify\", commitish + \"^{commit}\"],\n            read_only=True,\n        )\n\n    def get_merge_base(self, commitishes):\n        \"\"\"Get a merge base hexsha\n\n        Parameters\n        ----------\n        commitishes: str or list of str\n          List of commitishes (branches, hexshas, etc) to determine the merge\n          base of. If a single value provided, returns merge_base with the\n          current branch.\n\n        Returns\n        -------\n        str or None\n          If no merge-base for given commits, or specified treeish doesn't\n          exist, None returned\n        \"\"\"\n        if isinstance(commitishes, str):\n            commitishes = [commitishes]\n        if not commitishes:\n            raise ValueError(\"Provide at least a single value\")\n        elif len(commitishes) == 1:\n            commitishes = commitishes + [self.get_active_branch()]\n\n        try:\n            base = self.call_git_oneline(['merge-base'] + commitishes,\n                                         read_only=True)\n        except CommandError as exc:\n            if exc.code == 1 and not (exc.stdout or exc.stderr):\n                # No merge base was found (unrelated commits).\n                return None\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                return None\n            raise\n\n        return base\n\n    def is_ancestor(self, reva, revb):\n        \"\"\"Is `reva` an ancestor of `revb`?\n\n        Parameters\n        ----------\n        reva, revb : str\n            Revisions.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        return self.call_git_success(\n            [\"merge-base\", \"--is-ancestor\", reva, revb],\n            read_only=True)\n\n    def get_commit_date(self, branch=None, date='authored'):\n        \"\"\"Get the date stamp of the last commit (in a branch or head otherwise)\n\n        Parameters\n        ----------\n        date: {'authored', 'committed'}\n          Which date to return.  \"authored\" will be the date shown by \"git show\"\n          and the one possibly specified via --date to `git commit`\n\n        Returns\n        -------\n        int or None\n          None if no commit\n        \"\"\"\n        if date == 'committed':\n            format = '%ct'\n        elif date == 'authored':\n            format = '%at'\n        else:\n            raise ValueError('unknow date type: {}'.format(date))\n        d = self.format_commit(format, commitish=branch)\n        return int(d) if d else None\n\n    def get_active_branch(self):\n        \"\"\"Get the name of the active branch\n\n        Returns\n        -------\n        str or None\n          Returns None if there is no active branch, i.e. detached HEAD,\n          and the branch name otherwise.\n        \"\"\"\n        try:\n            out = self.call_git([\"symbolic-ref\", \"HEAD\"], expect_fail=True,\n                                read_only=True)\n        except CommandError as e:\n            if 'HEAD is not a symbolic ref' in e.stderr:\n                lgr.debug(\"detached HEAD in %s\", self)\n                return None\n            else:\n                raise e\n        return out.strip()[11:]  # strip refs/heads/\n\n    def get_corresponding_branch(self, branch=None):\n        \"\"\"Always returns None, a plain GitRepo has no managed branches\"\"\"\n        return None\n\n    def get_branches(self):\n        \"\"\"Get all branches of the repo.\n\n        Returns\n        -------\n        [str]\n            Names of all branches of this repository.\n        \"\"\"\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')\n        ]\n\n    def get_remote_branches(self):\n        \"\"\"Get all branches of all remotes of the repo.\n\n        Returns\n        -----------\n        [str]\n            Names of all remote branches.\n        \"\"\"\n        # TODO: Reconsider melting with get_branches()\n\n        # TODO: treat entries like this: origin/HEAD -> origin/master'\n        # currently this is done in collection\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')\n        ]\n\n    def get_remotes(self, with_urls_only=False):\n        \"\"\"Get known remotes of the repository\n\n        Parameters\n        ----------\n        with_urls_only : bool, optional\n          return only remotes which have urls\n\n        Returns\n        -------\n        remotes : list of str\n          List of names of the remotes\n        \"\"\"\n        from datalad.utils import unique\n\n        self.config.reload()\n        remotes = unique([x[7:] for x in self.config.sections()\n                          if x.startswith(\"remote.\")])\n\n        if with_urls_only:\n            remotes = [\n                r for r in remotes\n                if self.config.get('remote.%s.url' % r)\n            ]\n        return remotes\n\n    # TODO this is practically unused outside the tests, consider turning\n    # into a test helper and trim from the API\n    def get_files(self, branch=None):\n        \"\"\"Get a list of files in git.\n\n        Lists the files in the (remote) branch.\n\n        Parameters\n        ----------\n        branch: str\n          Name of the branch to query. Default: active branch.\n\n        Returns\n        -------\n        [str]\n          list of files.\n        \"\"\"\n        return [\n            str(p.relative_to(self.pathobj))\n            for p in self.get_content_info(\n                paths=None, ref=branch, untracked='no')\n            ]\n\n    def add_remote(self, name, url, options=None):\n        \"\"\"Register remote pointing to a url\n        \"\"\"\n        cmd = ['remote', 'add']\n        if options:\n            cmd += options\n        cmd += [name, url]\n\n        # for historical reasons this method returns stdout and\n        # stderr, keeping that for now\n        result = self._call_git(cmd)\n        self.config.reload()\n        return result\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n\n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n                raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\n            else:\n                raise e\n\n        # config.reload necessary, because the associated remote config\n        # will vanish\n        self.config.reload()\n        return\n\n    def _maybe_open_ssh_connection(self, remote, prefer_push=True):\n        \"\"\"Open connection if `remote` has an SSH URL.\n\n        Doing so enables SSH caching, preventing datalad-sshrun subprocesses\n        from opening (and then closing) their own.\n\n        Parameters\n        ----------\n        remote : str\n        prefer_push : bool, optional\n            Use `remote.<remote>.pushurl` if there is one, falling back to\n            `remote.<remote>.url`.\n        \"\"\"\n        if remote:\n            url = None\n            if prefer_push:\n                url = self.get_remote_url(remote, push=True)\n            url = url or self.get_remote_url(remote)\n            if url and is_ssh(url):\n                ssh_manager.get_connection(url).open()\n\n    def update_remote(self, name=None, verbose=False):\n        \"\"\"\n        \"\"\"\n        options = [\"-v\"] if verbose else []\n        self._maybe_open_ssh_connection(name)\n        name = [name] if name else []\n        self.call_git(\n            ['remote'] + name + ['update'] + options,\n            expect_stderr=True\n        )\n\n    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,\n              **kwargs):\n        \"\"\"Fetches changes from a remote (or all remotes).\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to fetch from. If no remote is given and\n          `all_` is not set, the tracking branch is fetched.\n        refspec : str or list, optional\n          refspec(s) to fetch.\n        all_ : bool, optional\n          fetch all remotes (and all of their branches).\n          Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-fetch.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-fetch.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        return list(\n            self.fetch_(\n                remote=remote,\n                refspec=refspec,\n                all_=all_,\n                git_options=git_options,\n            )\n        )\n\n    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `fetch`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],\n            action='fetch',\n            urlvars=('remote.{}.url', 'remote.{}.url'),\n            protocol=GitProgress,\n            info_cls=FetchInfo,\n            info_from='stderr',\n            add_remote=False,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def push(self, remote=None, refspec=None, all_remotes=False,\n             all_=False, git_options=None, **kwargs):\n        \"\"\"Push changes to a remote (or all remotes).\n\n        If remote and refspec are specified, and remote has\n        `remote.{remote}.datalad-push-default-first` configuration variable\n        set (e.g. by `create-sibling-github`), we will first push the first\n        refspec separately to possibly ensure that the first refspec is chosen\n        by remote as the \"default branch\".\n        See https://github.com/datalad/datalad/issues/4997\n        Upon successful push if this variable was set in the local git config,\n        we unset it, so subsequent pushes would proceed normally.\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to push to. If no remote is given and\n          `all_` is not set, the tracking branch is pushed.\n        refspec : str or list, optional\n          refspec(s) to push.\n        all_ : bool, optional\n          push to all remotes. Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-push.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-push.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        if all_remotes:\n            # be nice to the elderly\n            all_ = True\n\n        push_refspecs = [refspec]\n        cfg = self.config  # shortcut\n        cfg_push_var = \"remote.{}.datalad-push-default-first\".format(remote)\n        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):\n            refspec = ensure_list(refspec)\n            lgr.debug(\"As indicated by %s pushing first refspec %s separately first\",\n                      cfg_push_var, refspec[0])\n            push_refspecs = [[refspec[0]], refspec[1:]]\n\n        push_res = []\n        for refspecs in push_refspecs:\n            push_res.extend(\n                self.push_(\n                    remote=remote,\n                    refspec=refspecs,\n                    all_=all_,\n                    git_options=git_options,\n                )\n            )\n        # note: above push_ should raise exception if errors out\n        if '--dry-run' not in git_options \\\n            and cfg.get_from_source('local', cfg_push_var) is not None:\n            lgr.debug(\"Removing %s variable from local git config after successful push\", cfg_push_var)\n            cfg.unset(cfg_push_var, 'local')\n        return push_res\n\n    def push_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `push`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],\n            action='push',\n            urlvars=('remote.{}.pushurl', 'remote.{}.url'),\n            protocol=StdOutCaptureWithGitProgress,\n            info_cls=PushInfo,\n            info_from='stdout',\n            add_remote=True,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def _fetch_push_helper(\n            self,\n            base_cmd,     # arg list\n            action,       # label fetch|push\n            urlvars,      # variables to query for URLs\n            protocol,     # processor for output\n            info_cls,     # Push|FetchInfo\n            info_from,    # stdout, stderr\n            add_remote,   # whether to add a 'remote' field to the info dict\n            remote=None, refspec=None, all_=False, git_options=None):\n\n        git_options = ensure_list(git_options)\n\n        cmd = base_cmd + git_options\n\n        if remote is None:\n            if refspec:\n                # conflicts with using tracking branch or push all remotes\n                # For now: Just fail.\n                # TODO: May be check whether it fits to tracking branch\n                raise ValueError(\n                    \"refspec specified without a remote. ({})\".format(refspec))\n            if all_:\n                remotes_to_process = self.get_remotes(with_urls_only=True)\n            else:\n                # No explicit remote to fetch.\n                # => get tracking branch:\n                tb_remote, refspec = self.get_tracking_branch()\n                if tb_remote is not None:\n                    remotes_to_process = [tb_remote]\n                else:\n                    # No remote, no tracking branch\n                    # => fail\n                    raise ValueError(\n                        \"Neither a remote is specified to {} \"\n                        \"from nor a tracking branch is set up.\".format(action))\n        else:\n            if all_:\n                raise ValueError(\n                    \"Option 'all_' conflicts with specified remote \"\n                    \"'{}'.\".format(remote))\n            remotes_to_process = [remote]\n\n        if refspec:\n            # prep for appending to cmd\n            refspec = ensure_list(refspec)\n\n        # no need for progress report, when there is just one remote\n        log_remote_progress = len(remotes_to_process) > 1\n        if log_remote_progress:\n            pbar_id = '{}remotes-{}'.format(action, id(self))\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start %sing remotes for %s', action, self,\n                total=len(remotes_to_process),\n                label=action.capitalize(),\n                unit=' Remotes',\n            )\n        try:\n            for remote in remotes_to_process:\n                r_cmd = cmd + [remote]\n                if refspec:\n                    r_cmd += refspec\n\n                if log_remote_progress:\n                    log_progress(\n                        lgr.info,\n                        pbar_id,\n                        '{}ing remote %s'.format(action.capitalize()),\n                        remote,\n                        update=1,\n                        increment=True,\n                    )\n                # best effort to enable SSH connection caching\n                url = self.config.get(\n                    # make two attempts to get a URL\n                    urlvars[0].format(remote),\n                    self.config.get(\n                        urlvars[1].format(remote),\n                        None)\n                )\n                if url and is_ssh(url):\n                    ssh_manager.get_connection(url).open()\n                try:\n                    out = self._git_runner.run(\n                        r_cmd,\n                        protocol=protocol,\n                    )\n                    output = out[info_from] or ''\n                except CommandError as e:\n                    output = None\n                    # intercept some errors that we express as an error report\n                    # in the info dicts\n                    if re.match(\n                            '.*^error: failed to (push|fetch) some refs',\n                            e.stderr,\n                            re.DOTALL | re.MULTILINE):\n                        output = getattr(e, info_from)\n                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()\n                                          if l.startswith('hint: ')])\n                        if output is None:\n                            output = ''\n                    if not output:\n                        raise\n\n                for line in output.splitlines():\n                    try:\n                        # push info doesn't identify a remote, add it here\n                        pi = info_cls._from_line(line)\n                        if add_remote:\n                            pi['remote'] = remote\n                        # There were errors, but Git provided hints\n                        if 'error' in pi['operations']:\n                            pi['hints'] = hints or None\n                        yield pi\n                    except Exception:\n                        # it is not progress and no push info\n                        # don't hide it completely\n                        lgr.debug('git-%s reported: %s', action, line)\n        finally:\n            if log_remote_progress:\n                log_progress(\n                    lgr.info,\n                    pbar_id,\n                    'Finished %sing remotes for %s', action, self,\n                )\n\n    def get_remote_url(self, name, push=False):\n        \"\"\"Get the url of a remote.\n\n        Reads the configuration of remote `name` and returns its url or None,\n        if there is no url configured.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        push: bool\n          if True, get the pushurl instead of the fetch url.\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        return self.config.get(var, None)\n\n    def set_remote_url(self, name, url, push=False):\n        \"\"\"Set the URL a remote is pointing to\n\n        Sets the URL of the remote `name`. Requires the remote to already exist.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        url: str\n        push: bool\n          if True, set the push URL, otherwise the fetch URL\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        self.config.set(var, url, scope='local', reload=True)\n\n    def get_branch_commits_(self, branch=None, limit=None, stop=None):\n        \"\"\"Return commit hexshas for a branch\n\n        Parameters\n        ----------\n        branch: str, optional\n          If not provided, assumes current branch\n        limit: None | 'left-only', optional\n          Limit which commits to report.  If None -- all commits (merged or not),\n          if 'left-only' -- only the commits from the left side of the tree upon\n          merges\n        stop: str, optional\n          hexsha of the commit at which stop reporting (matched one is not\n          reported either)\n\n        Yields\n        ------\n        str\n        \"\"\"\n        cmd = ['rev-list']\n        if limit == 'left-only':\n            cmd.append('--left-only')\n        if not branch:\n            branch = self.get_active_branch()\n        cmd.append(branch)\n        # and trailing -- marker to make sure that Git never confused the branch\n        # with a potentially existing directory of the same name\n        cmd.append('--')\n        for r in self.call_git_items_(cmd):\n            if stop and stop == r:\n                return\n            yield r\n\n    def checkout(self, name, options=None):\n        \"\"\"\n        \"\"\"\n        # TODO: May be check for the need of -b options herein?\n        cmd = ['checkout']\n        if options:\n            cmd += options\n        cmd += [str(name)]\n\n        self.call_git(cmd, expect_stderr=True)\n        # checkout can change committed config, or create branch config\n        self.config.reload()\n\n    # TODO: Before implementing annex merge, find usages and check for a needed\n    # change to call super().merge\n    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):\n        if options is None:\n            options = []\n        if msg:\n            options = options + [\"-m\", msg]\n        options += ['--allow-unrelated-histories']\n        self.call_git(\n            ['merge'] + options + [name],\n            **kwargs\n        )\n\n    def remove_branch(self, branch):\n        self.call_git(['branch', '-D', branch])\n\n    def cherry_pick(self, commit):\n        \"\"\"Cherry pick `commit` to the current branch.\n\n        Parameters\n        ----------\n        commit : str\n            A single commit.\n        \"\"\"\n        self.call_git([\"cherry-pick\", commit])\n\n    @property\n    def dirty(self):\n        \"\"\"Is the repository dirty?\n\n        Note: This provides a quick answer when you simply want to know if\n        there are any untracked changes or modifications in this repository or\n        its submodules. For finer-grained control and more detailed reporting,\n        use status() instead.\n        \"\"\"\n        stdout = self.call_git(\n            [\"status\", \"--porcelain\",\n             # Ensure the result isn't influenced by status.showUntrackedFiles.\n             \"--untracked-files=normal\",\n             # Ensure the result isn't influenced by diff.ignoreSubmodules.\n             \"--ignore-submodules=none\"])\n        if bool(stdout.strip()):\n            # The quick `git status`-based check can give a different answer\n            # than `datalad status` for submodules on an adjusted branch.\n            st = self.diffstatus(fr=\"HEAD\" if self.get_hexsha() else None,\n                                 to=None, untracked=\"normal\")\n            return any(r.get(\"state\") != \"clean\" for r in st.values())\n        return False\n\n    @property\n    def untracked_files(self):\n        \"\"\"Legacy interface, do not use! Use the status() method instead.\n\n        Despite its name, it also reports on untracked datasets, and\n        yields their names with trailing path separators.\n        \"\"\"\n        return [\n            '{}{}'.format(\n                str(p.relative_to(self.pathobj)),\n                os.sep if props['type'] != 'file' else ''\n            )\n            for p, props in self.status(\n                    untracked='all', eval_submodule_state='no').items()\n            if props.get('state', None) == 'untracked'\n        ]\n\n    def gc(self, allow_background=False, auto=False):\n        \"\"\"Perform house keeping (garbage collection, repacking)\"\"\"\n        cmd_options = []\n        if not allow_background:\n            cmd_options += ['-c', 'gc.autodetach=0']\n        cmd_options += ['gc', '--aggressive']\n        if auto:\n            cmd_options += ['--auto']\n        self.call_git(cmd_options)\n\n    def _parse_gitmodules(self):\n        # TODO read .gitconfig from Git blob?\n        gitmodules = self.pathobj / '.gitmodules'\n        if not gitmodules.exists():\n            return {}\n        # pull out file content\n        out = self.call_git(\n            ['config', '-z', '-l', '--file', '.gitmodules'],\n            read_only=True)\n        # abuse our config parser\n        # disable multi-value report, because we could not deal with them\n        # anyways, and they should not appear in a normal .gitmodules file\n        # but could easily appear when duplicates are included. In this case,\n        # we better not crash\n        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)\n        mods = {}\n        for k, v in db.items():\n            if not k.startswith('submodule.'):\n                # we don't know what this is\n                lgr.warning(\"Skip unrecognized .gitmodule specification: %s=%s\", k, v)\n                continue\n            k_l = k.split('.')\n            # module name is everything after 'submodule.' that is not the variable\n            # name\n            mod_name = '.'.join(k_l[1:-1])\n            mod = mods.get(mod_name, {})\n            # variable name is the last 'dot-free' segment in the key\n            mod[k_l[-1]] = v\n            mods[mod_name] = mod\n\n        out = {}\n        # bring into traditional shape\n        for name, props in mods.items():\n            if 'path' not in props:\n                lgr.warning(\"Failed to get '%s.path', skipping this submodule\", name)\n                continue\n            modprops = {'gitmodule_{}'.format(k): v\n                        for k, v in props.items()\n                        if not k.startswith('__')}\n            # Keep as PurePosixPath for possible normalization of / in the path etc\n            modpath = PurePosixPath(props['path'])\n            modprops['gitmodule_name'] = name\n            out[modpath] = modprops\n        return out\n\n    def get_submodules_(self, paths=None):\n        \"\"\"Yield submodules in this repository.\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`. Paths must be relative\n            to the resolved repository root, and must be normed to match the\n            reporting done by Git, i.e. no parent dir components\n            (ala \"some/../this\").\n\n        Returns\n        -------\n        A generator that yields a dictionary with information for each\n        submodule.\n        \"\"\"\n        if not (self.pathobj / \".gitmodules\").exists():\n            return\n\n        modinfo = self._parse_gitmodules()\n        if not modinfo:\n            # we exit early, if there is nothing on record (even though\n            # a .gitmodules file exists).\n            # without this conditional exit, we would be able to discover\n            # subprojects even when they are not recorded in .gitmodules,\n            # but at the cost of running an unconstrained ls-files call\n            # below\n            return\n\n        posix_mod_paths = [m.as_posix() for m in modinfo]\n        if paths:\n            # harmonize them into relative to the repository\n            posix_paths = []\n            for path in paths:\n                path = ut.PurePath(path)\n                if path.is_absolute():\n                    try:\n                        path = path.relative_to(self.pathobj)\n                    except ValueError as exc:\n                        lgr.debug(\n                            \"Path %s it not underneath %s, skipping since nothing should match it: %s\",\n                            path, self.pathobj, CapturedException(exc)\n                        )\n                        continue\n                posix_paths.append(path.as_posix())\n\n            # constrain the report by the given paths, make sure all paths are POSIX\n            posix_mod_paths = list(get_filtered_paths_(\n                posix_mod_paths,\n                posix_paths,\n                include_within_path=True,\n            ))\n\n        for r in self.call_git_items_(\n            ['ls-files', '--stage', '-z'],\n            sep='\\0',\n            files=posix_mod_paths,\n            read_only=True,\n            keep_ends=True,\n        ):\n            if not r.startswith('160000'):\n                # make sure this method never talks about non-dataset\n                # content\n                continue\n            props, rpath = r.split('\\t')\n            mode, gitsha, stage = props.split(' ')\n            if stage not in ('0', '2'):\n                # we either have non-merge situation, or a simple merge\n                # situation (i.e. stage=0). the reported gitsha always\n                # matches what we have locally.\n                # or we are in a three-way merge, in which case stage=2\n                # is what we want to report, because it matches the\n                # current HEAD (see git-read-tree manpage).\n                # there is either a stage 2 or stage 0, never both\n                continue\n            # remove the expected line separator from the path\n            rpath = rpath[:-1]\n            path = PurePosixPath(rpath)\n            yield dict(\n                path=self.pathobj / rpath, # full path returned here\n                type='dataset',\n                gitshasum=gitsha,\n                **modinfo.get(path, {})\n            )\n\n    def get_submodules(self, sorted_=True, paths=None):\n        \"\"\"Return list of submodules.\n\n        Parameters\n        ----------\n        sorted_ : bool, optional\n            Sort submodules by path name.\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`.\n\n        Returns\n        -------\n        List of submodule namedtuples if `compat` is true or otherwise a list\n        of dictionaries as returned by `get_submodules_`.\n        \"\"\"\n        xs = self.get_submodules_(paths=paths)\n\n        if sorted_:\n            xs = sorted(xs, key=lambda x: x[\"path\"])\n        return list(xs)\n\n    def update_ref(self, ref, value, oldvalue=None, symbolic=False):\n        \"\"\"Update the object name stored in a ref \"safely\".\n\n        Just a shim for `git update-ref` call if not symbolic, and\n        `git symbolic-ref` if symbolic\n\n        Parameters\n        ----------\n        ref : str\n          Reference, such as `ref/heads/BRANCHNAME` or HEAD.\n        value : str\n          Value to update to, e.g. hexsha of a commit when updating for a\n          branch ref, or branch ref if updating HEAD\n        oldvalue: str\n          Value to update from. Safeguard to be verified by git. This is only\n          valid if `symbolic` is not True.\n        symbolic : None\n          To instruct if ref is symbolic, e.g. should be used in case of\n          ref=HEAD\n        \"\"\"\n        if symbolic:\n            if oldvalue:\n                raise ValueError(\"oldvalue and symbolic must not be given both\")\n            cmd = ['symbolic-ref', ref, value]\n        else:\n            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])\n        self.call_git(cmd)\n\n    def tag(self, tag, message=None, commit=None, options=None):\n        \"\"\"Tag a commit\n\n        Parameters\n        ----------\n        tag : str\n          Custom tag label. Must be a valid tag name.\n        message : str, optional\n          If provided, adds ['-m', <message>] to the list of `git tag`\n          arguments.\n        commit : str, optional\n          If provided, will be appended as last argument to the `git tag` call,\n          and can be used to identify the commit that shall be tagged, if\n          not HEAD.\n        options : list, optional\n          Additional command options, inserted prior a potential `commit`\n          argument.\n        \"\"\"\n        # TODO: call in save.py complains about extensive logging. When does it\n        # happen in what way? Figure out, whether to just silence it or raise or\n        # whatever else.\n        args = ['tag']\n        if message:\n            args += ['-m', message]\n        if options is not None:\n            args.extend(options)\n        args.append(tag)\n        if commit:\n            args.append(commit)\n        self.call_git(args)\n\n    def get_tags(self, output=None):\n        \"\"\"Get list of tags\n\n        Parameters\n        ----------\n        output : str, optional\n          If given, limit the return value to a list of values matching that\n          particular key of the tag properties.\n\n        Returns\n        -------\n        list\n          Each item is a dictionary with information on a tag. At present\n          this includes 'hexsha', and 'name', where the latter is the string\n          label of the tag, and the former the hexsha of the object the tag\n          is attached to. The list is sorted by the creator date (committer\n          date for lightweight tags and tagger date for annotated tags), with\n          the most recent commit being the last element.\n        \"\"\"\n        tags = [\n            dict(\n                name=t['refname:strip=2'],\n                hexsha=t['object'] if t['object'] else t['objectname'],\n            )\n            for t in self.for_each_ref_(\n                fields=['refname:strip=2', 'objectname', 'object'],\n                pattern='refs/tags',\n                sort='creatordate')\n        ]\n        if output:\n            return [t[output] for t in tags]\n        else:\n            return tags\n\n    def describe(self, commitish=None, **kwargs):\n        \"\"\" Quick and dirty implementation to call git-describe\n\n        Parameters\n        ----------\n        kwargs:\n            transformed to cmdline options for git-describe;\n            see __init__ for description of the transformation\n        \"\"\"\n        # TODO: be more precise what failure to expect when and raise actual\n        # errors\n        cmd = ['describe'] + to_options(**kwargs)\n        if commitish is not None:\n            cmd.append(commitish)\n        try:\n            describe = self.call_git(cmd, expect_fail=True)\n            return describe.strip()\n        # TODO: WTF \"catch everything\"?\n        except:\n            return None\n\n    def get_tracking_branch(self, branch=None, remote_only=False):\n        \"\"\"Get the tracking branch for `branch` if there is any.\n\n        Parameters\n        ----------\n        branch: str\n            local branch to look up. If none is given, active branch is used.\n        remote_only : bool\n            Don't return a value if the upstream remote is set to \".\" (meaning\n            this repository).\n\n        Returns\n        -------\n        tuple\n            (remote or None, refspec or None) of the tracking branch\n        \"\"\"\n        if branch is None:\n            branch = self.get_corresponding_branch() or self.get_active_branch()\n            if branch is None:\n                return None, None\n\n        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)\n        if remote_only and track_remote == \".\":\n            return None, None\n        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)\n        return track_remote, track_branch\n\n    @property\n    def count_objects(self):\n        \"\"\"return dictionary with count, size(in KiB) information of git objects\n        \"\"\"\n\n        count_cmd = ['count-objects', '-v']\n        count_str = self.call_git(count_cmd)\n        count = {key: int(value)\n                 for key, value in [item.split(': ')\n                                    for item in count_str.split('\\n')\n                                    if len(item.split(': ')) == 2]}\n        return count\n\n    def get_git_attributes(self):\n        \"\"\"Query gitattributes which apply to top level directory\n\n        It is a thin compatibility/shortcut wrapper around more versatile\n        get_gitattributes which operates on a list of paths and returns\n        a dictionary per each path\n\n        Returns\n        -------\n        dict:\n          a dictionary with attribute name and value items relevant for the\n          top ('.') directory of the repository, and thus most likely the\n          default ones (if not overwritten with more rules) for all files within\n          repo.\n        \"\"\"\n        return self.get_gitattributes('.')['.']\n\n    def get_gitattributes(self, path, index_only=False):\n        \"\"\"Query gitattributes for one or more paths\n\n        Parameters\n        ----------\n        path: path or list\n          Path(s) to query. Paths may be relative or absolute.\n        index_only: bool\n          Flag whether to consider only gitattribute setting that are reflected\n          in the repository index, not just in the work tree content.\n\n        Returns\n        -------\n        dict:\n          Each key is a queried path (always relative to the repository root),\n          each value is a dictionary with attribute\n          name and value items. Attribute values are either True or False,\n          for set and unset attributes, or are the literal attribute value.\n        \"\"\"\n        path = ensure_list(path)\n        cmd = [\"check-attr\", \"-z\", \"--all\"]\n        if index_only:\n            cmd.append('--cached')\n        # make sure we have one entry for each query path to\n        # simplify work with the result\n        attributes = {p: {} for p in path}\n        attr = []\n        for item in self.call_git_items_(cmd, files=path, sep='\\0',\n                                         read_only=True):\n            attr.append(item)\n            if len(attr) < 3:\n                continue\n            # we have a full record\n            p, name, value = attr\n            attrs = attributes[p]\n            attrs[name] = \\\n                True if value == 'set' else False if value == 'unset' else value\n            # done, reset item\n            attr = []\n        return {relpath(k, self.path) if isabs(k) else k: v\n                for k, v in attributes.items()}\n\n    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):\n        \"\"\"Set gitattributes\n\n        By default appends additional lines to `attrfile`. Note, that later\n        lines in `attrfile` overrule earlier ones, which may or may not be\n        what you want. Set `mode` to 'w' to replace the entire file by\n        what you provided in `attrs`.\n\n        Parameters\n        ----------\n        attrs : list\n          Each item is a 2-tuple, where the first element is a path pattern,\n          and the second element is a dictionary with attribute key/value\n          pairs. The attribute dictionary must use the same semantics as those\n          returned by `get_gitattributes()`. Path patterns can use absolute paths,\n          in which case they will be normalized relative to the directory\n          that contains the target .gitattributes file (see `attrfile`).\n        attrfile: path\n          Path relative to the repository root of the .gitattributes file the\n          attributes shall be set in.\n        mode: str\n          'a' to append .gitattributes, 'w' to replace it\n        \"\"\"\n\n        git_attributes_file = op.join(self.path, attrfile)\n        attrdir = op.dirname(git_attributes_file)\n        if not op.exists(attrdir):\n            os.makedirs(attrdir)\n\n        with open(git_attributes_file, mode + '+') as f:\n            # for append, fix existing files that do not end with \\n\n            if mode == 'a' and f.tell():\n                f.seek(max(0, f.tell() - len(os.linesep)))\n                if not f.read().endswith('\\n'):\n                    f.write('\\n')\n\n            for pattern, attr in sorted(attrs, key=lambda x: x[0]):\n                # normalize the pattern relative to the target .gitattributes file\n                npath = _normalize_path(\n                    op.join(self.path, op.dirname(attrfile)), pattern)\n                # paths in gitattributes always have to be POSIX\n                npath = Path(npath).as_posix()\n                attrline = u''\n                if npath.count(' '):\n                    # quote patterns with spaces\n                    attrline += u'\"{}\"'.format(npath.replace('\"', '\\\\\"'))\n                else:\n                    attrline += npath\n                for a in sorted(attr):\n                    val = attr[a]\n                    if val is True:\n                        attrline += ' {}'.format(a)\n                    elif val is False:\n                        attrline += ' -{}'.format(a)\n                    else:\n                        attrline += ' {}={}'.format(a, val)\n                f.write('{}\\n'.format(attrline))\n\n    def get_content_info(self, paths=None, ref=None, untracked='all'):\n        \"\"\"Get identifier and type information from repository content.\n\n        This is simplified front-end for `git ls-files/tree`.\n\n        Both commands differ in their behavior when queried about subdataset\n        paths. ls-files will not report anything, ls-tree will report on the\n        subdataset record. This function uniformly follows the behavior of\n        ls-tree (report on the respective subdataset mount).\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath) or None\n          Specific paths, relative to the resolved repository root, to query\n          info for. Paths must be normed to match the reporting done by Git,\n          i.e. no parent dir components (ala \"some/../this\").\n          If `None`, info is reported for all content.\n        ref : gitref or None\n          If given, content information is retrieved for this Git reference\n          (via ls-tree), otherwise content information is produced for the\n          present work tree (via ls-files). With a given reference, the\n          reported content properties also contain a 'bytesize' record,\n          stating the size of a file in bytes.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when no `ref` was given:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n\n          `gitshasum`\n            SHASUM of the item as tracked by Git, or None, if not\n            tracked. This could be different from the SHASUM of the file\n            in the worktree, if it was modified.\n\n        Raises\n        ------\n        ValueError\n          In case of an invalid Git reference (e.g. 'HEAD' in an empty\n          repository)\n        \"\"\"\n        lgr.debug('%s.get_content_info(...)', self)\n        # TODO limit by file type to replace code in subdatasets command\n        info = dict()\n\n        if paths:  # is not None separate after\n            # path matching will happen against what Git reports\n            # and Git always reports POSIX paths\n            # any incoming path has to be relative already, so we can simply\n            # convert unconditionally\n            # note: will be list-ified below\n            posix_paths = [ut.PurePath(p).as_posix() for p in paths]\n        elif paths is not None:\n            return info\n        else:\n            posix_paths = None\n\n        if posix_paths and (not ref or external_versions[\"cmd:git\"] >= \"2.29.0\"):\n            # If a path points within a submodule, we need to map it to the\n            # containing submodule before feeding it to ls-files or ls-tree.\n            #\n            # Before Git 2.29.0, ls-tree and ls-files differed in how they\n            # reported paths within submodules: ls-files provided no output,\n            # and ls-tree listed the submodule. Now they both return no output.\n            submodules = [s[\"path\"].relative_to(self.pathobj).as_posix()\n                          for s in self.get_submodules_()]\n            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well\n            posix_paths = get_parent_paths(posix_paths, submodules)\n\n        # this will not work in direct mode, but everything else should be\n        # just fine\n        if not ref:\n            # make sure no operations are pending before we figure things\n            # out in the worktree\n            self.precommit()\n\n            # --exclude-standard will make sure to honor and standard way\n            # git can be instructed to ignore content, and will prevent\n            # crap from contaminating untracked file reports\n            cmd = ['ls-files', '--stage', '-z']\n            # untracked report mode, using labels from `git diff` option style\n            if untracked == 'all':\n                cmd += ['--exclude-standard', '-o']\n            elif untracked == 'normal':\n                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']\n            elif untracked == 'no':\n                pass\n            else:\n                raise ValueError(\n                    'unknown value for `untracked`: {}'.format(untracked))\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\\t(?P<fname>.*)$')\n        else:\n            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\\s]*(?P<size>[0-9-]+)\\t(?P<fname>.*)$')\n\n        lgr.debug('Query repo: %s', cmd)\n        try:\n            stdout = self.call_git(\n                cmd,\n                files=posix_paths,\n                expect_fail=True,\n                read_only=True)\n        except CommandError as exc:\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                raise InvalidGitReferenceError(ref)\n            raise\n        lgr.debug('Done query repo: %s', cmd)\n\n        self._get_content_info_line_helper(\n            ref,\n            info,\n            stdout.split('\\0'),\n            props_re)\n\n        lgr.debug('Done %s.get_content_info(...)', self)\n        return info\n\n    def _get_content_info_line_helper(self, ref, info, lines, props_re):\n        \"\"\"Internal helper of get_content_info() to parse Git output\"\"\"\n        mode_type_map = {\n            '100644': 'file',\n            '100755': 'file',\n            '120000': 'symlink',\n            '160000': 'dataset',\n        }\n        for line in lines:\n            if not line:\n                continue\n            inf = {}\n            props = props_re.match(line)\n            if not props:\n                # Kludge: Filter out paths starting with .git/ to work around\n                # an `ls-files -o` bug that was fixed in Git 2.25.\n                #\n                # TODO: Drop this condition when GIT_MIN_VERSION is at least\n                # 2.25.\n                if line.startswith(\".git/\"):\n                    lgr.debug(\"Filtering out .git/ file: %s\", line)\n                    continue\n                # not known to Git, but Git always reports POSIX\n                path = ut.PurePosixPath(line)\n                inf['gitshasum'] = None\n            else:\n                # again Git reports always in POSIX\n                path = ut.PurePosixPath(props.group('fname'))\n\n            # revisit the file props after this path has not been rejected\n            if props:\n                inf['gitshasum'] = props.group('sha')\n                inf['type'] = mode_type_map.get(\n                    props.group('type'), props.group('type'))\n\n                if ref and inf['type'] == 'file':\n                    inf['bytesize'] = int(props.group('size'))\n\n            # join item path with repo path to get a universally useful\n            # path representation with auto-conversion and tons of other\n            # stuff\n            path = self.pathobj.joinpath(path)\n            if 'type' not in inf:\n                # be nice and assign types for untracked content\n                inf['type'] = 'symlink' if path.is_symlink() \\\n                    else 'directory' if path.is_dir() else 'file'\n            info[path] = inf\n\n    def status(self, paths=None, untracked='all', eval_submodule_state='full'):\n        \"\"\"Simplified `git status` equivalent.\n\n        Parameters\n        ----------\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list. If a query path points\n          into a subdataset, a report is made on the subdataset record\n          within the queried dataset only (no recursion).\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        lgr.debug('Query status of %r for %s paths',\n                  self, len(paths) if paths is not None else 'all')\n        return self.diffstatus(\n            fr='HEAD' if self.get_hexsha() else None,\n            to=None,\n            paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state)\n\n    def diff(self, fr, to, paths=None, untracked='all',\n             eval_submodule_state='full'):\n        \"\"\"Like status(), but reports changes between to arbitrary revisions\n\n        Parameters\n        ----------\n        fr : str or None\n          Revision specification (anything that Git understands). Passing\n          `None` considers anything in the target state as new.\n        to : str or None\n          Revision specification (anything that Git understands), or None\n          to compare to the state of the work tree.\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when `to` is None:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        return {k: v for k, v in self.diffstatus(\n            fr=fr, to=to, paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state).items()\n            if v.get('state', None) != 'clean'}\n\n    def diffstatus(self, fr, to, paths=None, untracked='all',\n                   eval_submodule_state='full', _cache=None):\n        \"\"\"Like diff(), but reports the status of 'clean' content too.\n\n        It supports an additional submodule evaluation state 'global'.\n        If given, it will return a single 'modified'\n        (vs. 'clean') state label for the entire repository, as soon as\n        it can.\n        \"\"\"\n        def _get_cache_key(label, paths, ref, untracked=None):\n            return self.path, label, tuple(paths) if paths else None, \\\n                ref, untracked\n\n        if _cache is None:\n            _cache = {}\n\n        if paths is not None:\n            # at this point we must normalize paths to the form that\n            # Git would report them, to easy matching later on\n            paths = map(ut.Path, paths)\n            paths = [\n                p.relative_to(self.pathobj) if p.is_absolute() else p\n                for p in paths\n            ]\n\n        # TODO report more info from get_content_info() calls in return\n        # value, those are cheap and possibly useful to a consumer\n        # we need (at most) three calls to git\n        if to is None:\n            # everything we know about the worktree, including os.stat\n            # for each file\n            key = _get_cache_key('ci', paths, None, untracked)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(\n                    paths=paths, ref=None, untracked=untracked)\n                _cache[key] = to_state\n            # we want Git to tell us what it considers modified and avoid\n            # reimplementing logic ourselves\n            key = _get_cache_key('mod', paths, None)\n            if key in _cache:\n                modified = _cache[key]\n            else:\n                # from Git 2.31.0 onwards ls-files has --deduplicate\n                # by for backward compatibility keep doing deduplication here\n                modified = set(\n                    self.pathobj.joinpath(ut.PurePosixPath(p))\n                    for p in self.call_git_items_(\n                        # we must also look for deleted files, for the logic\n                        # below to work. Only from Git 2.31.0 would they be\n                        # included with `-m` alone\n                        ['ls-files', '-z', '-m', '-d'],\n                        # low-level code cannot handle pathobjs\n                        files=[str(p) for p in paths] if paths is not None else None,\n                        sep='\\0',\n                        read_only=True)\n                    if p)\n                _cache[key] = modified\n        else:\n            key = _get_cache_key('ci', paths, to)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(paths=paths, ref=to)\n                _cache[key] = to_state\n            # we do not need worktree modification detection in this case\n            modified = None\n        # origin state\n        key = _get_cache_key('ci', paths, fr)\n        if key in _cache:\n            from_state = _cache[key]\n        else:\n            if fr:\n                from_state = self.get_content_info(paths=paths, ref=fr)\n            else:\n                # no ref means from nothing\n                from_state = {}\n            _cache[key] = from_state\n\n        status = dict()\n        for f, to_state_r in to_state.items():\n            props = self._diffstatus_get_state_props(\n                f,\n                from_state.get(f, None),\n                to_state_r,\n                # are we comparing against a recorded commit or the worktree\n                to is not None,\n                # if we have worktree modification info, report if\n                # path is reported as modified in it\n                modified and f in modified,\n                eval_submodule_state)\n            # potential early exit in \"global\" eval mode\n            if eval_submodule_state == 'global' and \\\n                    props.get('state', None) not in ('clean', None):\n                # any modification means globally 'modified'\n                return 'modified'\n            status[f] = props\n\n        for f, from_state_r in from_state.items():\n            if f not in to_state:\n                # we new this, but now it is gone and Git is not complaining\n                # about it being missing -> properly deleted and deletion\n                # stages\n                status[f] = dict(\n                    state='deleted',\n                    type=from_state_r['type'],\n                    # report the shasum to distinguish from a plainly vanished\n                    # file\n                    gitshasum=from_state_r['gitshasum'],\n                )\n                if eval_submodule_state == 'global':\n                    return 'modified'\n\n        if to is not None or eval_submodule_state == 'no':\n            # if we have `to` we are specifically comparing against\n            # a recorded state, and this function only attempts\n            # to label the state of a subdataset, not investigate\n            # specifically what the changes in subdatasets are\n            # this is done by a high-level command like rev-diff\n            # so the comparison within this repo and the present\n            # `state` label are all we need, and they are done already\n            if eval_submodule_state == 'global':\n                return 'clean'\n            else:\n                return status\n\n        # loop over all subdatasets and look for additional modifications\n        for f, st in status.items():\n            f = str(f)\n            if 'state' in st or not st['type'] == 'dataset':\n                # no business here\n                continue\n            if not GitRepo.is_valid_repo(f):\n                # submodule is not present, no chance for a conflict\n                st['state'] = 'clean'\n                continue\n            # we have to recurse into the dataset and get its status\n            subrepo = repo_from_path(f)\n            # get the HEAD commit, or the one of the corresponding branch\n            # only that one counts re super-sub relationship\n            # save() syncs the corresponding branch each time\n            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())\n            st['gitshasum'] = subrepo_commit\n            # subdataset records must be labeled clean up to this point\n            # test if current commit in subdataset deviates from what is\n            # recorded in the dataset\n            st['state'] = 'modified' \\\n                if st['prev_gitshasum'] != subrepo_commit \\\n                else 'clean'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n            if eval_submodule_state == 'commit':\n                continue\n            # the recorded commit did not change, so we need to make\n            # a more expensive traversal\n            st['state'] = subrepo.diffstatus(\n                # we can use 'HEAD' because we know that the commit\n                # did not change. using 'HEAD' will facilitate\n                # caching the result\n                fr='HEAD',\n                to=None,\n                paths=None,\n                untracked=untracked,\n                eval_submodule_state='global',\n                _cache=_cache) if st['state'] == 'clean' else 'modified'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n\n        if eval_submodule_state == 'global':\n            return 'clean'\n        else:\n            return status\n\n    def _diffstatus_get_state_props(self, f, from_state, to_state,\n                                    against_commit,\n                                    modified_in_worktree,\n                                    eval_submodule_state):\n        \"\"\"Helper to determine diff properties for a single path\n\n        Parameters\n        ----------\n        f : Path\n        from_state : dict\n        to_state : dict\n        against_commit : bool\n          Flag whether `to_state` reflects a commit or the worktree.\n        modified_in_worktree : bool\n          Flag whether a worktree modification is reported. This is ignored\n          when `against_commit` is True.\n        eval_submodule_state : {'commit', 'no', ...}\n        \"\"\"\n        if against_commit:\n            # we can ignore any worktree modification reported when\n            # comparing against a commit\n            modified_in_worktree = False\n\n        props = {}\n        if 'type' in to_state:\n            props['type'] = to_state['type']\n\n        to_sha = to_state['gitshasum']\n        from_sha = from_state['gitshasum'] if from_state else None\n\n        # determine the state of `f` from from_state and to_state records, if\n        # it can be determined conclusively from it. If not, it will\n        # stay None for now\n        state = None\n        if not from_state:\n            # this is new, or rather not known to the previous state\n            state = 'added' if to_sha else 'untracked'\n        elif to_sha == from_sha and not modified_in_worktree:\n            # something that is seemingly unmodified, based on the info\n            # gathered so far\n            if to_state['type'] == 'dataset':\n                if against_commit or eval_submodule_state == 'commit':\n                    # we compare against a recorded state, just based on\n                    # the shas we can be confident, otherwise the state\n                    # of a subdataset isn't fully known yet, because\n                    # `modified_in_worktree` will only reflect changes\n                    # in the commit of a subdataset without looking into\n                    # it for uncommitted changes. Such tests are done\n                    # later and based on further conditionals for\n                    # performance reasons\n                    state = 'clean'\n            else:\n                # no change in git record, and no change on disk\n                # at this point we know that the reported object ids\n                # for this file are identical in the to and from\n                # records.  If to is None, we're comparing to the\n                # working tree and a deleted file will still have an\n                # identical id, so we need to check whether the file is\n                # gone before declaring it clean. This working tree\n                # check is irrelevant and wrong if to is a ref.\n                state = 'clean' \\\n                    if against_commit or (f.exists() or f.is_symlink()) \\\n                    else 'deleted'\n        else:\n            # change in git record, or on disk\n            # for subdatasets leave the 'modified' judgement to the caller\n            # for supporting corner cases, such as adjusted branch\n            # which require inspection of a subdataset\n            # TODO we could have a new file that is already staged\n            # but had subsequent modifications done to it that are\n            # unstaged. Such file would presently show up as 'added'\n            # ATM I think this is OK, but worth stating...\n            state = ('modified'\n                     if against_commit or to_state['type'] != 'dataset'\n                     else None\n                    ) if f.exists() or f.is_symlink() else 'deleted'\n            # TODO record before and after state for diff-like use\n            # cases\n\n        if state in ('clean', 'added', 'modified', None):\n            # assign present gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['gitshasum'] = to_sha\n            if 'bytesize' in to_state:\n                # if we got this cheap, report it\n                props['bytesize'] = to_state['bytesize']\n            elif state == 'clean' and 'bytesize' in from_state:\n                # no change, we can take this old size info\n                props['bytesize'] = from_state['bytesize']\n        if state in ('clean', 'modified', 'deleted', None):\n            # assign previous gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['prev_gitshasum'] = from_sha\n        if state:\n            # only report a state if we could determine any\n            # outside code tests for existence of the property\n            # and not (always) for the value\n            props['state'] = state\n        return props\n\n    def _save_pre(self, paths, _status, **kwargs):\n        # helper to get an actionable status report\n        if paths is not None and not paths and not _status:\n            return\n        if _status is None:\n            if 'untracked' not in kwargs:\n                kwargs['untracked'] = 'normal'\n            status = self.status(\n                paths=paths,\n                **{k: kwargs[k] for k in kwargs\n                   if k in ('untracked', 'eval_submodule_state')})\n        else:\n            # we want to be able to add items down the line\n            # make sure to detach from prev. owner\n            status = _status.copy()\n        return status\n\n    def get_staged_paths(self):\n        \"\"\"Returns a list of any stage repository path(s)\n\n        This is a rather fast call, as it will not depend on what is going on\n        in the worktree.\n        \"\"\"\n        try:\n            return list(self.call_git_items_(\n                ['diff', '--name-only', '--staged'],\n                expect_stderr=True))\n        except CommandError as e:\n            lgr.debug(CapturedException(e))\n            return []\n\n    def _save_post(self, message, files, partial_commit, amend=False,\n                   allow_empty=False):\n        # helper to commit changes reported in status\n\n        # TODO remove pathobj stringification when commit() can\n        # handle it\n        to_commit = [str(f.relative_to(self.pathobj))\n                     for f in files] \\\n                    if partial_commit else None\n        if not partial_commit or to_commit or allow_empty or \\\n                (amend and message):\n            # we directly call GitRepo.commit() to avoid a whole slew\n            # if direct-mode safeguards and workarounds in the AnnexRepo\n            # implementation (which also run an additional dry-run commit\n            GitRepo.commit(\n                self,\n                files=to_commit,\n                msg=message,\n                options=to_options(amend=amend, allow_empty=allow_empty),\n                # do not raise on empty commit\n                # it could be that the `add` in this save-cycle has already\n                # brought back a 'modified' file into a clean state\n                careless=True,\n            )\n\n    def save(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Save dataset content.\n\n        Parameters\n        ----------\n        message : str or None\n          A message to accompany the changeset in the log. If None,\n          a default message is used.\n        paths : list or None\n          Any content with path matching any of the paths given in this\n          list will be saved. Matching will be performed against the\n          dataset status (GitRepo.status()), or a custom status provided\n          via `_status`. If no paths are provided, ALL non-clean paths\n          present in the repo status or `_status` will be saved.\n        _status : dict or None\n          If None, Repo.status() will be queried for the given `ds`. If\n          a dict is given, its content will be used as a constraint.\n          For example, to save only modified content, but no untracked\n          content, set `paths` to None and provide a `_status` that has\n          no entries for untracked content.\n        **kwargs :\n          Additional arguments that are passed to underlying Repo methods.\n          Supported:\n\n          - git : bool (passed to Repo.add()\n          - eval_submodule_state : {'full', 'commit', 'no'}\n            passed to Repo.status()\n          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()\n          - amend : bool (passed to GitRepo.commit)\n        \"\"\"\n        return list(\n            self.save_(\n                message=message,\n                paths=paths,\n                _status=_status,\n                **kwargs\n            )\n        )\n\n    def save_(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Like `save()` but working as a generator.\"\"\"\n        from datalad.interface.results import get_status_dict\n\n        status_state = _get_save_status_state(\n            self._save_pre(paths, _status, **kwargs) or {}\n        )\n        amend = kwargs.get('amend', False)\n\n        # TODO: check on those None's -- may be those are also \"nothing to worry about\"\n        # and we could just return?\n        if not any(status_state.values()) and not (message and amend):\n            # all clean, nothing todo\n            lgr.debug('Nothing to save in %r, exiting early', self)\n            return\n\n        # three things are to be done:\n        # - remove (deleted if not already staged)\n        # - add (modified/untracked)\n        # - commit (with all paths that have been touched, to bypass\n        #   potential pre-staged bits)\n\n        staged_paths = self.get_staged_paths()\n        need_partial_commit = bool(staged_paths)\n        if need_partial_commit and hasattr(self, \"call_annex\"):\n            # so we have some staged content. let's check which ones\n            # are symlinks -- those could be annex key links that\n            # are broken after a `git-mv` operation\n            # https://github.com/datalad/datalad/issues/4967\n            # call `git-annex pre-commit` on them to rectify this before\n            # saving the wrong symlinks\n            added = status_state['added']\n            tofix = [\n                sp for sp in staged_paths\n                if added.get(self.pathobj / sp, {}).get(\"type\") == \"symlink\"\n            ]\n            if tofix:\n                self.call_annex(['pre-commit'], files=tofix)\n\n        submodule_change = False\n\n        if status_state['deleted']:\n            vanished_subds = [\n                str(f.relative_to(self.pathobj))\n                for f, props in status_state['deleted'].items()\n                if props.get('type') == 'dataset'\n            ]\n            if vanished_subds:\n                # we submodule removal we use `git-rm`, because the clean-up\n                # is more complex than just an index update -- make no\n                # sense to have a duplicate implementation.\n                # we do not yield here, but only altogether below -- we are just\n                # processing gone components, should always be quick.\n                self._call_git(['rm', '-q'], files=vanished_subds)\n                submodule_change = True\n            # remove anything from the index that was found to be gone\n            self._call_git(\n                ['update-index', '--remove'],\n                files=[\n                    str(f.relative_to(self.pathobj))\n                    for f, props in status_state['deleted'].items()\n                    # do not update the index, if there is already\n                    # something staged for this path (e.g.,\n                    # a directory was removed and a file staged\n                    # in its place)\n                    if not props.get('gitshasum')\n                    # we already did the submodules\n                    and props.get('type') != 'dataset'\n                ]\n            )\n            # now yield all deletions\n            for p, props in status_state['deleted'].items():\n                yield get_status_dict(\n                    action='delete',\n                    refds=self.pathobj,\n                    type=props.get('type'),\n                    path=p,\n                    status='ok',\n                    logger=lgr)\n\n        # TODO this additional query should not be, based on status as given\n        # if anyhow possible, however, when paths are given, status may\n        # not contain all required information. In case of path=None AND\n        # _status=None, we should be able to avoid this, because\n        # status should have the full info already\n        # looks for contained repositories\n        untracked_dirs = [\n            f.relative_to(self.pathobj)\n            for f, props in status_state['untracked'].items()\n            if props.get('type', None) == 'directory']\n        to_add_submodules = []\n        if untracked_dirs:\n            to_add_submodules = [\n                sm for sm, sm_props in\n                self.get_content_info(\n                    untracked_dirs,\n                    ref=None,\n                    # request exhaustive list, so that everything that is\n                    # still reported as a directory must be its own repository\n                    untracked='all').items()\n                if sm_props.get('type', None) == 'directory']\n            to_add_submodules = _prune_deeper_repos(to_add_submodules)\n\n        to_stage_submodules = {\n            f: props\n            for f, props in status_state['modified_or_untracked'].items()\n            if props.get('type', None) == 'dataset'}\n        if to_stage_submodules:\n            lgr.debug(\n                '%i submodule path(s) to stage in %r %s',\n                len(to_stage_submodules), self,\n                to_stage_submodules\n                if len(to_stage_submodules) < 10 else '')\n            to_add_submodules += list(to_stage_submodules)\n\n        if to_add_submodules:\n            for r in self._save_add_submodules(to_add_submodules):\n                if r.get('status', None) == 'ok':\n                    submodule_change = True\n                yield r\n\n        if submodule_change:\n            # this will alter the config, reload\n            self.config.reload()\n            # need to include .gitmodules in what needs committing\n            f = self.pathobj.joinpath('.gitmodules')\n            status_state['modified_or_untracked'][f] = \\\n                status_state['modified'][f] = \\\n                dict(type='file', state='modified')\n            # now stage .gitmodules\n            self._call_git(['update-index', '--add'], files=['.gitmodules'])\n            # and report on it\n            yield get_status_dict(\n                action='add',\n                refds=self.pathobj,\n                type='file',\n                path=f,\n                status='ok',\n                logger=lgr)\n\n        to_add = {\n            # TODO remove pathobj stringification when add() can\n            # handle it\n            str(f.relative_to(self.pathobj)): props\n            for f, props in status_state['modified_or_untracked'].items()\n            if not (f in to_add_submodules or f in to_stage_submodules)}\n        if to_add:\n            compat_config = \\\n                self.config.obtain(\"datalad.save.windows-compat-warning\")\n            to_add, problems = self._check_for_win_compat(to_add, compat_config)\n            lgr.debug(\n                '%i path(s) to add to %s %s',\n                len(to_add), self, to_add if len(to_add) < 10 else '')\n\n            if to_add:\n                yield from self._save_add(\n                        to_add,\n                        git_opts=None,\n                        **{k: kwargs[k] for k in kwargs\n                           if k in (('git',) if hasattr(self, 'uuid')\n                                    else tuple())})\n            if problems:\n                from datalad.interface.results import get_status_dict\n                msg = \\\n                    'Incompatible name for Windows systems; disable with ' \\\n                    'datalad.save.windows-compat-warning.',\n                for path in problems:\n                    yield get_status_dict(\n                        action='save',\n                        refds=self.pathobj,\n                        type='file',\n                        path=(self.pathobj / ut.PurePosixPath(path)),\n                        status='impossible',\n                        message=msg,\n                        logger=lgr)\n\n\n        # https://github.com/datalad/datalad/issues/6558\n        # file could have become a directory. Unfortunately git\n        # would then mistakenly refuse to commit if that old path is also\n        # given to commit, so we better filter it out\n        if status_state['deleted'] and status_state['added']:\n            # check if any \"deleted\" is a directory now. Then for those\n            # there should be some other path under that directory in 'added'\n            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:\n                # this could potentially be expensive if lots of files become\n                # directories, but it is unlikely to happen often\n                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish\n                # path_is_subpath faster, also if comparing to \"in f.parents\"\n                f_str = str(f)\n                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):\n                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post\n\n        # Note, that allow_empty is always ok when we amend. Required when we\n        # amend an empty commit while the amendment is empty, too (though\n        # possibly different message). If an empty commit was okay before, it's\n        # okay now.\n        status_state.pop('modified_or_untracked')  # pop the hybrid state\n        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,\n                        allow_empty=amend)\n        # TODO yield result for commit, prev helper checked hexsha pre\n        # and post...\n\n    def _check_for_win_compat(self, files, config):\n        \"\"\"Check file names for illegal characters or reserved names on Windows\n\n        In the case that a non-Windows-compatible file is detected, warn users\n        about potential interoperability issues.\n\n        Parameters\n        ----------\n        files\n          list of files to add\n        config\n          value of self.config.obtain(\"datalad.save.windows-compat-warning\"),\n          used to choose appropriate behavior. \"none\" performs no check,\n          \"warning\" warns in case of incompatibilities, and \"error\" results in\n          an error result in case of incompatibilities\n        \"\"\"\n        # don't perform any check when the configuration is set to 'none'\n        if config == 'none':\n            return files, None\n\n        from collections import defaultdict\n        problems = defaultdict(list)\n        for file in files:\n            for part in Path(file).parts:\n                # check every component of the path for incompatibilities\n                if Path(part).stem.upper() in RESERVED_NAMES_WIN:\n                    problems['Elements using a reserved filename:'].append(part)\n                    problems['paths'].append(file)\n                if re.search(ILLEGAL_CHARS_WIN, part):\n                    problems['Elements with illegal characters:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith('.'):\n                    problems['Elements ending with a dot:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith(' '):\n                    problems['Elements ending with a space:'].append(part)\n                    problems['paths'].append(file)\n        if not problems:\n            return files, None\n        msg = \\\n            \"Some elements of your dataset are not compatible with \" \\\n            \"Windows systems. Disable this check by changing \" \\\n            \"datalad.save.windows-compat-warning or consider renaming \" \\\n            \"the following elements: \"\n        for k, v in problems.items():\n            # use the key as an explanation, and report filenames only once\n            msg += f\"\\n{k} {[*{*v}]}\" if k != 'paths' else ''\n        if config == 'warning':\n            lgr.warning(msg)\n            return files, None\n\n        elif config == 'error':\n            # take the problematic files out of to_add\n            for path in [*{*problems['paths']}]:\n                files.pop(path)\n            return files, [*{*problems['paths']}]\n\n    def _save_add(self, files, git_opts=None):\n        \"\"\"Simple helper to add files in save()\"\"\"\n        from datalad.interface.results import get_status_dict\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.largefiles to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.largefiles=nothing', 'add'] +\n                ensure_list(git_opts) + ['--verbose'],\n                files=list(files.keys()),\n                pathspec_from_file=True,\n            )\n            # get all the entries\n            for r in self._process_git_get_output(*add_out):\n                yield get_status_dict(\n                    action=r.get('command', 'add'),\n                    refds=self.pathobj,\n                    type='file',\n                    path=(self.pathobj / ut.PurePosixPath(r['file']))\n                    if 'file' in r else None,\n                    status='ok' if r.get('success', None) else 'error',\n                    key=r.get('key', None),\n                    # while there is no git-annex underneath here, we\n                    # tend to fake its behavior, so we can also support\n                    # this type of messaging\n                    message='\\n'.join(r['error-messages'])\n                    if 'error-messages' in r else None,\n                    logger=lgr)\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n    def _save_add_submodules(self, paths):\n        \"\"\"Add new submodules, or updates records of existing ones\n\n        This method does not use `git submodule add`, but aims to be more\n        efficient by limiting the scope to mere in-place registration of\n        multiple already present repositories.\n\n        Parameters\n        ----------\n        paths : list(Path)\n\n        Yields\n        ------\n        dict\n          Result records\n        \"\"\"\n        from datalad.interface.results import get_status_dict\n\n        # first gather info from all datasets in read-only fashion, and then\n        # update index, .gitmodules and .git/config at once\n        info = []\n        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843\n        # We must not add already known submodules explicitly since \"untracked\"\n        # can be assigned even for known ones (TODO: add issue, might have been closed)?\n        # Not sure if operating on relative paths would provide any speed up so use full\n        known_sub_paths = {s['path'] for s in self.get_submodules_()}\n        for path in paths:\n            already_known = path in known_sub_paths\n            rpath = str(path.relative_to(self.pathobj).as_posix())\n            subm = repo_from_path(path)\n            # if there is a corresponding branch, we want to record it's state.\n            # we rely on the corresponding branch being synced already.\n            # `save` should do that each time it runs.\n            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())\n            if not subm_commit:\n                yield get_status_dict(\n                    action='add_submodule',\n                    ds=self,\n                    path=path,\n                    status='error',\n                    message=('cannot add subdataset %s with no commits', subm),\n                    logger=lgr)\n                continue\n            # make an attempt to configure a submodule source URL based on the\n            # discovered remote configuration\n            remote, branch = subm.get_tracking_branch()\n            url = subm.get_remote_url(remote) if remote else None\n            if url is None:\n                url = './{}'.format(rpath)\n            subm_id = subm.config.get('datalad.dataset.id', None)\n            info.append(\n                dict(\n                     # if we have additional information on this path, pass it on.\n                     # if not, treat it as an untracked directory\n                     paths[path] if isinstance(paths, dict)\n                     else dict(type='directory', state='untracked'),\n                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,\n                     url=url, known=already_known))\n\n        # bypass any convenience or safe-manipulator for speed reasons\n        # use case: saving many new subdatasets in a single run\n        with (self.pathobj / '.gitmodules').open('a') as gmf, \\\n             (self.pathobj / '.git' / 'config').open('a') as gcf:\n            for i in info:\n                # we update the subproject commit unconditionally\n                self.call_git([\n                    'update-index', '--add', '--replace', '--cacheinfo', '160000',\n                    i['commit'], i['rpath']\n                ])\n                # only write the .gitmodules/.config changes when this is not yet\n                # a subdataset and not yet already known\n                # TODO: we could update the URL, and branch info at this point,\n                # even for previously registered subdatasets\n                if not i['known'] and (i['type'] != 'dataset' or (\n                        i['type'] == 'dataset' and i['state'] == 'untracked')):\n                    gmprops = dict(path=i['rpath'], url=i['url'])\n                    if i['id']:\n                        gmprops['datalad-id'] = i['id']\n                    write_config_section(\n                        gmf, 'submodule', i['rpath'], gmprops)\n                    write_config_section(\n                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))\n\n                # This mirrors the result structure yielded for\n                # to_stage_submodules below.\n                yield get_status_dict(\n                    action='add',\n                    refds=self.pathobj,\n                    type='dataset',\n                    key=None,\n                    path=i['path'],\n                    status='ok',\n                    logger=lgr)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 2244,
                            "end_lineno": 2253,
                            "parent": {
                                "file_name": "datalad/support/gitrepo.py",
                                "type": "class",
                                "name": "GitRepo",
                                "content": "class GitRepo(CoreGitRepo):\n    \"\"\"Representation of a git repository\n\n    \"\"\"\n    # We must check git config to have name and email set, but\n    # should do it once\n    _config_checked = False\n\n    GIT_MIN_VERSION = \"2.19.1\"\n    git_version = None\n\n    @classmethod\n    def _check_git_version(cls):\n        external_versions.check(\"cmd:git\", min_version=cls.GIT_MIN_VERSION)\n        cls.git_version = external_versions['cmd:git']\n\n    # This is the least common denominator to claim that a user\n    # used DataLad.\n    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262\n    @due.dcite(Doi(\"10.21105/joss.03262\"),\n               # override path since there is no need ATM for such details\n               path=\"datalad\",\n               description=\"DataLad - Data management and distribution platform\")\n    def __init__(self, path, runner=None, create=True,\n                 git_opts=None, repo=None, fake_dates=False,\n                 create_sanity_checks=True,\n                 **kwargs):\n        \"\"\"Creates representation of git repository at `path`.\n\n        Can also be used to create a git repository at `path`.\n\n        Parameters\n        ----------\n        path: str\n          path to the git repository; In case it's not an absolute path,\n          it's relative to PWD\n        create: bool, optional\n          if true, creates a git repository at `path` if there is none. Also\n          creates `path`, if it doesn't exist.\n          If set to false, an exception is raised in case `path` doesn't exist\n          or doesn't contain a git repository.\n        repo: git.Repo, optional\n          This argument is ignored.\n        create_sanity_checks: bool, optional\n          Whether to perform sanity checks during initialization (when\n          `create=True` and target path is not a valid repo already), such as\n          that new repository is not created in the directory where git already\n          tracks some files.\n        kwargs:\n          keyword arguments serving as additional options to the git-init\n          command. Therefore, it makes sense only if called with `create`.\n\n          Generally, this way of passing options to the git executable is\n          (or will be) used a lot in this class. It's a transformation of\n          python-style keyword arguments (or a `dict`) to command line arguments,\n          provided by GitPython.\n\n          A single character keyword will be prefixed by '-', multiple characters\n          by '--'. An underscore in the keyword becomes a dash. The value of the\n          keyword argument is used as the value for the corresponding command\n          line argument. Assigning a boolean creates a flag.\n\n          Examples:\n          no_commit=True => --no-commit\n          C='/my/path'   => -C /my/path\n\n        \"\"\"\n        # this will set up .pathobj and .dot_git\n        super().__init__(path)\n\n        if self.git_version is None:\n            self._check_git_version()\n\n        # BEGIN Repo validity test\n        # We want to fail early for tests, that would be performed a lot. In\n        # particular this is about GitRepo.is_valid_repo. We would use the\n        # latter to decide whether or not to call GitRepo() only for __init__ to\n        # then test the same things again. If we fail early we can save the\n        # additional test from outer scope.\n        self.path = path\n\n        # Note, that the following three path objects are used often and\n        # therefore are stored for performance. Path object creation comes with\n        # a cost. Most notably, this is used for validity checking of the\n        # repository.\n        _valid_repo = self.is_valid_git()\n\n        do_create = False\n        if create and not _valid_repo:\n            if repo is not None:\n                # `repo` passed with `create`, which doesn't make sense\n                raise TypeError(\"argument 'repo' must not be used with 'create'\")\n            do_create = True\n        else:\n            # Note: We used to call gitpy.Repo(path) here, which potentially\n            # raised NoSuchPathError or InvalidGitRepositoryError. This is\n            # used by callers of GitRepo.__init__() to detect whether we have a\n            # valid repo at `path`. Now, with switching to lazy loading property\n            # `repo`, we detect those cases without instantiating a\n            # gitpy.Repo().\n\n            if not exists(path):\n                raise NoSuchPathError(path)\n            if not _valid_repo:\n                raise InvalidGitRepositoryError(path)\n        # END Repo validity test\n\n        # So that we \"share\" control paths with git/git-annex\n        if ssh_manager:\n            ssh_manager.ensure_initialized()\n\n        # note: we may also want to distinguish between a path to the worktree\n        # and the actual repository\n\n        if git_opts is None:\n            git_opts = {}\n        if kwargs:\n            git_opts.update(kwargs)\n\n        self._cfg = None\n\n        if do_create:  # we figured it out earlier\n            from_cmdline = git_opts.pop('_from_cmdline_', [])\n            self.init(\n                sanity_checks=create_sanity_checks,\n                init_options=from_cmdline + to_options(**git_opts),\n            )\n\n        # with DryRunProtocol path might still not exist\n        if exists(self.path):\n            self.inode = os.stat(self.path).st_ino\n        else:\n            self.inode = None\n\n        if fake_dates:\n            self.configure_fake_dates()\n\n    @property\n    def bare(self):\n        \"\"\"Returns a bool indicating whether the repository is bare\n\n        Importantly, this is not reporting the configuration value\n        of 'core.bare', in order to be usable at a stage where a\n        Repo instance is not yet equipped with a ConfigManager.\n        Instead, it is testing whether the repository path and its\n        \"dot_git\" are identical. The value of 'core.bare' can be query\n        from the ConfigManager in a fully initialized instance.\n        \"\"\"\n        return self.pathobj == self.dot_git\n\n    @classmethod\n    def clone(cls, url, path, *args, clone_options=None, **kwargs):\n        \"\"\"Clone url into path\n\n        Provides workarounds for known issues (e.g.\n        https://github.com/datalad/datalad/issues/785)\n\n        Parameters\n        ----------\n        url : str\n        path : str\n        clone_options : dict or list\n          Arbitrary options that will be passed on to the underlying call to\n          `git-clone`. This may be a list of plain options or key-value pairs\n          that will be converted to a list of plain options with `to_options`.\n        expect_fail : bool\n          Whether expect that command might fail, so error should be logged then\n          at DEBUG level instead of ERROR\n        kwargs:\n          Passed to the Repo class constructor.\n        \"\"\"\n\n        if 'repo' in kwargs:\n            raise TypeError(\"argument 'repo' conflicts with cloning\")\n            # TODO: what about 'create'?\n\n        expect_fail = kwargs.pop('expect_fail', False)\n        # fail early on non-empty target:\n        from os import listdir\n        if exists(path) and listdir(path):\n            raise ValueError(\n                \"destination path '%s' already exists and is not an \"\n                \"empty directory.\" % path)\n        else:\n            # protect against cloning into existing and obviously dangling\n            # instance for that location\n            try:\n                del cls._unique_instances[path]\n            except KeyError:\n                # didn't exist - all fine\n                pass\n\n        # Massage URL\n        url_ri = RI(url) if not isinstance(url, RI) else url\n        if on_windows:\n            # When we're cloning from a local path on Windows, the URL at\n            # this point is platform-specific (e.g., \"..\\\\origin\"). According \n            # to Git clone's manpage, clone urls can't have backslashes.\n            # While Git does manage to clone a URL with backslashes, \n            # in the case of subdatasets cloned from relative paths it nevertheless\n            # messed up the resulting remote url, resulting in a mix of\n            # front and backslashes (see also gh-7180): \n            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\\\origin' \n            # Therefore, we're turning it to Posix now.\n            if isinstance(url_ri, PathRI):\n                url = Path(url).as_posix()\n                url_ri = PathRI(url)\n\n        else:\n            # if we are on windows, the local path of a URL\n            # would not end up being a proper local path and cloning\n            # would fail. Don't try to be smart and just pass the\n            # URL along unmodified\n\n            # try to get a local path from `url`:\n            try:\n                url = url_ri.localpath\n                url_ri = RI(url)\n            except ValueError:\n                pass\n\n        if is_ssh(url_ri):\n            ssh_manager.get_connection(url).open()\n        else:\n            if isinstance(url_ri, PathRI):\n                # expand user, because execution not going through a shell\n                # doesn't work well otherwise\n                new_url = os.path.expanduser(url)\n                if url != new_url:\n                    lgr.info(\"Expanded source path to %s from %s\", new_url, url)\n                    url = new_url\n\n        cmd = cls._git_cmd_prefix + ['clone', '--progress']\n        if clone_options:\n            if isinstance(clone_options, Mapping):\n                clone_options = to_options(**clone_options)\n            cmd.extend(clone_options)\n        cmd.extend([url, path])\n\n        fix_annex = None\n        ntries = 5  # 3 is not enough for robust workaround\n        for trial in range(ntries):\n            try:\n                lgr.debug(\"Git clone from %s to %s\", url, path)\n\n                res = GitWitlessRunner().run(cmd, protocol=GitProgress)\n                # fish out non-critical warnings by git-clone\n                # (empty repo clone, etc.), all other content is logged\n                # by the progress helper to 'debug'\n                for errline in res['stderr'].splitlines():\n                    if errline.startswith('warning:'):\n                        lgr.warning(errline[8:].strip())\n                lgr.debug(\"Git clone completed\")\n                break\n            except CommandError as e:\n                # log here but let caller decide what to do\n                ce = CapturedException(e)\n                str_e = str(e)\n                # see https://github.com/datalad/datalad/issues/785\n                if re.search(\"Request for .*aborted.*Unable to find\", str_e,\n                             re.DOTALL) \\\n                        and trial < ntries - 1:\n                    lgr.info(\n                        \"Hit a known issue with Git (see GH#785). Trial #%d, \"\n                        \"retrying\",\n                        trial)\n                    continue\n                    #(lgr.debug if expect_fail else lgr.error)(e_str)\n\n                if \"Clone succeeded, but checkout failed.\" in str_e:\n                    fix_annex = ce\n                    break\n\n                raise\n\n        # get ourselves a repository instance\n        gr = cls(path, *args, **kwargs)\n        if fix_annex:\n            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then\n            # would need to import our own subclass\n            if hasattr(gr, 'is_valid_annex'):\n                lgr.warning(\"Experienced issues while cloning. \"\n                            \"Trying to fix it, using git-annex-fsck.\")\n                if not gr.is_initialized():\n                    gr._init()\n                gr.fsck()\n            else:\n                lgr.warning(\"Experienced issues while cloning: %s\", fix_annex)\n        # ensure that Git doesn't mangle relative paths into obscure absolute\n        # paths: https://github.com/datalad/datalad/issues/3538\n        if isinstance(url_ri, PathRI):\n            url_path = Path(url)\n            if not url_path.is_absolute():\n                # get git-created path\n                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'\n                git_url = gr.config.get(remote_url)\n                if Path(git_url).is_absolute():\n                    # Git created an absolute path from a relative URL.\n                    git_url = op.relpath(git_url, gr.path)\n                path = Path(git_url)\n                # always in POSIX even on Windows\n                path = path.as_posix()\n                gr.config.set(remote_url, path,\n                              scope='local', force=True)\n        return gr\n\n    # Note: __del__ shouldn't be needed anymore as we switched to\n    #       `weakref.finalize`.\n    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods\n    #\n    #       Keeping both methods and this comment around as a reminder to not\n    #       use __del__, if we figure there's a need for cleanup in the future.\n    #\n    # def __del__(self):\n    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird\n    #     # stalls etc\n    #     self._cfg = None\n\n    def is_valid_git(self):\n        \"\"\"Returns whether the underlying repository appears to be still valid\n\n        Note, that this almost identical to the classmethod is_valid_repo().\n        However, if we are testing an existing instance, we can save Path object\n        creations. Since this testing is done a lot, this is relevant. Creation\n        of the Path objects in is_valid_repo() takes nearly half the time of the\n        entire function.\n\n        Also note, that this method is bound to an instance but still\n        class-dependent, meaning that a subclass cannot simply overwrite it.\n        This is particularly important for the call from within __init__(),\n        which in turn is called by the subclasses' __init__. Using an overwrite\n        would lead to the wrong thing being called.\n        \"\"\"\n        return self.is_valid()\n\n    @classmethod\n    def is_valid_repo(cls, path):\n        \"\"\"Returns if a given path points to a git repository\"\"\"\n        return cls.is_valid(path)\n\n    @staticmethod\n    def get_git_dir(repo):\n        \"\"\"figure out a repo's gitdir\n\n        '.git' might be a  directory, a symlink or a file\n\n        Note\n        ----\n        This method is likely to get deprecated, please use GitRepo.dot_git instead!\n        That one's not static, but it's cheaper and you should avoid\n        not having an instance of a repo you're working on anyway.\n        Note, that the property in opposition to this method returns an absolute path.\n\n\n        Parameters\n        ----------\n        repo: path or Repo instance\n          currently expected to be the repos base dir\n\n        Returns\n        -------\n        str\n          relative path to the repo's git dir; So, default would be \".git\"\n        \"\"\"\n        if isinstance(repo, GitRepo):\n            return str(repo.dot_git)\n        pathobj = Path(repo)\n        dot_git = _get_dot_git(pathobj, ok_missing=False)\n        try:\n            dot_git = dot_git.relative_to(pathobj)\n        except ValueError:\n            # is not a subpath, return as is\n            lgr.debug(\"Path %r is not subpath of %r\", dot_git, pathobj)\n        return str(dot_git)\n\n    @property\n    def config(self):\n        # just proxy the core repo APIs property for backward-compatibility\n        return self.cfg\n\n    def is_with_annex(self):\n        \"\"\"Report if GitRepo (assumed) has (remotes with) a git-annex branch\n        \"\"\"\n        return any(\n            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])\n        )\n\n    @classmethod\n    def get_toppath(cls, path, follow_up=True, git_options=None):\n        \"\"\"Return top-level of a repository given the path.\n\n        Parameters\n        -----------\n        follow_up : bool\n          If path has symlinks -- they get resolved by git.  If follow_up is\n          True, we will follow original path up until we hit the same resolved\n          path.  If no such path found, resolved one would be returned.\n        git_options: list of str\n          options to be passed to the git rev-parse call\n\n        Return None if no parent directory contains a git repository.\n        \"\"\"\n        cmd = ['git']\n        if git_options:\n            cmd.extend(git_options)\n        cmd += [\"rev-parse\", \"--show-toplevel\"]\n        try:\n            out = GitWitlessRunner(cwd=path).run(\n                cmd, protocol=StdOutErrCapture)\n            toppath = out['stdout'].rstrip('\\n\\r')\n        except CommandError:\n            return None\n        except OSError:\n            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,\n                                          git_options=git_options)\n\n        # normalize the report, because, e.g. on windows it can come out\n        # with improper directory separators (C:/Users/datalad)\n        toppath = str(Path(toppath))\n\n        if follow_up:\n            path_ = path\n            path_prev = \"\"\n            while path_ and path_ != path_prev:  # on top /.. = /\n                if str(Path(path_).resolve()) == toppath:\n                    toppath = path_\n                    break\n                path_prev = path_\n                path_ = dirname(path_)\n\n        return toppath\n\n    @normalize_paths\n    def add(self, files, git=True, git_options=None, update=False):\n        \"\"\"Adds file(s) to the repository.\n\n        Parameters\n        ----------\n        files: list\n          list of paths to add\n        git: bool\n          somewhat ugly construction to be compatible with AnnexRepo.add();\n          has to be always true.\n        update: bool\n          --update option for git-add. From git's manpage:\n           Update the index just where it already has an entry matching\n           <pathspec>. This removes as well as modifies index entries to match\n           the working tree, but adds no new files.\n\n           If no <pathspec> is given when --update option is used, all tracked\n           files in the entire working tree are updated (old versions of Git\n           used to limit the update to the current directory and its\n           subdirectories).\n\n        Returns\n        -------\n        list\n          Of status dicts.\n        \"\"\"\n        # under all circumstances call this class' add_ (otherwise\n        # AnnexRepo.add would go into a loop\n        return list(GitRepo.add_(self, files, git=git, git_options=git_options,\n                    update=update))\n\n    def add_(self, files, git=True, git_options=None, update=False):\n        \"\"\"Like `add`, but returns a generator\"\"\"\n        # TODO: git_options is used as options for the git-add here,\n        # instead of options to the git executable => rename for consistency\n\n        if not git:\n            lgr.warning(\n                'GitRepo.add() called with git=%s, this should not happen',\n                git)\n            git = True\n\n        # there is no other way then to collect all files into a list\n        # at this point, because we need to pass them at once to a single\n        # `git add` call\n        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]\n\n        if not (files or git_options or update):\n            # wondering why just a warning? in cmdline this is also not an error\n            lgr.warning(\"add was called with empty file list and no options.\")\n            return\n\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.gitaddtoannex to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.gitaddtoannex=false', 'add'] +\n                ensure_list(git_options) +\n                to_options(update=update) + ['--verbose'],\n                files=files,\n                pathspec_from_file=True,\n                read_only=False,\n            )\n            # get all the entries\n            for o in self._process_git_get_output(*add_out):\n                yield o\n            # Note: as opposed to git cmdline, force is True by default in\n            #       gitpython, which would lead to add things, that are\n            #       ignored or excluded otherwise\n            # 2. Note: There is an issue with globbing (like adding '.'),\n            #       which apparently doesn't care for 'force' and therefore\n            #       adds '.git/...'. May be it's expanded at the wrong\n            #       point in time or sth. like that.\n            # For now, use direct call to git add.\n            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,\n            #                      force=False)\n            # TODO: May be make use of 'fprogress'-option to indicate\n            # progress\n            # But then, we don't have it for git-annex add, anyway.\n            #\n            # TODO: Is write=True a reasonable way to do it?\n            # May be should not write until success of operation is\n            # confirmed?\n            # What's best in case of a list of files?\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n        # Make sure return value from GitRepo is consistent with AnnexRepo\n        # currently simulating similar return value, assuming success\n        # for all files:\n        # TODO: Make return values consistent across both *Repo classes!\n        return\n\n    @staticmethod\n    def _process_git_get_output(stdout, stderr=None):\n        \"\"\"Given both outputs (stderr is ignored atm) of git add - process it\n\n        Primarily to centralize handling in both indirect annex and direct\n        modes when ran through proxy\n        \"\"\"\n        return [{u'file': f, u'success': True}\n                for f in re.findall(\"'(.*)'[\\n$]\", ensure_unicode(stdout))]\n\n    @normalize_paths(match_return_type=False)\n    def remove(self, files, recursive=False, **kwargs):\n        \"\"\"Remove files.\n\n        Calls git-rm.\n\n        Parameters\n        ----------\n        files: list of str\n          list of paths to remove\n        recursive: False\n          whether to allow recursive removal from subdirectories\n        kwargs:\n          see `__init__`\n\n        Returns\n        -------\n        [str]\n          list of successfully removed files.\n        \"\"\"\n        if recursive:\n            kwargs['r'] = True\n\n        # the name is chosen badly, but the purpose is to make sure that\n        # any pending operations actually manifest themselves in the Git repo\n        # on disk (in case of an AnnexRepo, it could be pending batch\n        # processes that need closing)\n        self.precommit()\n\n        # output per removed file is expected to be \"rm 'PATH'\":\n        return [\n            line.strip()[4:-1]\n            for line in self.call_git_items_(\n                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)\n        ]\n\n    def precommit(self):\n        \"\"\"Perform pre-commit maintenance tasks\n        \"\"\"\n        # we used to clean up GitPython here\n        pass\n\n    @staticmethod\n    def _get_prefixed_commit_msg(msg):\n        DATALAD_PREFIX = \"[DATALAD]\"\n        return DATALAD_PREFIX if not msg else \"%s %s\" % (DATALAD_PREFIX, msg)\n\n    def configure_fake_dates(self):\n        \"\"\"Configure repository to use fake dates.\n        \"\"\"\n        lgr.debug(\"Enabling fake dates\")\n        self.config.set(\"datalad.fake-dates\", \"true\")\n\n    @property\n    def fake_dates_enabled(self):\n        \"\"\"Is the repository configured to use fake dates?\n        \"\"\"\n        # this turned into a private property of the CoreGitRepo\n        return self._fake_dates_enabled\n\n    def add_fake_dates(self, env):\n        # was renamed in CoreGitRepo\n        return self.add_fake_dates_to_env(env)\n\n    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,\n               files=None, date=None, index_file=None):\n        \"\"\"Commit changes to git.\n\n        Parameters\n        ----------\n        msg: str, optional\n          commit-message\n        options: list of str, optional\n          cmdline options for git-commit\n        _datalad_msg: bool, optional\n          To signal that commit is automated commit by datalad, so\n          it would carry the [DATALAD] prefix\n        careless: bool, optional\n          if False, raise when there's nothing actually committed;\n          if True, don't care\n        files: list of str, optional\n          path(s) to commit\n        date: str, optional\n          Date in one of the formats git understands\n        index_file: str, optional\n          An alternative index to use\n        \"\"\"\n\n        self.precommit()\n\n        # assemble commandline\n        cmd = ['commit']\n        options = ensure_list(options)\n\n        if date:\n            options += [\"--date\", date]\n\n        orig_msg = msg\n        if not msg:\n            if '--amend' in options:\n                if '--no-edit' not in options:\n                    # don't overwrite old commit message with our default\n                    # message by default, but re-use old one. In other words:\n                    # Make --no-edit the default:\n                    options += [\"--no-edit\"]\n            else:\n                msg = 'Recorded changes'\n                _datalad_msg = True\n\n        if _datalad_msg:\n            msg = self._get_prefixed_commit_msg(msg)\n\n        if msg:\n            options += [\"-m\", msg]\n        cmd.extend(options)\n\n        # set up env for commit\n        env = self.add_fake_dates(None) \\\n            if self.fake_dates_enabled else os.environ.copy()\n        if index_file:\n            env['GIT_INDEX_FILE'] = index_file\n\n        lgr.debug(\"Committing via direct call of git: %s\", cmd)\n\n        prev_sha = self.get_hexsha()\n\n        # Old code was doing clever --amend'ing of chunked series of commits manually\n        # here, but with pathspec_from_file it is no longer needed.\n        # store pre-commit state to be able to check if anything was committed\n        try:\n            # Note: call_git operates via joining call_git_items_ and that one wipes out\n            # .stdout from exception and collects/repopulates stderr only. Let's use\n            # _call_git which returns both outputs and collects/re-populates both stdout\n            # **and** stderr\n            _ = self._call_git(\n                    cmd,\n                    files=files,\n                    env=env,\n                    pathspec_from_file=True,\n            )\n        except CommandError as e:\n            # real errors first\n            if \"did not match any file(s) known to git\" in e.stderr:\n                raise FileNotInRepositoryError(\n                    cmd=e.cmd,\n                    msg=\"File(s) unknown to git\",\n                    code=e.code,\n                    filename=linesep.join([\n                        l for l in e.stderr.splitlines()\n                        if l.startswith(\"error: pathspec\")\n                    ])\n                )\n            # behavior choices now\n            elif not careless:\n                # not willing to compromise at all\n                raise\n            elif 'nothing to commit' in e.stdout:\n                lgr.debug(\"nothing to commit in %s. Ignored.\", self)\n            elif 'no changes added to commit' in e.stdout or \\\n                    'nothing added to commit' in e.stdout:\n                lgr.debug(\"no changes added to commit in %s. Ignored.\", self)\n            else:\n                raise\n        if orig_msg \\\n                or '--dry-run' in cmd \\\n                or prev_sha == self.get_hexsha() \\\n                or ('--amend' in cmd and '--no-edit' in cmd) \\\n                or (not is_interactive()) \\\n                or self.config.obtain('datalad.save.no-message') != 'interactive':\n            # we had a message given, or nothing was committed, or prev. commit\n            # was amended, or we are not connected to a terminal, or no\n            # interactive message input is desired:\n            # we can go home\n            return\n\n        # handle interactive message entry by running another `git-commit`\n        self._git_runner.run(\n            cmd + ['--amend', '--edit'],\n            protocol=NoCapture,\n            stdin=None,\n            env=env,\n        )\n\n    # TODO usage is only in the tests, consider making a test helper and\n    # remove from GitRepo API\n    def get_indexed_files(self):\n        \"\"\"Get a list of files in git's index\n\n        Returns\n        -------\n        list\n            list of paths rooting in git's base dir\n        \"\"\"\n\n        return [\n            str(r.relative_to(self.pathobj))\n            for r in self.get_content_info(\n                paths=None, ref=None, untracked='no')\n        ]\n\n    def format_commit(self, fmt, commitish=None):\n        \"\"\"Return `git show` output for `commitish`.\n\n        Parameters\n        ----------\n        fmt : str\n            A format string accepted by `git show`.\n        commitish: str, optional\n          Any commit identifier (defaults to \"HEAD\").\n\n        Returns\n        -------\n        str or, if there are not commits yet, None.\n        \"\"\"\n        # use git-log and not git-show due to faster performance with\n        # complex commits (e.g. octopus merges)\n        # https://github.com/datalad/datalad/issues/4801\n        cmd = ['log', '-1', '-z', '--format=' + fmt]\n        if commitish is not None:\n            cmd.append(commitish + \"^{commit}\")\n        # make sure Git takes our argument as a revision\n        cmd.append('--')\n        try:\n            stdout = self.call_git(\n                cmd, expect_stderr=True, expect_fail=True,\n                read_only=True)\n        except CommandError as e:\n            if 'bad revision' in e.stderr:\n                raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n            elif 'does not have any commits yet' in e.stderr:\n                return None\n            else:\n                raise e\n        # This trailing null is coming from the -z above, which avoids the\n        # newline that Git would append to the output. We could drop -z and\n        # strip the newline directly, but then we'd have to worry about\n        # compatibility across platforms.\n        return stdout.rsplit(\"\\0\", 1)[0]\n\n    def get_hexsha(self, commitish=None, short=False):\n        \"\"\"Return a hexsha for a given commitish.\n\n        Parameters\n        ----------\n        commitish : str, optional\n          Any identifier that refers to a commit (defaults to \"HEAD\").\n        short : bool, optional\n          Return the abbreviated form of the hexsha.\n\n        Returns\n        -------\n        str or, if no commitish was given and there are no commits yet, None.\n\n        Raises\n        ------\n        ValueError\n          If a commitish was given, but no corresponding commit could be\n          determined.\n        \"\"\"\n        # use --quiet because the 'Needed a single revision' error message\n        # that is the result of running this in a repo with no commits\n        # isn't useful to report\n        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(\n            commitish if commitish else 'HEAD')\n        ]\n        if short:\n            cmd.append('--short')\n        try:\n            return self.call_git_oneline(cmd, read_only=True)\n        except CommandError as e:\n            if commitish is None:\n                return None\n            raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n\n    @normalize_paths(match_return_type=False)\n    def get_last_commit_hexsha(self, files):\n        \"\"\"Return the hash of the last commit the modified any of the given\n        paths\"\"\"\n        try:\n            commit = self.call_git(\n                ['rev-list', '-n1', 'HEAD'],\n                files=files,\n                expect_fail=True,\n                read_only=True,\n            )\n            commit = commit.strip()\n            return commit if commit else None\n        except CommandError:\n            if self.get_hexsha() is None:\n                # unborn branch, don't freak out\n                return None\n            raise\n\n    def get_revisions(self, revrange=None, fmt=\"%H\", options=None):\n        \"\"\"Return list of revisions in `revrange`.\n\n        Parameters\n        ----------\n        revrange : str or list of str or None, optional\n            Revisions or revision ranges to walk. If None, revision defaults to\n            HEAD unless a revision-modifying option like `--all` or\n            `--branches` is included in `options`.\n        fmt : string, optional\n            Format accepted by `--format` option of `git log`. This should not\n            contain new lines because the output is split on new lines.\n        options : list of str, optional\n            Options to pass to `git log`.  This should not include `--format`.\n\n        Returns\n        -------\n        List of revisions (str), formatted according to `fmt`.\n        \"\"\"\n        if revrange is None:\n            revrange = []\n        elif isinstance(revrange, str):\n            revrange = [revrange]\n\n        cmd = [\"log\", \"--format={}\".format(fmt)]\n        cmd.extend((options or []) + revrange + [\"--\"])\n        try:\n            stdout = self.call_git(cmd, expect_fail=True, read_only=True)\n        except CommandError as e:\n            if \"does not have any commits\" in e.stderr:\n                return []\n            raise\n        return stdout.splitlines()\n\n    def commit_exists(self, commitish):\n        \"\"\"Does `commitish` exist in the repo?\n\n        Parameters\n        ----------\n        commitish : str\n            A commit or an object that can be dereferenced to one.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        # Note: The peeling operator \"^{commit}\" is required so that rev-parse\n        # doesn't succeed if passed a full hexsha that is valid but doesn't\n        # exist.\n        return self.call_git_success(\n            [\"rev-parse\", \"--verify\", commitish + \"^{commit}\"],\n            read_only=True,\n        )\n\n    def get_merge_base(self, commitishes):\n        \"\"\"Get a merge base hexsha\n\n        Parameters\n        ----------\n        commitishes: str or list of str\n          List of commitishes (branches, hexshas, etc) to determine the merge\n          base of. If a single value provided, returns merge_base with the\n          current branch.\n\n        Returns\n        -------\n        str or None\n          If no merge-base for given commits, or specified treeish doesn't\n          exist, None returned\n        \"\"\"\n        if isinstance(commitishes, str):\n            commitishes = [commitishes]\n        if not commitishes:\n            raise ValueError(\"Provide at least a single value\")\n        elif len(commitishes) == 1:\n            commitishes = commitishes + [self.get_active_branch()]\n\n        try:\n            base = self.call_git_oneline(['merge-base'] + commitishes,\n                                         read_only=True)\n        except CommandError as exc:\n            if exc.code == 1 and not (exc.stdout or exc.stderr):\n                # No merge base was found (unrelated commits).\n                return None\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                return None\n            raise\n\n        return base\n\n    def is_ancestor(self, reva, revb):\n        \"\"\"Is `reva` an ancestor of `revb`?\n\n        Parameters\n        ----------\n        reva, revb : str\n            Revisions.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        return self.call_git_success(\n            [\"merge-base\", \"--is-ancestor\", reva, revb],\n            read_only=True)\n\n    def get_commit_date(self, branch=None, date='authored'):\n        \"\"\"Get the date stamp of the last commit (in a branch or head otherwise)\n\n        Parameters\n        ----------\n        date: {'authored', 'committed'}\n          Which date to return.  \"authored\" will be the date shown by \"git show\"\n          and the one possibly specified via --date to `git commit`\n\n        Returns\n        -------\n        int or None\n          None if no commit\n        \"\"\"\n        if date == 'committed':\n            format = '%ct'\n        elif date == 'authored':\n            format = '%at'\n        else:\n            raise ValueError('unknow date type: {}'.format(date))\n        d = self.format_commit(format, commitish=branch)\n        return int(d) if d else None\n\n    def get_active_branch(self):\n        \"\"\"Get the name of the active branch\n\n        Returns\n        -------\n        str or None\n          Returns None if there is no active branch, i.e. detached HEAD,\n          and the branch name otherwise.\n        \"\"\"\n        try:\n            out = self.call_git([\"symbolic-ref\", \"HEAD\"], expect_fail=True,\n                                read_only=True)\n        except CommandError as e:\n            if 'HEAD is not a symbolic ref' in e.stderr:\n                lgr.debug(\"detached HEAD in %s\", self)\n                return None\n            else:\n                raise e\n        return out.strip()[11:]  # strip refs/heads/\n\n    def get_corresponding_branch(self, branch=None):\n        \"\"\"Always returns None, a plain GitRepo has no managed branches\"\"\"\n        return None\n\n    def get_branches(self):\n        \"\"\"Get all branches of the repo.\n\n        Returns\n        -------\n        [str]\n            Names of all branches of this repository.\n        \"\"\"\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')\n        ]\n\n    def get_remote_branches(self):\n        \"\"\"Get all branches of all remotes of the repo.\n\n        Returns\n        -----------\n        [str]\n            Names of all remote branches.\n        \"\"\"\n        # TODO: Reconsider melting with get_branches()\n\n        # TODO: treat entries like this: origin/HEAD -> origin/master'\n        # currently this is done in collection\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')\n        ]\n\n    def get_remotes(self, with_urls_only=False):\n        \"\"\"Get known remotes of the repository\n\n        Parameters\n        ----------\n        with_urls_only : bool, optional\n          return only remotes which have urls\n\n        Returns\n        -------\n        remotes : list of str\n          List of names of the remotes\n        \"\"\"\n        from datalad.utils import unique\n\n        self.config.reload()\n        remotes = unique([x[7:] for x in self.config.sections()\n                          if x.startswith(\"remote.\")])\n\n        if with_urls_only:\n            remotes = [\n                r for r in remotes\n                if self.config.get('remote.%s.url' % r)\n            ]\n        return remotes\n\n    # TODO this is practically unused outside the tests, consider turning\n    # into a test helper and trim from the API\n    def get_files(self, branch=None):\n        \"\"\"Get a list of files in git.\n\n        Lists the files in the (remote) branch.\n\n        Parameters\n        ----------\n        branch: str\n          Name of the branch to query. Default: active branch.\n\n        Returns\n        -------\n        [str]\n          list of files.\n        \"\"\"\n        return [\n            str(p.relative_to(self.pathobj))\n            for p in self.get_content_info(\n                paths=None, ref=branch, untracked='no')\n            ]\n\n    def add_remote(self, name, url, options=None):\n        \"\"\"Register remote pointing to a url\n        \"\"\"\n        cmd = ['remote', 'add']\n        if options:\n            cmd += options\n        cmd += [name, url]\n\n        # for historical reasons this method returns stdout and\n        # stderr, keeping that for now\n        result = self._call_git(cmd)\n        self.config.reload()\n        return result\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n\n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n                raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\n            else:\n                raise e\n\n        # config.reload necessary, because the associated remote config\n        # will vanish\n        self.config.reload()\n        return\n\n    def _maybe_open_ssh_connection(self, remote, prefer_push=True):\n        \"\"\"Open connection if `remote` has an SSH URL.\n\n        Doing so enables SSH caching, preventing datalad-sshrun subprocesses\n        from opening (and then closing) their own.\n\n        Parameters\n        ----------\n        remote : str\n        prefer_push : bool, optional\n            Use `remote.<remote>.pushurl` if there is one, falling back to\n            `remote.<remote>.url`.\n        \"\"\"\n        if remote:\n            url = None\n            if prefer_push:\n                url = self.get_remote_url(remote, push=True)\n            url = url or self.get_remote_url(remote)\n            if url and is_ssh(url):\n                ssh_manager.get_connection(url).open()\n\n    def update_remote(self, name=None, verbose=False):\n        \"\"\"\n        \"\"\"\n        options = [\"-v\"] if verbose else []\n        self._maybe_open_ssh_connection(name)\n        name = [name] if name else []\n        self.call_git(\n            ['remote'] + name + ['update'] + options,\n            expect_stderr=True\n        )\n\n    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,\n              **kwargs):\n        \"\"\"Fetches changes from a remote (or all remotes).\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to fetch from. If no remote is given and\n          `all_` is not set, the tracking branch is fetched.\n        refspec : str or list, optional\n          refspec(s) to fetch.\n        all_ : bool, optional\n          fetch all remotes (and all of their branches).\n          Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-fetch.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-fetch.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        return list(\n            self.fetch_(\n                remote=remote,\n                refspec=refspec,\n                all_=all_,\n                git_options=git_options,\n            )\n        )\n\n    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `fetch`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],\n            action='fetch',\n            urlvars=('remote.{}.url', 'remote.{}.url'),\n            protocol=GitProgress,\n            info_cls=FetchInfo,\n            info_from='stderr',\n            add_remote=False,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def push(self, remote=None, refspec=None, all_remotes=False,\n             all_=False, git_options=None, **kwargs):\n        \"\"\"Push changes to a remote (or all remotes).\n\n        If remote and refspec are specified, and remote has\n        `remote.{remote}.datalad-push-default-first` configuration variable\n        set (e.g. by `create-sibling-github`), we will first push the first\n        refspec separately to possibly ensure that the first refspec is chosen\n        by remote as the \"default branch\".\n        See https://github.com/datalad/datalad/issues/4997\n        Upon successful push if this variable was set in the local git config,\n        we unset it, so subsequent pushes would proceed normally.\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to push to. If no remote is given and\n          `all_` is not set, the tracking branch is pushed.\n        refspec : str or list, optional\n          refspec(s) to push.\n        all_ : bool, optional\n          push to all remotes. Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-push.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-push.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        if all_remotes:\n            # be nice to the elderly\n            all_ = True\n\n        push_refspecs = [refspec]\n        cfg = self.config  # shortcut\n        cfg_push_var = \"remote.{}.datalad-push-default-first\".format(remote)\n        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):\n            refspec = ensure_list(refspec)\n            lgr.debug(\"As indicated by %s pushing first refspec %s separately first\",\n                      cfg_push_var, refspec[0])\n            push_refspecs = [[refspec[0]], refspec[1:]]\n\n        push_res = []\n        for refspecs in push_refspecs:\n            push_res.extend(\n                self.push_(\n                    remote=remote,\n                    refspec=refspecs,\n                    all_=all_,\n                    git_options=git_options,\n                )\n            )\n        # note: above push_ should raise exception if errors out\n        if '--dry-run' not in git_options \\\n            and cfg.get_from_source('local', cfg_push_var) is not None:\n            lgr.debug(\"Removing %s variable from local git config after successful push\", cfg_push_var)\n            cfg.unset(cfg_push_var, 'local')\n        return push_res\n\n    def push_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `push`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],\n            action='push',\n            urlvars=('remote.{}.pushurl', 'remote.{}.url'),\n            protocol=StdOutCaptureWithGitProgress,\n            info_cls=PushInfo,\n            info_from='stdout',\n            add_remote=True,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def _fetch_push_helper(\n            self,\n            base_cmd,     # arg list\n            action,       # label fetch|push\n            urlvars,      # variables to query for URLs\n            protocol,     # processor for output\n            info_cls,     # Push|FetchInfo\n            info_from,    # stdout, stderr\n            add_remote,   # whether to add a 'remote' field to the info dict\n            remote=None, refspec=None, all_=False, git_options=None):\n\n        git_options = ensure_list(git_options)\n\n        cmd = base_cmd + git_options\n\n        if remote is None:\n            if refspec:\n                # conflicts with using tracking branch or push all remotes\n                # For now: Just fail.\n                # TODO: May be check whether it fits to tracking branch\n                raise ValueError(\n                    \"refspec specified without a remote. ({})\".format(refspec))\n            if all_:\n                remotes_to_process = self.get_remotes(with_urls_only=True)\n            else:\n                # No explicit remote to fetch.\n                # => get tracking branch:\n                tb_remote, refspec = self.get_tracking_branch()\n                if tb_remote is not None:\n                    remotes_to_process = [tb_remote]\n                else:\n                    # No remote, no tracking branch\n                    # => fail\n                    raise ValueError(\n                        \"Neither a remote is specified to {} \"\n                        \"from nor a tracking branch is set up.\".format(action))\n        else:\n            if all_:\n                raise ValueError(\n                    \"Option 'all_' conflicts with specified remote \"\n                    \"'{}'.\".format(remote))\n            remotes_to_process = [remote]\n\n        if refspec:\n            # prep for appending to cmd\n            refspec = ensure_list(refspec)\n\n        # no need for progress report, when there is just one remote\n        log_remote_progress = len(remotes_to_process) > 1\n        if log_remote_progress:\n            pbar_id = '{}remotes-{}'.format(action, id(self))\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start %sing remotes for %s', action, self,\n                total=len(remotes_to_process),\n                label=action.capitalize(),\n                unit=' Remotes',\n            )\n        try:\n            for remote in remotes_to_process:\n                r_cmd = cmd + [remote]\n                if refspec:\n                    r_cmd += refspec\n\n                if log_remote_progress:\n                    log_progress(\n                        lgr.info,\n                        pbar_id,\n                        '{}ing remote %s'.format(action.capitalize()),\n                        remote,\n                        update=1,\n                        increment=True,\n                    )\n                # best effort to enable SSH connection caching\n                url = self.config.get(\n                    # make two attempts to get a URL\n                    urlvars[0].format(remote),\n                    self.config.get(\n                        urlvars[1].format(remote),\n                        None)\n                )\n                if url and is_ssh(url):\n                    ssh_manager.get_connection(url).open()\n                try:\n                    out = self._git_runner.run(\n                        r_cmd,\n                        protocol=protocol,\n                    )\n                    output = out[info_from] or ''\n                except CommandError as e:\n                    output = None\n                    # intercept some errors that we express as an error report\n                    # in the info dicts\n                    if re.match(\n                            '.*^error: failed to (push|fetch) some refs',\n                            e.stderr,\n                            re.DOTALL | re.MULTILINE):\n                        output = getattr(e, info_from)\n                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()\n                                          if l.startswith('hint: ')])\n                        if output is None:\n                            output = ''\n                    if not output:\n                        raise\n\n                for line in output.splitlines():\n                    try:\n                        # push info doesn't identify a remote, add it here\n                        pi = info_cls._from_line(line)\n                        if add_remote:\n                            pi['remote'] = remote\n                        # There were errors, but Git provided hints\n                        if 'error' in pi['operations']:\n                            pi['hints'] = hints or None\n                        yield pi\n                    except Exception:\n                        # it is not progress and no push info\n                        # don't hide it completely\n                        lgr.debug('git-%s reported: %s', action, line)\n        finally:\n            if log_remote_progress:\n                log_progress(\n                    lgr.info,\n                    pbar_id,\n                    'Finished %sing remotes for %s', action, self,\n                )\n\n    def get_remote_url(self, name, push=False):\n        \"\"\"Get the url of a remote.\n\n        Reads the configuration of remote `name` and returns its url or None,\n        if there is no url configured.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        push: bool\n          if True, get the pushurl instead of the fetch url.\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        return self.config.get(var, None)\n\n    def set_remote_url(self, name, url, push=False):\n        \"\"\"Set the URL a remote is pointing to\n\n        Sets the URL of the remote `name`. Requires the remote to already exist.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        url: str\n        push: bool\n          if True, set the push URL, otherwise the fetch URL\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        self.config.set(var, url, scope='local', reload=True)\n\n    def get_branch_commits_(self, branch=None, limit=None, stop=None):\n        \"\"\"Return commit hexshas for a branch\n\n        Parameters\n        ----------\n        branch: str, optional\n          If not provided, assumes current branch\n        limit: None | 'left-only', optional\n          Limit which commits to report.  If None -- all commits (merged or not),\n          if 'left-only' -- only the commits from the left side of the tree upon\n          merges\n        stop: str, optional\n          hexsha of the commit at which stop reporting (matched one is not\n          reported either)\n\n        Yields\n        ------\n        str\n        \"\"\"\n        cmd = ['rev-list']\n        if limit == 'left-only':\n            cmd.append('--left-only')\n        if not branch:\n            branch = self.get_active_branch()\n        cmd.append(branch)\n        # and trailing -- marker to make sure that Git never confused the branch\n        # with a potentially existing directory of the same name\n        cmd.append('--')\n        for r in self.call_git_items_(cmd):\n            if stop and stop == r:\n                return\n            yield r\n\n    def checkout(self, name, options=None):\n        \"\"\"\n        \"\"\"\n        # TODO: May be check for the need of -b options herein?\n        cmd = ['checkout']\n        if options:\n            cmd += options\n        cmd += [str(name)]\n\n        self.call_git(cmd, expect_stderr=True)\n        # checkout can change committed config, or create branch config\n        self.config.reload()\n\n    # TODO: Before implementing annex merge, find usages and check for a needed\n    # change to call super().merge\n    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):\n        if options is None:\n            options = []\n        if msg:\n            options = options + [\"-m\", msg]\n        options += ['--allow-unrelated-histories']\n        self.call_git(\n            ['merge'] + options + [name],\n            **kwargs\n        )\n\n    def remove_branch(self, branch):\n        self.call_git(['branch', '-D', branch])\n\n    def cherry_pick(self, commit):\n        \"\"\"Cherry pick `commit` to the current branch.\n\n        Parameters\n        ----------\n        commit : str\n            A single commit.\n        \"\"\"\n        self.call_git([\"cherry-pick\", commit])\n\n    @property\n    def dirty(self):\n        \"\"\"Is the repository dirty?\n\n        Note: This provides a quick answer when you simply want to know if\n        there are any untracked changes or modifications in this repository or\n        its submodules. For finer-grained control and more detailed reporting,\n        use status() instead.\n        \"\"\"\n        stdout = self.call_git(\n            [\"status\", \"--porcelain\",\n             # Ensure the result isn't influenced by status.showUntrackedFiles.\n             \"--untracked-files=normal\",\n             # Ensure the result isn't influenced by diff.ignoreSubmodules.\n             \"--ignore-submodules=none\"])\n        if bool(stdout.strip()):\n            # The quick `git status`-based check can give a different answer\n            # than `datalad status` for submodules on an adjusted branch.\n            st = self.diffstatus(fr=\"HEAD\" if self.get_hexsha() else None,\n                                 to=None, untracked=\"normal\")\n            return any(r.get(\"state\") != \"clean\" for r in st.values())\n        return False\n\n    @property\n    def untracked_files(self):\n        \"\"\"Legacy interface, do not use! Use the status() method instead.\n\n        Despite its name, it also reports on untracked datasets, and\n        yields their names with trailing path separators.\n        \"\"\"\n        return [\n            '{}{}'.format(\n                str(p.relative_to(self.pathobj)),\n                os.sep if props['type'] != 'file' else ''\n            )\n            for p, props in self.status(\n                    untracked='all', eval_submodule_state='no').items()\n            if props.get('state', None) == 'untracked'\n        ]\n\n    def gc(self, allow_background=False, auto=False):\n        \"\"\"Perform house keeping (garbage collection, repacking)\"\"\"\n        cmd_options = []\n        if not allow_background:\n            cmd_options += ['-c', 'gc.autodetach=0']\n        cmd_options += ['gc', '--aggressive']\n        if auto:\n            cmd_options += ['--auto']\n        self.call_git(cmd_options)\n\n    def _parse_gitmodules(self):\n        # TODO read .gitconfig from Git blob?\n        gitmodules = self.pathobj / '.gitmodules'\n        if not gitmodules.exists():\n            return {}\n        # pull out file content\n        out = self.call_git(\n            ['config', '-z', '-l', '--file', '.gitmodules'],\n            read_only=True)\n        # abuse our config parser\n        # disable multi-value report, because we could not deal with them\n        # anyways, and they should not appear in a normal .gitmodules file\n        # but could easily appear when duplicates are included. In this case,\n        # we better not crash\n        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)\n        mods = {}\n        for k, v in db.items():\n            if not k.startswith('submodule.'):\n                # we don't know what this is\n                lgr.warning(\"Skip unrecognized .gitmodule specification: %s=%s\", k, v)\n                continue\n            k_l = k.split('.')\n            # module name is everything after 'submodule.' that is not the variable\n            # name\n            mod_name = '.'.join(k_l[1:-1])\n            mod = mods.get(mod_name, {})\n            # variable name is the last 'dot-free' segment in the key\n            mod[k_l[-1]] = v\n            mods[mod_name] = mod\n\n        out = {}\n        # bring into traditional shape\n        for name, props in mods.items():\n            if 'path' not in props:\n                lgr.warning(\"Failed to get '%s.path', skipping this submodule\", name)\n                continue\n            modprops = {'gitmodule_{}'.format(k): v\n                        for k, v in props.items()\n                        if not k.startswith('__')}\n            # Keep as PurePosixPath for possible normalization of / in the path etc\n            modpath = PurePosixPath(props['path'])\n            modprops['gitmodule_name'] = name\n            out[modpath] = modprops\n        return out\n\n    def get_submodules_(self, paths=None):\n        \"\"\"Yield submodules in this repository.\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`. Paths must be relative\n            to the resolved repository root, and must be normed to match the\n            reporting done by Git, i.e. no parent dir components\n            (ala \"some/../this\").\n\n        Returns\n        -------\n        A generator that yields a dictionary with information for each\n        submodule.\n        \"\"\"\n        if not (self.pathobj / \".gitmodules\").exists():\n            return\n\n        modinfo = self._parse_gitmodules()\n        if not modinfo:\n            # we exit early, if there is nothing on record (even though\n            # a .gitmodules file exists).\n            # without this conditional exit, we would be able to discover\n            # subprojects even when they are not recorded in .gitmodules,\n            # but at the cost of running an unconstrained ls-files call\n            # below\n            return\n\n        posix_mod_paths = [m.as_posix() for m in modinfo]\n        if paths:\n            # harmonize them into relative to the repository\n            posix_paths = []\n            for path in paths:\n                path = ut.PurePath(path)\n                if path.is_absolute():\n                    try:\n                        path = path.relative_to(self.pathobj)\n                    except ValueError as exc:\n                        lgr.debug(\n                            \"Path %s it not underneath %s, skipping since nothing should match it: %s\",\n                            path, self.pathobj, CapturedException(exc)\n                        )\n                        continue\n                posix_paths.append(path.as_posix())\n\n            # constrain the report by the given paths, make sure all paths are POSIX\n            posix_mod_paths = list(get_filtered_paths_(\n                posix_mod_paths,\n                posix_paths,\n                include_within_path=True,\n            ))\n\n        for r in self.call_git_items_(\n            ['ls-files', '--stage', '-z'],\n            sep='\\0',\n            files=posix_mod_paths,\n            read_only=True,\n            keep_ends=True,\n        ):\n            if not r.startswith('160000'):\n                # make sure this method never talks about non-dataset\n                # content\n                continue\n            props, rpath = r.split('\\t')\n            mode, gitsha, stage = props.split(' ')\n            if stage not in ('0', '2'):\n                # we either have non-merge situation, or a simple merge\n                # situation (i.e. stage=0). the reported gitsha always\n                # matches what we have locally.\n                # or we are in a three-way merge, in which case stage=2\n                # is what we want to report, because it matches the\n                # current HEAD (see git-read-tree manpage).\n                # there is either a stage 2 or stage 0, never both\n                continue\n            # remove the expected line separator from the path\n            rpath = rpath[:-1]\n            path = PurePosixPath(rpath)\n            yield dict(\n                path=self.pathobj / rpath, # full path returned here\n                type='dataset',\n                gitshasum=gitsha,\n                **modinfo.get(path, {})\n            )\n\n    def get_submodules(self, sorted_=True, paths=None):\n        \"\"\"Return list of submodules.\n\n        Parameters\n        ----------\n        sorted_ : bool, optional\n            Sort submodules by path name.\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`.\n\n        Returns\n        -------\n        List of submodule namedtuples if `compat` is true or otherwise a list\n        of dictionaries as returned by `get_submodules_`.\n        \"\"\"\n        xs = self.get_submodules_(paths=paths)\n\n        if sorted_:\n            xs = sorted(xs, key=lambda x: x[\"path\"])\n        return list(xs)\n\n    def update_ref(self, ref, value, oldvalue=None, symbolic=False):\n        \"\"\"Update the object name stored in a ref \"safely\".\n\n        Just a shim for `git update-ref` call if not symbolic, and\n        `git symbolic-ref` if symbolic\n\n        Parameters\n        ----------\n        ref : str\n          Reference, such as `ref/heads/BRANCHNAME` or HEAD.\n        value : str\n          Value to update to, e.g. hexsha of a commit when updating for a\n          branch ref, or branch ref if updating HEAD\n        oldvalue: str\n          Value to update from. Safeguard to be verified by git. This is only\n          valid if `symbolic` is not True.\n        symbolic : None\n          To instruct if ref is symbolic, e.g. should be used in case of\n          ref=HEAD\n        \"\"\"\n        if symbolic:\n            if oldvalue:\n                raise ValueError(\"oldvalue and symbolic must not be given both\")\n            cmd = ['symbolic-ref', ref, value]\n        else:\n            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])\n        self.call_git(cmd)\n\n    def tag(self, tag, message=None, commit=None, options=None):\n        \"\"\"Tag a commit\n\n        Parameters\n        ----------\n        tag : str\n          Custom tag label. Must be a valid tag name.\n        message : str, optional\n          If provided, adds ['-m', <message>] to the list of `git tag`\n          arguments.\n        commit : str, optional\n          If provided, will be appended as last argument to the `git tag` call,\n          and can be used to identify the commit that shall be tagged, if\n          not HEAD.\n        options : list, optional\n          Additional command options, inserted prior a potential `commit`\n          argument.\n        \"\"\"\n        # TODO: call in save.py complains about extensive logging. When does it\n        # happen in what way? Figure out, whether to just silence it or raise or\n        # whatever else.\n        args = ['tag']\n        if message:\n            args += ['-m', message]\n        if options is not None:\n            args.extend(options)\n        args.append(tag)\n        if commit:\n            args.append(commit)\n        self.call_git(args)\n\n    def get_tags(self, output=None):\n        \"\"\"Get list of tags\n\n        Parameters\n        ----------\n        output : str, optional\n          If given, limit the return value to a list of values matching that\n          particular key of the tag properties.\n\n        Returns\n        -------\n        list\n          Each item is a dictionary with information on a tag. At present\n          this includes 'hexsha', and 'name', where the latter is the string\n          label of the tag, and the former the hexsha of the object the tag\n          is attached to. The list is sorted by the creator date (committer\n          date for lightweight tags and tagger date for annotated tags), with\n          the most recent commit being the last element.\n        \"\"\"\n        tags = [\n            dict(\n                name=t['refname:strip=2'],\n                hexsha=t['object'] if t['object'] else t['objectname'],\n            )\n            for t in self.for_each_ref_(\n                fields=['refname:strip=2', 'objectname', 'object'],\n                pattern='refs/tags',\n                sort='creatordate')\n        ]\n        if output:\n            return [t[output] for t in tags]\n        else:\n            return tags\n\n    def describe(self, commitish=None, **kwargs):\n        \"\"\" Quick and dirty implementation to call git-describe\n\n        Parameters\n        ----------\n        kwargs:\n            transformed to cmdline options for git-describe;\n            see __init__ for description of the transformation\n        \"\"\"\n        # TODO: be more precise what failure to expect when and raise actual\n        # errors\n        cmd = ['describe'] + to_options(**kwargs)\n        if commitish is not None:\n            cmd.append(commitish)\n        try:\n            describe = self.call_git(cmd, expect_fail=True)\n            return describe.strip()\n        # TODO: WTF \"catch everything\"?\n        except:\n            return None\n\n    def get_tracking_branch(self, branch=None, remote_only=False):\n        \"\"\"Get the tracking branch for `branch` if there is any.\n\n        Parameters\n        ----------\n        branch: str\n            local branch to look up. If none is given, active branch is used.\n        remote_only : bool\n            Don't return a value if the upstream remote is set to \".\" (meaning\n            this repository).\n\n        Returns\n        -------\n        tuple\n            (remote or None, refspec or None) of the tracking branch\n        \"\"\"\n        if branch is None:\n            branch = self.get_corresponding_branch() or self.get_active_branch()\n            if branch is None:\n                return None, None\n\n        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)\n        if remote_only and track_remote == \".\":\n            return None, None\n        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)\n        return track_remote, track_branch\n\n    @property\n    def count_objects(self):\n        \"\"\"return dictionary with count, size(in KiB) information of git objects\n        \"\"\"\n\n        count_cmd = ['count-objects', '-v']\n        count_str = self.call_git(count_cmd)\n        count = {key: int(value)\n                 for key, value in [item.split(': ')\n                                    for item in count_str.split('\\n')\n                                    if len(item.split(': ')) == 2]}\n        return count\n\n    def get_git_attributes(self):\n        \"\"\"Query gitattributes which apply to top level directory\n\n        It is a thin compatibility/shortcut wrapper around more versatile\n        get_gitattributes which operates on a list of paths and returns\n        a dictionary per each path\n\n        Returns\n        -------\n        dict:\n          a dictionary with attribute name and value items relevant for the\n          top ('.') directory of the repository, and thus most likely the\n          default ones (if not overwritten with more rules) for all files within\n          repo.\n        \"\"\"\n        return self.get_gitattributes('.')['.']\n\n    def get_gitattributes(self, path, index_only=False):\n        \"\"\"Query gitattributes for one or more paths\n\n        Parameters\n        ----------\n        path: path or list\n          Path(s) to query. Paths may be relative or absolute.\n        index_only: bool\n          Flag whether to consider only gitattribute setting that are reflected\n          in the repository index, not just in the work tree content.\n\n        Returns\n        -------\n        dict:\n          Each key is a queried path (always relative to the repository root),\n          each value is a dictionary with attribute\n          name and value items. Attribute values are either True or False,\n          for set and unset attributes, or are the literal attribute value.\n        \"\"\"\n        path = ensure_list(path)\n        cmd = [\"check-attr\", \"-z\", \"--all\"]\n        if index_only:\n            cmd.append('--cached')\n        # make sure we have one entry for each query path to\n        # simplify work with the result\n        attributes = {p: {} for p in path}\n        attr = []\n        for item in self.call_git_items_(cmd, files=path, sep='\\0',\n                                         read_only=True):\n            attr.append(item)\n            if len(attr) < 3:\n                continue\n            # we have a full record\n            p, name, value = attr\n            attrs = attributes[p]\n            attrs[name] = \\\n                True if value == 'set' else False if value == 'unset' else value\n            # done, reset item\n            attr = []\n        return {relpath(k, self.path) if isabs(k) else k: v\n                for k, v in attributes.items()}\n\n    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):\n        \"\"\"Set gitattributes\n\n        By default appends additional lines to `attrfile`. Note, that later\n        lines in `attrfile` overrule earlier ones, which may or may not be\n        what you want. Set `mode` to 'w' to replace the entire file by\n        what you provided in `attrs`.\n\n        Parameters\n        ----------\n        attrs : list\n          Each item is a 2-tuple, where the first element is a path pattern,\n          and the second element is a dictionary with attribute key/value\n          pairs. The attribute dictionary must use the same semantics as those\n          returned by `get_gitattributes()`. Path patterns can use absolute paths,\n          in which case they will be normalized relative to the directory\n          that contains the target .gitattributes file (see `attrfile`).\n        attrfile: path\n          Path relative to the repository root of the .gitattributes file the\n          attributes shall be set in.\n        mode: str\n          'a' to append .gitattributes, 'w' to replace it\n        \"\"\"\n\n        git_attributes_file = op.join(self.path, attrfile)\n        attrdir = op.dirname(git_attributes_file)\n        if not op.exists(attrdir):\n            os.makedirs(attrdir)\n\n        with open(git_attributes_file, mode + '+') as f:\n            # for append, fix existing files that do not end with \\n\n            if mode == 'a' and f.tell():\n                f.seek(max(0, f.tell() - len(os.linesep)))\n                if not f.read().endswith('\\n'):\n                    f.write('\\n')\n\n            for pattern, attr in sorted(attrs, key=lambda x: x[0]):\n                # normalize the pattern relative to the target .gitattributes file\n                npath = _normalize_path(\n                    op.join(self.path, op.dirname(attrfile)), pattern)\n                # paths in gitattributes always have to be POSIX\n                npath = Path(npath).as_posix()\n                attrline = u''\n                if npath.count(' '):\n                    # quote patterns with spaces\n                    attrline += u'\"{}\"'.format(npath.replace('\"', '\\\\\"'))\n                else:\n                    attrline += npath\n                for a in sorted(attr):\n                    val = attr[a]\n                    if val is True:\n                        attrline += ' {}'.format(a)\n                    elif val is False:\n                        attrline += ' -{}'.format(a)\n                    else:\n                        attrline += ' {}={}'.format(a, val)\n                f.write('{}\\n'.format(attrline))\n\n    def get_content_info(self, paths=None, ref=None, untracked='all'):\n        \"\"\"Get identifier and type information from repository content.\n\n        This is simplified front-end for `git ls-files/tree`.\n\n        Both commands differ in their behavior when queried about subdataset\n        paths. ls-files will not report anything, ls-tree will report on the\n        subdataset record. This function uniformly follows the behavior of\n        ls-tree (report on the respective subdataset mount).\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath) or None\n          Specific paths, relative to the resolved repository root, to query\n          info for. Paths must be normed to match the reporting done by Git,\n          i.e. no parent dir components (ala \"some/../this\").\n          If `None`, info is reported for all content.\n        ref : gitref or None\n          If given, content information is retrieved for this Git reference\n          (via ls-tree), otherwise content information is produced for the\n          present work tree (via ls-files). With a given reference, the\n          reported content properties also contain a 'bytesize' record,\n          stating the size of a file in bytes.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when no `ref` was given:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n\n          `gitshasum`\n            SHASUM of the item as tracked by Git, or None, if not\n            tracked. This could be different from the SHASUM of the file\n            in the worktree, if it was modified.\n\n        Raises\n        ------\n        ValueError\n          In case of an invalid Git reference (e.g. 'HEAD' in an empty\n          repository)\n        \"\"\"\n        lgr.debug('%s.get_content_info(...)', self)\n        # TODO limit by file type to replace code in subdatasets command\n        info = dict()\n\n        if paths:  # is not None separate after\n            # path matching will happen against what Git reports\n            # and Git always reports POSIX paths\n            # any incoming path has to be relative already, so we can simply\n            # convert unconditionally\n            # note: will be list-ified below\n            posix_paths = [ut.PurePath(p).as_posix() for p in paths]\n        elif paths is not None:\n            return info\n        else:\n            posix_paths = None\n\n        if posix_paths and (not ref or external_versions[\"cmd:git\"] >= \"2.29.0\"):\n            # If a path points within a submodule, we need to map it to the\n            # containing submodule before feeding it to ls-files or ls-tree.\n            #\n            # Before Git 2.29.0, ls-tree and ls-files differed in how they\n            # reported paths within submodules: ls-files provided no output,\n            # and ls-tree listed the submodule. Now they both return no output.\n            submodules = [s[\"path\"].relative_to(self.pathobj).as_posix()\n                          for s in self.get_submodules_()]\n            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well\n            posix_paths = get_parent_paths(posix_paths, submodules)\n\n        # this will not work in direct mode, but everything else should be\n        # just fine\n        if not ref:\n            # make sure no operations are pending before we figure things\n            # out in the worktree\n            self.precommit()\n\n            # --exclude-standard will make sure to honor and standard way\n            # git can be instructed to ignore content, and will prevent\n            # crap from contaminating untracked file reports\n            cmd = ['ls-files', '--stage', '-z']\n            # untracked report mode, using labels from `git diff` option style\n            if untracked == 'all':\n                cmd += ['--exclude-standard', '-o']\n            elif untracked == 'normal':\n                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']\n            elif untracked == 'no':\n                pass\n            else:\n                raise ValueError(\n                    'unknown value for `untracked`: {}'.format(untracked))\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\\t(?P<fname>.*)$')\n        else:\n            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\\s]*(?P<size>[0-9-]+)\\t(?P<fname>.*)$')\n\n        lgr.debug('Query repo: %s', cmd)\n        try:\n            stdout = self.call_git(\n                cmd,\n                files=posix_paths,\n                expect_fail=True,\n                read_only=True)\n        except CommandError as exc:\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                raise InvalidGitReferenceError(ref)\n            raise\n        lgr.debug('Done query repo: %s', cmd)\n\n        self._get_content_info_line_helper(\n            ref,\n            info,\n            stdout.split('\\0'),\n            props_re)\n\n        lgr.debug('Done %s.get_content_info(...)', self)\n        return info\n\n    def _get_content_info_line_helper(self, ref, info, lines, props_re):\n        \"\"\"Internal helper of get_content_info() to parse Git output\"\"\"\n        mode_type_map = {\n            '100644': 'file',\n            '100755': 'file',\n            '120000': 'symlink',\n            '160000': 'dataset',\n        }\n        for line in lines:\n            if not line:\n                continue\n            inf = {}\n            props = props_re.match(line)\n            if not props:\n                # Kludge: Filter out paths starting with .git/ to work around\n                # an `ls-files -o` bug that was fixed in Git 2.25.\n                #\n                # TODO: Drop this condition when GIT_MIN_VERSION is at least\n                # 2.25.\n                if line.startswith(\".git/\"):\n                    lgr.debug(\"Filtering out .git/ file: %s\", line)\n                    continue\n                # not known to Git, but Git always reports POSIX\n                path = ut.PurePosixPath(line)\n                inf['gitshasum'] = None\n            else:\n                # again Git reports always in POSIX\n                path = ut.PurePosixPath(props.group('fname'))\n\n            # revisit the file props after this path has not been rejected\n            if props:\n                inf['gitshasum'] = props.group('sha')\n                inf['type'] = mode_type_map.get(\n                    props.group('type'), props.group('type'))\n\n                if ref and inf['type'] == 'file':\n                    inf['bytesize'] = int(props.group('size'))\n\n            # join item path with repo path to get a universally useful\n            # path representation with auto-conversion and tons of other\n            # stuff\n            path = self.pathobj.joinpath(path)\n            if 'type' not in inf:\n                # be nice and assign types for untracked content\n                inf['type'] = 'symlink' if path.is_symlink() \\\n                    else 'directory' if path.is_dir() else 'file'\n            info[path] = inf\n\n    def status(self, paths=None, untracked='all', eval_submodule_state='full'):\n        \"\"\"Simplified `git status` equivalent.\n\n        Parameters\n        ----------\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list. If a query path points\n          into a subdataset, a report is made on the subdataset record\n          within the queried dataset only (no recursion).\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        lgr.debug('Query status of %r for %s paths',\n                  self, len(paths) if paths is not None else 'all')\n        return self.diffstatus(\n            fr='HEAD' if self.get_hexsha() else None,\n            to=None,\n            paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state)\n\n    def diff(self, fr, to, paths=None, untracked='all',\n             eval_submodule_state='full'):\n        \"\"\"Like status(), but reports changes between to arbitrary revisions\n\n        Parameters\n        ----------\n        fr : str or None\n          Revision specification (anything that Git understands). Passing\n          `None` considers anything in the target state as new.\n        to : str or None\n          Revision specification (anything that Git understands), or None\n          to compare to the state of the work tree.\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when `to` is None:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        return {k: v for k, v in self.diffstatus(\n            fr=fr, to=to, paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state).items()\n            if v.get('state', None) != 'clean'}\n\n    def diffstatus(self, fr, to, paths=None, untracked='all',\n                   eval_submodule_state='full', _cache=None):\n        \"\"\"Like diff(), but reports the status of 'clean' content too.\n\n        It supports an additional submodule evaluation state 'global'.\n        If given, it will return a single 'modified'\n        (vs. 'clean') state label for the entire repository, as soon as\n        it can.\n        \"\"\"\n        def _get_cache_key(label, paths, ref, untracked=None):\n            return self.path, label, tuple(paths) if paths else None, \\\n                ref, untracked\n\n        if _cache is None:\n            _cache = {}\n\n        if paths is not None:\n            # at this point we must normalize paths to the form that\n            # Git would report them, to easy matching later on\n            paths = map(ut.Path, paths)\n            paths = [\n                p.relative_to(self.pathobj) if p.is_absolute() else p\n                for p in paths\n            ]\n\n        # TODO report more info from get_content_info() calls in return\n        # value, those are cheap and possibly useful to a consumer\n        # we need (at most) three calls to git\n        if to is None:\n            # everything we know about the worktree, including os.stat\n            # for each file\n            key = _get_cache_key('ci', paths, None, untracked)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(\n                    paths=paths, ref=None, untracked=untracked)\n                _cache[key] = to_state\n            # we want Git to tell us what it considers modified and avoid\n            # reimplementing logic ourselves\n            key = _get_cache_key('mod', paths, None)\n            if key in _cache:\n                modified = _cache[key]\n            else:\n                # from Git 2.31.0 onwards ls-files has --deduplicate\n                # by for backward compatibility keep doing deduplication here\n                modified = set(\n                    self.pathobj.joinpath(ut.PurePosixPath(p))\n                    for p in self.call_git_items_(\n                        # we must also look for deleted files, for the logic\n                        # below to work. Only from Git 2.31.0 would they be\n                        # included with `-m` alone\n                        ['ls-files', '-z', '-m', '-d'],\n                        # low-level code cannot handle pathobjs\n                        files=[str(p) for p in paths] if paths is not None else None,\n                        sep='\\0',\n                        read_only=True)\n                    if p)\n                _cache[key] = modified\n        else:\n            key = _get_cache_key('ci', paths, to)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(paths=paths, ref=to)\n                _cache[key] = to_state\n            # we do not need worktree modification detection in this case\n            modified = None\n        # origin state\n        key = _get_cache_key('ci', paths, fr)\n        if key in _cache:\n            from_state = _cache[key]\n        else:\n            if fr:\n                from_state = self.get_content_info(paths=paths, ref=fr)\n            else:\n                # no ref means from nothing\n                from_state = {}\n            _cache[key] = from_state\n\n        status = dict()\n        for f, to_state_r in to_state.items():\n            props = self._diffstatus_get_state_props(\n                f,\n                from_state.get(f, None),\n                to_state_r,\n                # are we comparing against a recorded commit or the worktree\n                to is not None,\n                # if we have worktree modification info, report if\n                # path is reported as modified in it\n                modified and f in modified,\n                eval_submodule_state)\n            # potential early exit in \"global\" eval mode\n            if eval_submodule_state == 'global' and \\\n                    props.get('state', None) not in ('clean', None):\n                # any modification means globally 'modified'\n                return 'modified'\n            status[f] = props\n\n        for f, from_state_r in from_state.items():\n            if f not in to_state:\n                # we new this, but now it is gone and Git is not complaining\n                # about it being missing -> properly deleted and deletion\n                # stages\n                status[f] = dict(\n                    state='deleted',\n                    type=from_state_r['type'],\n                    # report the shasum to distinguish from a plainly vanished\n                    # file\n                    gitshasum=from_state_r['gitshasum'],\n                )\n                if eval_submodule_state == 'global':\n                    return 'modified'\n\n        if to is not None or eval_submodule_state == 'no':\n            # if we have `to` we are specifically comparing against\n            # a recorded state, and this function only attempts\n            # to label the state of a subdataset, not investigate\n            # specifically what the changes in subdatasets are\n            # this is done by a high-level command like rev-diff\n            # so the comparison within this repo and the present\n            # `state` label are all we need, and they are done already\n            if eval_submodule_state == 'global':\n                return 'clean'\n            else:\n                return status\n\n        # loop over all subdatasets and look for additional modifications\n        for f, st in status.items():\n            f = str(f)\n            if 'state' in st or not st['type'] == 'dataset':\n                # no business here\n                continue\n            if not GitRepo.is_valid_repo(f):\n                # submodule is not present, no chance for a conflict\n                st['state'] = 'clean'\n                continue\n            # we have to recurse into the dataset and get its status\n            subrepo = repo_from_path(f)\n            # get the HEAD commit, or the one of the corresponding branch\n            # only that one counts re super-sub relationship\n            # save() syncs the corresponding branch each time\n            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())\n            st['gitshasum'] = subrepo_commit\n            # subdataset records must be labeled clean up to this point\n            # test if current commit in subdataset deviates from what is\n            # recorded in the dataset\n            st['state'] = 'modified' \\\n                if st['prev_gitshasum'] != subrepo_commit \\\n                else 'clean'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n            if eval_submodule_state == 'commit':\n                continue\n            # the recorded commit did not change, so we need to make\n            # a more expensive traversal\n            st['state'] = subrepo.diffstatus(\n                # we can use 'HEAD' because we know that the commit\n                # did not change. using 'HEAD' will facilitate\n                # caching the result\n                fr='HEAD',\n                to=None,\n                paths=None,\n                untracked=untracked,\n                eval_submodule_state='global',\n                _cache=_cache) if st['state'] == 'clean' else 'modified'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n\n        if eval_submodule_state == 'global':\n            return 'clean'\n        else:\n            return status\n\n    def _diffstatus_get_state_props(self, f, from_state, to_state,\n                                    against_commit,\n                                    modified_in_worktree,\n                                    eval_submodule_state):\n        \"\"\"Helper to determine diff properties for a single path\n\n        Parameters\n        ----------\n        f : Path\n        from_state : dict\n        to_state : dict\n        against_commit : bool\n          Flag whether `to_state` reflects a commit or the worktree.\n        modified_in_worktree : bool\n          Flag whether a worktree modification is reported. This is ignored\n          when `against_commit` is True.\n        eval_submodule_state : {'commit', 'no', ...}\n        \"\"\"\n        if against_commit:\n            # we can ignore any worktree modification reported when\n            # comparing against a commit\n            modified_in_worktree = False\n\n        props = {}\n        if 'type' in to_state:\n            props['type'] = to_state['type']\n\n        to_sha = to_state['gitshasum']\n        from_sha = from_state['gitshasum'] if from_state else None\n\n        # determine the state of `f` from from_state and to_state records, if\n        # it can be determined conclusively from it. If not, it will\n        # stay None for now\n        state = None\n        if not from_state:\n            # this is new, or rather not known to the previous state\n            state = 'added' if to_sha else 'untracked'\n        elif to_sha == from_sha and not modified_in_worktree:\n            # something that is seemingly unmodified, based on the info\n            # gathered so far\n            if to_state['type'] == 'dataset':\n                if against_commit or eval_submodule_state == 'commit':\n                    # we compare against a recorded state, just based on\n                    # the shas we can be confident, otherwise the state\n                    # of a subdataset isn't fully known yet, because\n                    # `modified_in_worktree` will only reflect changes\n                    # in the commit of a subdataset without looking into\n                    # it for uncommitted changes. Such tests are done\n                    # later and based on further conditionals for\n                    # performance reasons\n                    state = 'clean'\n            else:\n                # no change in git record, and no change on disk\n                # at this point we know that the reported object ids\n                # for this file are identical in the to and from\n                # records.  If to is None, we're comparing to the\n                # working tree and a deleted file will still have an\n                # identical id, so we need to check whether the file is\n                # gone before declaring it clean. This working tree\n                # check is irrelevant and wrong if to is a ref.\n                state = 'clean' \\\n                    if against_commit or (f.exists() or f.is_symlink()) \\\n                    else 'deleted'\n        else:\n            # change in git record, or on disk\n            # for subdatasets leave the 'modified' judgement to the caller\n            # for supporting corner cases, such as adjusted branch\n            # which require inspection of a subdataset\n            # TODO we could have a new file that is already staged\n            # but had subsequent modifications done to it that are\n            # unstaged. Such file would presently show up as 'added'\n            # ATM I think this is OK, but worth stating...\n            state = ('modified'\n                     if against_commit or to_state['type'] != 'dataset'\n                     else None\n                    ) if f.exists() or f.is_symlink() else 'deleted'\n            # TODO record before and after state for diff-like use\n            # cases\n\n        if state in ('clean', 'added', 'modified', None):\n            # assign present gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['gitshasum'] = to_sha\n            if 'bytesize' in to_state:\n                # if we got this cheap, report it\n                props['bytesize'] = to_state['bytesize']\n            elif state == 'clean' and 'bytesize' in from_state:\n                # no change, we can take this old size info\n                props['bytesize'] = from_state['bytesize']\n        if state in ('clean', 'modified', 'deleted', None):\n            # assign previous gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['prev_gitshasum'] = from_sha\n        if state:\n            # only report a state if we could determine any\n            # outside code tests for existence of the property\n            # and not (always) for the value\n            props['state'] = state\n        return props\n\n    def _save_pre(self, paths, _status, **kwargs):\n        # helper to get an actionable status report\n        if paths is not None and not paths and not _status:\n            return\n        if _status is None:\n            if 'untracked' not in kwargs:\n                kwargs['untracked'] = 'normal'\n            status = self.status(\n                paths=paths,\n                **{k: kwargs[k] for k in kwargs\n                   if k in ('untracked', 'eval_submodule_state')})\n        else:\n            # we want to be able to add items down the line\n            # make sure to detach from prev. owner\n            status = _status.copy()\n        return status\n\n    def get_staged_paths(self):\n        \"\"\"Returns a list of any stage repository path(s)\n\n        This is a rather fast call, as it will not depend on what is going on\n        in the worktree.\n        \"\"\"\n        try:\n            return list(self.call_git_items_(\n                ['diff', '--name-only', '--staged'],\n                expect_stderr=True))\n        except CommandError as e:\n            lgr.debug(CapturedException(e))\n            return []\n\n    def _save_post(self, message, files, partial_commit, amend=False,\n                   allow_empty=False):\n        # helper to commit changes reported in status\n\n        # TODO remove pathobj stringification when commit() can\n        # handle it\n        to_commit = [str(f.relative_to(self.pathobj))\n                     for f in files] \\\n                    if partial_commit else None\n        if not partial_commit or to_commit or allow_empty or \\\n                (amend and message):\n            # we directly call GitRepo.commit() to avoid a whole slew\n            # if direct-mode safeguards and workarounds in the AnnexRepo\n            # implementation (which also run an additional dry-run commit\n            GitRepo.commit(\n                self,\n                files=to_commit,\n                msg=message,\n                options=to_options(amend=amend, allow_empty=allow_empty),\n                # do not raise on empty commit\n                # it could be that the `add` in this save-cycle has already\n                # brought back a 'modified' file into a clean state\n                careless=True,\n            )\n\n    def save(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Save dataset content.\n\n        Parameters\n        ----------\n        message : str or None\n          A message to accompany the changeset in the log. If None,\n          a default message is used.\n        paths : list or None\n          Any content with path matching any of the paths given in this\n          list will be saved. Matching will be performed against the\n          dataset status (GitRepo.status()), or a custom status provided\n          via `_status`. If no paths are provided, ALL non-clean paths\n          present in the repo status or `_status` will be saved.\n        _status : dict or None\n          If None, Repo.status() will be queried for the given `ds`. If\n          a dict is given, its content will be used as a constraint.\n          For example, to save only modified content, but no untracked\n          content, set `paths` to None and provide a `_status` that has\n          no entries for untracked content.\n        **kwargs :\n          Additional arguments that are passed to underlying Repo methods.\n          Supported:\n\n          - git : bool (passed to Repo.add()\n          - eval_submodule_state : {'full', 'commit', 'no'}\n            passed to Repo.status()\n          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()\n          - amend : bool (passed to GitRepo.commit)\n        \"\"\"\n        return list(\n            self.save_(\n                message=message,\n                paths=paths,\n                _status=_status,\n                **kwargs\n            )\n        )\n\n    def save_(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Like `save()` but working as a generator.\"\"\"\n        from datalad.interface.results import get_status_dict\n\n        status_state = _get_save_status_state(\n            self._save_pre(paths, _status, **kwargs) or {}\n        )\n        amend = kwargs.get('amend', False)\n\n        # TODO: check on those None's -- may be those are also \"nothing to worry about\"\n        # and we could just return?\n        if not any(status_state.values()) and not (message and amend):\n            # all clean, nothing todo\n            lgr.debug('Nothing to save in %r, exiting early', self)\n            return\n\n        # three things are to be done:\n        # - remove (deleted if not already staged)\n        # - add (modified/untracked)\n        # - commit (with all paths that have been touched, to bypass\n        #   potential pre-staged bits)\n\n        staged_paths = self.get_staged_paths()\n        need_partial_commit = bool(staged_paths)\n        if need_partial_commit and hasattr(self, \"call_annex\"):\n            # so we have some staged content. let's check which ones\n            # are symlinks -- those could be annex key links that\n            # are broken after a `git-mv` operation\n            # https://github.com/datalad/datalad/issues/4967\n            # call `git-annex pre-commit` on them to rectify this before\n            # saving the wrong symlinks\n            added = status_state['added']\n            tofix = [\n                sp for sp in staged_paths\n                if added.get(self.pathobj / sp, {}).get(\"type\") == \"symlink\"\n            ]\n            if tofix:\n                self.call_annex(['pre-commit'], files=tofix)\n\n        submodule_change = False\n\n        if status_state['deleted']:\n            vanished_subds = [\n                str(f.relative_to(self.pathobj))\n                for f, props in status_state['deleted'].items()\n                if props.get('type') == 'dataset'\n            ]\n            if vanished_subds:\n                # we submodule removal we use `git-rm`, because the clean-up\n                # is more complex than just an index update -- make no\n                # sense to have a duplicate implementation.\n                # we do not yield here, but only altogether below -- we are just\n                # processing gone components, should always be quick.\n                self._call_git(['rm', '-q'], files=vanished_subds)\n                submodule_change = True\n            # remove anything from the index that was found to be gone\n            self._call_git(\n                ['update-index', '--remove'],\n                files=[\n                    str(f.relative_to(self.pathobj))\n                    for f, props in status_state['deleted'].items()\n                    # do not update the index, if there is already\n                    # something staged for this path (e.g.,\n                    # a directory was removed and a file staged\n                    # in its place)\n                    if not props.get('gitshasum')\n                    # we already did the submodules\n                    and props.get('type') != 'dataset'\n                ]\n            )\n            # now yield all deletions\n            for p, props in status_state['deleted'].items():\n                yield get_status_dict(\n                    action='delete',\n                    refds=self.pathobj,\n                    type=props.get('type'),\n                    path=p,\n                    status='ok',\n                    logger=lgr)\n\n        # TODO this additional query should not be, based on status as given\n        # if anyhow possible, however, when paths are given, status may\n        # not contain all required information. In case of path=None AND\n        # _status=None, we should be able to avoid this, because\n        # status should have the full info already\n        # looks for contained repositories\n        untracked_dirs = [\n            f.relative_to(self.pathobj)\n            for f, props in status_state['untracked'].items()\n            if props.get('type', None) == 'directory']\n        to_add_submodules = []\n        if untracked_dirs:\n            to_add_submodules = [\n                sm for sm, sm_props in\n                self.get_content_info(\n                    untracked_dirs,\n                    ref=None,\n                    # request exhaustive list, so that everything that is\n                    # still reported as a directory must be its own repository\n                    untracked='all').items()\n                if sm_props.get('type', None) == 'directory']\n            to_add_submodules = _prune_deeper_repos(to_add_submodules)\n\n        to_stage_submodules = {\n            f: props\n            for f, props in status_state['modified_or_untracked'].items()\n            if props.get('type', None) == 'dataset'}\n        if to_stage_submodules:\n            lgr.debug(\n                '%i submodule path(s) to stage in %r %s',\n                len(to_stage_submodules), self,\n                to_stage_submodules\n                if len(to_stage_submodules) < 10 else '')\n            to_add_submodules += list(to_stage_submodules)\n\n        if to_add_submodules:\n            for r in self._save_add_submodules(to_add_submodules):\n                if r.get('status', None) == 'ok':\n                    submodule_change = True\n                yield r\n\n        if submodule_change:\n            # this will alter the config, reload\n            self.config.reload()\n            # need to include .gitmodules in what needs committing\n            f = self.pathobj.joinpath('.gitmodules')\n            status_state['modified_or_untracked'][f] = \\\n                status_state['modified'][f] = \\\n                dict(type='file', state='modified')\n            # now stage .gitmodules\n            self._call_git(['update-index', '--add'], files=['.gitmodules'])\n            # and report on it\n            yield get_status_dict(\n                action='add',\n                refds=self.pathobj,\n                type='file',\n                path=f,\n                status='ok',\n                logger=lgr)\n\n        to_add = {\n            # TODO remove pathobj stringification when add() can\n            # handle it\n            str(f.relative_to(self.pathobj)): props\n            for f, props in status_state['modified_or_untracked'].items()\n            if not (f in to_add_submodules or f in to_stage_submodules)}\n        if to_add:\n            compat_config = \\\n                self.config.obtain(\"datalad.save.windows-compat-warning\")\n            to_add, problems = self._check_for_win_compat(to_add, compat_config)\n            lgr.debug(\n                '%i path(s) to add to %s %s',\n                len(to_add), self, to_add if len(to_add) < 10 else '')\n\n            if to_add:\n                yield from self._save_add(\n                        to_add,\n                        git_opts=None,\n                        **{k: kwargs[k] for k in kwargs\n                           if k in (('git',) if hasattr(self, 'uuid')\n                                    else tuple())})\n            if problems:\n                from datalad.interface.results import get_status_dict\n                msg = \\\n                    'Incompatible name for Windows systems; disable with ' \\\n                    'datalad.save.windows-compat-warning.',\n                for path in problems:\n                    yield get_status_dict(\n                        action='save',\n                        refds=self.pathobj,\n                        type='file',\n                        path=(self.pathobj / ut.PurePosixPath(path)),\n                        status='impossible',\n                        message=msg,\n                        logger=lgr)\n\n\n        # https://github.com/datalad/datalad/issues/6558\n        # file could have become a directory. Unfortunately git\n        # would then mistakenly refuse to commit if that old path is also\n        # given to commit, so we better filter it out\n        if status_state['deleted'] and status_state['added']:\n            # check if any \"deleted\" is a directory now. Then for those\n            # there should be some other path under that directory in 'added'\n            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:\n                # this could potentially be expensive if lots of files become\n                # directories, but it is unlikely to happen often\n                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish\n                # path_is_subpath faster, also if comparing to \"in f.parents\"\n                f_str = str(f)\n                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):\n                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post\n\n        # Note, that allow_empty is always ok when we amend. Required when we\n        # amend an empty commit while the amendment is empty, too (though\n        # possibly different message). If an empty commit was okay before, it's\n        # okay now.\n        status_state.pop('modified_or_untracked')  # pop the hybrid state\n        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,\n                        allow_empty=amend)\n        # TODO yield result for commit, prev helper checked hexsha pre\n        # and post...\n\n    def _check_for_win_compat(self, files, config):\n        \"\"\"Check file names for illegal characters or reserved names on Windows\n\n        In the case that a non-Windows-compatible file is detected, warn users\n        about potential interoperability issues.\n\n        Parameters\n        ----------\n        files\n          list of files to add\n        config\n          value of self.config.obtain(\"datalad.save.windows-compat-warning\"),\n          used to choose appropriate behavior. \"none\" performs no check,\n          \"warning\" warns in case of incompatibilities, and \"error\" results in\n          an error result in case of incompatibilities\n        \"\"\"\n        # don't perform any check when the configuration is set to 'none'\n        if config == 'none':\n            return files, None\n\n        from collections import defaultdict\n        problems = defaultdict(list)\n        for file in files:\n            for part in Path(file).parts:\n                # check every component of the path for incompatibilities\n                if Path(part).stem.upper() in RESERVED_NAMES_WIN:\n                    problems['Elements using a reserved filename:'].append(part)\n                    problems['paths'].append(file)\n                if re.search(ILLEGAL_CHARS_WIN, part):\n                    problems['Elements with illegal characters:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith('.'):\n                    problems['Elements ending with a dot:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith(' '):\n                    problems['Elements ending with a space:'].append(part)\n                    problems['paths'].append(file)\n        if not problems:\n            return files, None\n        msg = \\\n            \"Some elements of your dataset are not compatible with \" \\\n            \"Windows systems. Disable this check by changing \" \\\n            \"datalad.save.windows-compat-warning or consider renaming \" \\\n            \"the following elements: \"\n        for k, v in problems.items():\n            # use the key as an explanation, and report filenames only once\n            msg += f\"\\n{k} {[*{*v}]}\" if k != 'paths' else ''\n        if config == 'warning':\n            lgr.warning(msg)\n            return files, None\n\n        elif config == 'error':\n            # take the problematic files out of to_add\n            for path in [*{*problems['paths']}]:\n                files.pop(path)\n            return files, [*{*problems['paths']}]\n\n    def _save_add(self, files, git_opts=None):\n        \"\"\"Simple helper to add files in save()\"\"\"\n        from datalad.interface.results import get_status_dict\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.largefiles to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.largefiles=nothing', 'add'] +\n                ensure_list(git_opts) + ['--verbose'],\n                files=list(files.keys()),\n                pathspec_from_file=True,\n            )\n            # get all the entries\n            for r in self._process_git_get_output(*add_out):\n                yield get_status_dict(\n                    action=r.get('command', 'add'),\n                    refds=self.pathobj,\n                    type='file',\n                    path=(self.pathobj / ut.PurePosixPath(r['file']))\n                    if 'file' in r else None,\n                    status='ok' if r.get('success', None) else 'error',\n                    key=r.get('key', None),\n                    # while there is no git-annex underneath here, we\n                    # tend to fake its behavior, so we can also support\n                    # this type of messaging\n                    message='\\n'.join(r['error-messages'])\n                    if 'error-messages' in r else None,\n                    logger=lgr)\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n    def _save_add_submodules(self, paths):\n        \"\"\"Add new submodules, or updates records of existing ones\n\n        This method does not use `git submodule add`, but aims to be more\n        efficient by limiting the scope to mere in-place registration of\n        multiple already present repositories.\n\n        Parameters\n        ----------\n        paths : list(Path)\n\n        Yields\n        ------\n        dict\n          Result records\n        \"\"\"\n        from datalad.interface.results import get_status_dict\n\n        # first gather info from all datasets in read-only fashion, and then\n        # update index, .gitmodules and .git/config at once\n        info = []\n        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843\n        # We must not add already known submodules explicitly since \"untracked\"\n        # can be assigned even for known ones (TODO: add issue, might have been closed)?\n        # Not sure if operating on relative paths would provide any speed up so use full\n        known_sub_paths = {s['path'] for s in self.get_submodules_()}\n        for path in paths:\n            already_known = path in known_sub_paths\n            rpath = str(path.relative_to(self.pathobj).as_posix())\n            subm = repo_from_path(path)\n            # if there is a corresponding branch, we want to record it's state.\n            # we rely on the corresponding branch being synced already.\n            # `save` should do that each time it runs.\n            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())\n            if not subm_commit:\n                yield get_status_dict(\n                    action='add_submodule',\n                    ds=self,\n                    path=path,\n                    status='error',\n                    message=('cannot add subdataset %s with no commits', subm),\n                    logger=lgr)\n                continue\n            # make an attempt to configure a submodule source URL based on the\n            # discovered remote configuration\n            remote, branch = subm.get_tracking_branch()\n            url = subm.get_remote_url(remote) if remote else None\n            if url is None:\n                url = './{}'.format(rpath)\n            subm_id = subm.config.get('datalad.dataset.id', None)\n            info.append(\n                dict(\n                     # if we have additional information on this path, pass it on.\n                     # if not, treat it as an untracked directory\n                     paths[path] if isinstance(paths, dict)\n                     else dict(type='directory', state='untracked'),\n                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,\n                     url=url, known=already_known))\n\n        # bypass any convenience or safe-manipulator for speed reasons\n        # use case: saving many new subdatasets in a single run\n        with (self.pathobj / '.gitmodules').open('a') as gmf, \\\n             (self.pathobj / '.git' / 'config').open('a') as gcf:\n            for i in info:\n                # we update the subproject commit unconditionally\n                self.call_git([\n                    'update-index', '--add', '--replace', '--cacheinfo', '160000',\n                    i['commit'], i['rpath']\n                ])\n                # only write the .gitmodules/.config changes when this is not yet\n                # a subdataset and not yet already known\n                # TODO: we could update the URL, and branch info at this point,\n                # even for previously registered subdatasets\n                if not i['known'] and (i['type'] != 'dataset' or (\n                        i['type'] == 'dataset' and i['state'] == 'untracked')):\n                    gmprops = dict(path=i['rpath'], url=i['url'])\n                    if i['id']:\n                        gmprops['datalad-id'] = i['id']\n                    write_config_section(\n                        gmf, 'submodule', i['rpath'], gmprops)\n                    write_config_section(\n                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))\n\n                # This mirrors the result structure yielded for\n                # to_stage_submodules below.\n                yield get_status_dict(\n                    action='add',\n                    refds=self.pathobj,\n                    type='dataset',\n                    key=None,\n                    path=i['path'],\n                    status='ok',\n                    logger=lgr)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 776,
                                "end_lineno": 3765,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "datalad/support/gitrepo.py",
                            "type": "class",
                            "name": "GitRepo",
                            "content": "class GitRepo(CoreGitRepo):\n    \"\"\"Representation of a git repository\n\n    \"\"\"\n    # We must check git config to have name and email set, but\n    # should do it once\n    _config_checked = False\n\n    GIT_MIN_VERSION = \"2.19.1\"\n    git_version = None\n\n    @classmethod\n    def _check_git_version(cls):\n        external_versions.check(\"cmd:git\", min_version=cls.GIT_MIN_VERSION)\n        cls.git_version = external_versions['cmd:git']\n\n    # This is the least common denominator to claim that a user\n    # used DataLad.\n    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262\n    @due.dcite(Doi(\"10.21105/joss.03262\"),\n               # override path since there is no need ATM for such details\n               path=\"datalad\",\n               description=\"DataLad - Data management and distribution platform\")\n    def __init__(self, path, runner=None, create=True,\n                 git_opts=None, repo=None, fake_dates=False,\n                 create_sanity_checks=True,\n                 **kwargs):\n        \"\"\"Creates representation of git repository at `path`.\n\n        Can also be used to create a git repository at `path`.\n\n        Parameters\n        ----------\n        path: str\n          path to the git repository; In case it's not an absolute path,\n          it's relative to PWD\n        create: bool, optional\n          if true, creates a git repository at `path` if there is none. Also\n          creates `path`, if it doesn't exist.\n          If set to false, an exception is raised in case `path` doesn't exist\n          or doesn't contain a git repository.\n        repo: git.Repo, optional\n          This argument is ignored.\n        create_sanity_checks: bool, optional\n          Whether to perform sanity checks during initialization (when\n          `create=True` and target path is not a valid repo already), such as\n          that new repository is not created in the directory where git already\n          tracks some files.\n        kwargs:\n          keyword arguments serving as additional options to the git-init\n          command. Therefore, it makes sense only if called with `create`.\n\n          Generally, this way of passing options to the git executable is\n          (or will be) used a lot in this class. It's a transformation of\n          python-style keyword arguments (or a `dict`) to command line arguments,\n          provided by GitPython.\n\n          A single character keyword will be prefixed by '-', multiple characters\n          by '--'. An underscore in the keyword becomes a dash. The value of the\n          keyword argument is used as the value for the corresponding command\n          line argument. Assigning a boolean creates a flag.\n\n          Examples:\n          no_commit=True => --no-commit\n          C='/my/path'   => -C /my/path\n\n        \"\"\"\n        # this will set up .pathobj and .dot_git\n        super().__init__(path)\n\n        if self.git_version is None:\n            self._check_git_version()\n\n        # BEGIN Repo validity test\n        # We want to fail early for tests, that would be performed a lot. In\n        # particular this is about GitRepo.is_valid_repo. We would use the\n        # latter to decide whether or not to call GitRepo() only for __init__ to\n        # then test the same things again. If we fail early we can save the\n        # additional test from outer scope.\n        self.path = path\n\n        # Note, that the following three path objects are used often and\n        # therefore are stored for performance. Path object creation comes with\n        # a cost. Most notably, this is used for validity checking of the\n        # repository.\n        _valid_repo = self.is_valid_git()\n\n        do_create = False\n        if create and not _valid_repo:\n            if repo is not None:\n                # `repo` passed with `create`, which doesn't make sense\n                raise TypeError(\"argument 'repo' must not be used with 'create'\")\n            do_create = True\n        else:\n            # Note: We used to call gitpy.Repo(path) here, which potentially\n            # raised NoSuchPathError or InvalidGitRepositoryError. This is\n            # used by callers of GitRepo.__init__() to detect whether we have a\n            # valid repo at `path`. Now, with switching to lazy loading property\n            # `repo`, we detect those cases without instantiating a\n            # gitpy.Repo().\n\n            if not exists(path):\n                raise NoSuchPathError(path)\n            if not _valid_repo:\n                raise InvalidGitRepositoryError(path)\n        # END Repo validity test\n\n        # So that we \"share\" control paths with git/git-annex\n        if ssh_manager:\n            ssh_manager.ensure_initialized()\n\n        # note: we may also want to distinguish between a path to the worktree\n        # and the actual repository\n\n        if git_opts is None:\n            git_opts = {}\n        if kwargs:\n            git_opts.update(kwargs)\n\n        self._cfg = None\n\n        if do_create:  # we figured it out earlier\n            from_cmdline = git_opts.pop('_from_cmdline_', [])\n            self.init(\n                sanity_checks=create_sanity_checks,\n                init_options=from_cmdline + to_options(**git_opts),\n            )\n\n        # with DryRunProtocol path might still not exist\n        if exists(self.path):\n            self.inode = os.stat(self.path).st_ino\n        else:\n            self.inode = None\n\n        if fake_dates:\n            self.configure_fake_dates()\n\n    @property\n    def bare(self):\n        \"\"\"Returns a bool indicating whether the repository is bare\n\n        Importantly, this is not reporting the configuration value\n        of 'core.bare', in order to be usable at a stage where a\n        Repo instance is not yet equipped with a ConfigManager.\n        Instead, it is testing whether the repository path and its\n        \"dot_git\" are identical. The value of 'core.bare' can be query\n        from the ConfigManager in a fully initialized instance.\n        \"\"\"\n        return self.pathobj == self.dot_git\n\n    @classmethod\n    def clone(cls, url, path, *args, clone_options=None, **kwargs):\n        \"\"\"Clone url into path\n\n        Provides workarounds for known issues (e.g.\n        https://github.com/datalad/datalad/issues/785)\n\n        Parameters\n        ----------\n        url : str\n        path : str\n        clone_options : dict or list\n          Arbitrary options that will be passed on to the underlying call to\n          `git-clone`. This may be a list of plain options or key-value pairs\n          that will be converted to a list of plain options with `to_options`.\n        expect_fail : bool\n          Whether expect that command might fail, so error should be logged then\n          at DEBUG level instead of ERROR\n        kwargs:\n          Passed to the Repo class constructor.\n        \"\"\"\n\n        if 'repo' in kwargs:\n            raise TypeError(\"argument 'repo' conflicts with cloning\")\n            # TODO: what about 'create'?\n\n        expect_fail = kwargs.pop('expect_fail', False)\n        # fail early on non-empty target:\n        from os import listdir\n        if exists(path) and listdir(path):\n            raise ValueError(\n                \"destination path '%s' already exists and is not an \"\n                \"empty directory.\" % path)\n        else:\n            # protect against cloning into existing and obviously dangling\n            # instance for that location\n            try:\n                del cls._unique_instances[path]\n            except KeyError:\n                # didn't exist - all fine\n                pass\n\n        # Massage URL\n        url_ri = RI(url) if not isinstance(url, RI) else url\n        if on_windows:\n            # When we're cloning from a local path on Windows, the URL at\n            # this point is platform-specific (e.g., \"..\\\\origin\"). According \n            # to Git clone's manpage, clone urls can't have backslashes.\n            # While Git does manage to clone a URL with backslashes, \n            # in the case of subdatasets cloned from relative paths it nevertheless\n            # messed up the resulting remote url, resulting in a mix of\n            # front and backslashes (see also gh-7180): \n            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\\\origin' \n            # Therefore, we're turning it to Posix now.\n            if isinstance(url_ri, PathRI):\n                url = Path(url).as_posix()\n                url_ri = PathRI(url)\n\n        else:\n            # if we are on windows, the local path of a URL\n            # would not end up being a proper local path and cloning\n            # would fail. Don't try to be smart and just pass the\n            # URL along unmodified\n\n            # try to get a local path from `url`:\n            try:\n                url = url_ri.localpath\n                url_ri = RI(url)\n            except ValueError:\n                pass\n\n        if is_ssh(url_ri):\n            ssh_manager.get_connection(url).open()\n        else:\n            if isinstance(url_ri, PathRI):\n                # expand user, because execution not going through a shell\n                # doesn't work well otherwise\n                new_url = os.path.expanduser(url)\n                if url != new_url:\n                    lgr.info(\"Expanded source path to %s from %s\", new_url, url)\n                    url = new_url\n\n        cmd = cls._git_cmd_prefix + ['clone', '--progress']\n        if clone_options:\n            if isinstance(clone_options, Mapping):\n                clone_options = to_options(**clone_options)\n            cmd.extend(clone_options)\n        cmd.extend([url, path])\n\n        fix_annex = None\n        ntries = 5  # 3 is not enough for robust workaround\n        for trial in range(ntries):\n            try:\n                lgr.debug(\"Git clone from %s to %s\", url, path)\n\n                res = GitWitlessRunner().run(cmd, protocol=GitProgress)\n                # fish out non-critical warnings by git-clone\n                # (empty repo clone, etc.), all other content is logged\n                # by the progress helper to 'debug'\n                for errline in res['stderr'].splitlines():\n                    if errline.startswith('warning:'):\n                        lgr.warning(errline[8:].strip())\n                lgr.debug(\"Git clone completed\")\n                break\n            except CommandError as e:\n                # log here but let caller decide what to do\n                ce = CapturedException(e)\n                str_e = str(e)\n                # see https://github.com/datalad/datalad/issues/785\n                if re.search(\"Request for .*aborted.*Unable to find\", str_e,\n                             re.DOTALL) \\\n                        and trial < ntries - 1:\n                    lgr.info(\n                        \"Hit a known issue with Git (see GH#785). Trial #%d, \"\n                        \"retrying\",\n                        trial)\n                    continue\n                    #(lgr.debug if expect_fail else lgr.error)(e_str)\n\n                if \"Clone succeeded, but checkout failed.\" in str_e:\n                    fix_annex = ce\n                    break\n\n                raise\n\n        # get ourselves a repository instance\n        gr = cls(path, *args, **kwargs)\n        if fix_annex:\n            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then\n            # would need to import our own subclass\n            if hasattr(gr, 'is_valid_annex'):\n                lgr.warning(\"Experienced issues while cloning. \"\n                            \"Trying to fix it, using git-annex-fsck.\")\n                if not gr.is_initialized():\n                    gr._init()\n                gr.fsck()\n            else:\n                lgr.warning(\"Experienced issues while cloning: %s\", fix_annex)\n        # ensure that Git doesn't mangle relative paths into obscure absolute\n        # paths: https://github.com/datalad/datalad/issues/3538\n        if isinstance(url_ri, PathRI):\n            url_path = Path(url)\n            if not url_path.is_absolute():\n                # get git-created path\n                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'\n                git_url = gr.config.get(remote_url)\n                if Path(git_url).is_absolute():\n                    # Git created an absolute path from a relative URL.\n                    git_url = op.relpath(git_url, gr.path)\n                path = Path(git_url)\n                # always in POSIX even on Windows\n                path = path.as_posix()\n                gr.config.set(remote_url, path,\n                              scope='local', force=True)\n        return gr\n\n    # Note: __del__ shouldn't be needed anymore as we switched to\n    #       `weakref.finalize`.\n    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods\n    #\n    #       Keeping both methods and this comment around as a reminder to not\n    #       use __del__, if we figure there's a need for cleanup in the future.\n    #\n    # def __del__(self):\n    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird\n    #     # stalls etc\n    #     self._cfg = None\n\n    def is_valid_git(self):\n        \"\"\"Returns whether the underlying repository appears to be still valid\n\n        Note, that this almost identical to the classmethod is_valid_repo().\n        However, if we are testing an existing instance, we can save Path object\n        creations. Since this testing is done a lot, this is relevant. Creation\n        of the Path objects in is_valid_repo() takes nearly half the time of the\n        entire function.\n\n        Also note, that this method is bound to an instance but still\n        class-dependent, meaning that a subclass cannot simply overwrite it.\n        This is particularly important for the call from within __init__(),\n        which in turn is called by the subclasses' __init__. Using an overwrite\n        would lead to the wrong thing being called.\n        \"\"\"\n        return self.is_valid()\n\n    @classmethod\n    def is_valid_repo(cls, path):\n        \"\"\"Returns if a given path points to a git repository\"\"\"\n        return cls.is_valid(path)\n\n    @staticmethod\n    def get_git_dir(repo):\n        \"\"\"figure out a repo's gitdir\n\n        '.git' might be a  directory, a symlink or a file\n\n        Note\n        ----\n        This method is likely to get deprecated, please use GitRepo.dot_git instead!\n        That one's not static, but it's cheaper and you should avoid\n        not having an instance of a repo you're working on anyway.\n        Note, that the property in opposition to this method returns an absolute path.\n\n\n        Parameters\n        ----------\n        repo: path or Repo instance\n          currently expected to be the repos base dir\n\n        Returns\n        -------\n        str\n          relative path to the repo's git dir; So, default would be \".git\"\n        \"\"\"\n        if isinstance(repo, GitRepo):\n            return str(repo.dot_git)\n        pathobj = Path(repo)\n        dot_git = _get_dot_git(pathobj, ok_missing=False)\n        try:\n            dot_git = dot_git.relative_to(pathobj)\n        except ValueError:\n            # is not a subpath, return as is\n            lgr.debug(\"Path %r is not subpath of %r\", dot_git, pathobj)\n        return str(dot_git)\n\n    @property\n    def config(self):\n        # just proxy the core repo APIs property for backward-compatibility\n        return self.cfg\n\n    def is_with_annex(self):\n        \"\"\"Report if GitRepo (assumed) has (remotes with) a git-annex branch\n        \"\"\"\n        return any(\n            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])\n        )\n\n    @classmethod\n    def get_toppath(cls, path, follow_up=True, git_options=None):\n        \"\"\"Return top-level of a repository given the path.\n\n        Parameters\n        -----------\n        follow_up : bool\n          If path has symlinks -- they get resolved by git.  If follow_up is\n          True, we will follow original path up until we hit the same resolved\n          path.  If no such path found, resolved one would be returned.\n        git_options: list of str\n          options to be passed to the git rev-parse call\n\n        Return None if no parent directory contains a git repository.\n        \"\"\"\n        cmd = ['git']\n        if git_options:\n            cmd.extend(git_options)\n        cmd += [\"rev-parse\", \"--show-toplevel\"]\n        try:\n            out = GitWitlessRunner(cwd=path).run(\n                cmd, protocol=StdOutErrCapture)\n            toppath = out['stdout'].rstrip('\\n\\r')\n        except CommandError:\n            return None\n        except OSError:\n            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,\n                                          git_options=git_options)\n\n        # normalize the report, because, e.g. on windows it can come out\n        # with improper directory separators (C:/Users/datalad)\n        toppath = str(Path(toppath))\n\n        if follow_up:\n            path_ = path\n            path_prev = \"\"\n            while path_ and path_ != path_prev:  # on top /.. = /\n                if str(Path(path_).resolve()) == toppath:\n                    toppath = path_\n                    break\n                path_prev = path_\n                path_ = dirname(path_)\n\n        return toppath\n\n    @normalize_paths\n    def add(self, files, git=True, git_options=None, update=False):\n        \"\"\"Adds file(s) to the repository.\n\n        Parameters\n        ----------\n        files: list\n          list of paths to add\n        git: bool\n          somewhat ugly construction to be compatible with AnnexRepo.add();\n          has to be always true.\n        update: bool\n          --update option for git-add. From git's manpage:\n           Update the index just where it already has an entry matching\n           <pathspec>. This removes as well as modifies index entries to match\n           the working tree, but adds no new files.\n\n           If no <pathspec> is given when --update option is used, all tracked\n           files in the entire working tree are updated (old versions of Git\n           used to limit the update to the current directory and its\n           subdirectories).\n\n        Returns\n        -------\n        list\n          Of status dicts.\n        \"\"\"\n        # under all circumstances call this class' add_ (otherwise\n        # AnnexRepo.add would go into a loop\n        return list(GitRepo.add_(self, files, git=git, git_options=git_options,\n                    update=update))\n\n    def add_(self, files, git=True, git_options=None, update=False):\n        \"\"\"Like `add`, but returns a generator\"\"\"\n        # TODO: git_options is used as options for the git-add here,\n        # instead of options to the git executable => rename for consistency\n\n        if not git:\n            lgr.warning(\n                'GitRepo.add() called with git=%s, this should not happen',\n                git)\n            git = True\n\n        # there is no other way then to collect all files into a list\n        # at this point, because we need to pass them at once to a single\n        # `git add` call\n        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]\n\n        if not (files or git_options or update):\n            # wondering why just a warning? in cmdline this is also not an error\n            lgr.warning(\"add was called with empty file list and no options.\")\n            return\n\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.gitaddtoannex to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.gitaddtoannex=false', 'add'] +\n                ensure_list(git_options) +\n                to_options(update=update) + ['--verbose'],\n                files=files,\n                pathspec_from_file=True,\n                read_only=False,\n            )\n            # get all the entries\n            for o in self._process_git_get_output(*add_out):\n                yield o\n            # Note: as opposed to git cmdline, force is True by default in\n            #       gitpython, which would lead to add things, that are\n            #       ignored or excluded otherwise\n            # 2. Note: There is an issue with globbing (like adding '.'),\n            #       which apparently doesn't care for 'force' and therefore\n            #       adds '.git/...'. May be it's expanded at the wrong\n            #       point in time or sth. like that.\n            # For now, use direct call to git add.\n            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,\n            #                      force=False)\n            # TODO: May be make use of 'fprogress'-option to indicate\n            # progress\n            # But then, we don't have it for git-annex add, anyway.\n            #\n            # TODO: Is write=True a reasonable way to do it?\n            # May be should not write until success of operation is\n            # confirmed?\n            # What's best in case of a list of files?\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n        # Make sure return value from GitRepo is consistent with AnnexRepo\n        # currently simulating similar return value, assuming success\n        # for all files:\n        # TODO: Make return values consistent across both *Repo classes!\n        return\n\n    @staticmethod\n    def _process_git_get_output(stdout, stderr=None):\n        \"\"\"Given both outputs (stderr is ignored atm) of git add - process it\n\n        Primarily to centralize handling in both indirect annex and direct\n        modes when ran through proxy\n        \"\"\"\n        return [{u'file': f, u'success': True}\n                for f in re.findall(\"'(.*)'[\\n$]\", ensure_unicode(stdout))]\n\n    @normalize_paths(match_return_type=False)\n    def remove(self, files, recursive=False, **kwargs):\n        \"\"\"Remove files.\n\n        Calls git-rm.\n\n        Parameters\n        ----------\n        files: list of str\n          list of paths to remove\n        recursive: False\n          whether to allow recursive removal from subdirectories\n        kwargs:\n          see `__init__`\n\n        Returns\n        -------\n        [str]\n          list of successfully removed files.\n        \"\"\"\n        if recursive:\n            kwargs['r'] = True\n\n        # the name is chosen badly, but the purpose is to make sure that\n        # any pending operations actually manifest themselves in the Git repo\n        # on disk (in case of an AnnexRepo, it could be pending batch\n        # processes that need closing)\n        self.precommit()\n\n        # output per removed file is expected to be \"rm 'PATH'\":\n        return [\n            line.strip()[4:-1]\n            for line in self.call_git_items_(\n                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)\n        ]\n\n    def precommit(self):\n        \"\"\"Perform pre-commit maintenance tasks\n        \"\"\"\n        # we used to clean up GitPython here\n        pass\n\n    @staticmethod\n    def _get_prefixed_commit_msg(msg):\n        DATALAD_PREFIX = \"[DATALAD]\"\n        return DATALAD_PREFIX if not msg else \"%s %s\" % (DATALAD_PREFIX, msg)\n\n    def configure_fake_dates(self):\n        \"\"\"Configure repository to use fake dates.\n        \"\"\"\n        lgr.debug(\"Enabling fake dates\")\n        self.config.set(\"datalad.fake-dates\", \"true\")\n\n    @property\n    def fake_dates_enabled(self):\n        \"\"\"Is the repository configured to use fake dates?\n        \"\"\"\n        # this turned into a private property of the CoreGitRepo\n        return self._fake_dates_enabled\n\n    def add_fake_dates(self, env):\n        # was renamed in CoreGitRepo\n        return self.add_fake_dates_to_env(env)\n\n    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,\n               files=None, date=None, index_file=None):\n        \"\"\"Commit changes to git.\n\n        Parameters\n        ----------\n        msg: str, optional\n          commit-message\n        options: list of str, optional\n          cmdline options for git-commit\n        _datalad_msg: bool, optional\n          To signal that commit is automated commit by datalad, so\n          it would carry the [DATALAD] prefix\n        careless: bool, optional\n          if False, raise when there's nothing actually committed;\n          if True, don't care\n        files: list of str, optional\n          path(s) to commit\n        date: str, optional\n          Date in one of the formats git understands\n        index_file: str, optional\n          An alternative index to use\n        \"\"\"\n\n        self.precommit()\n\n        # assemble commandline\n        cmd = ['commit']\n        options = ensure_list(options)\n\n        if date:\n            options += [\"--date\", date]\n\n        orig_msg = msg\n        if not msg:\n            if '--amend' in options:\n                if '--no-edit' not in options:\n                    # don't overwrite old commit message with our default\n                    # message by default, but re-use old one. In other words:\n                    # Make --no-edit the default:\n                    options += [\"--no-edit\"]\n            else:\n                msg = 'Recorded changes'\n                _datalad_msg = True\n\n        if _datalad_msg:\n            msg = self._get_prefixed_commit_msg(msg)\n\n        if msg:\n            options += [\"-m\", msg]\n        cmd.extend(options)\n\n        # set up env for commit\n        env = self.add_fake_dates(None) \\\n            if self.fake_dates_enabled else os.environ.copy()\n        if index_file:\n            env['GIT_INDEX_FILE'] = index_file\n\n        lgr.debug(\"Committing via direct call of git: %s\", cmd)\n\n        prev_sha = self.get_hexsha()\n\n        # Old code was doing clever --amend'ing of chunked series of commits manually\n        # here, but with pathspec_from_file it is no longer needed.\n        # store pre-commit state to be able to check if anything was committed\n        try:\n            # Note: call_git operates via joining call_git_items_ and that one wipes out\n            # .stdout from exception and collects/repopulates stderr only. Let's use\n            # _call_git which returns both outputs and collects/re-populates both stdout\n            # **and** stderr\n            _ = self._call_git(\n                    cmd,\n                    files=files,\n                    env=env,\n                    pathspec_from_file=True,\n            )\n        except CommandError as e:\n            # real errors first\n            if \"did not match any file(s) known to git\" in e.stderr:\n                raise FileNotInRepositoryError(\n                    cmd=e.cmd,\n                    msg=\"File(s) unknown to git\",\n                    code=e.code,\n                    filename=linesep.join([\n                        l for l in e.stderr.splitlines()\n                        if l.startswith(\"error: pathspec\")\n                    ])\n                )\n            # behavior choices now\n            elif not careless:\n                # not willing to compromise at all\n                raise\n            elif 'nothing to commit' in e.stdout:\n                lgr.debug(\"nothing to commit in %s. Ignored.\", self)\n            elif 'no changes added to commit' in e.stdout or \\\n                    'nothing added to commit' in e.stdout:\n                lgr.debug(\"no changes added to commit in %s. Ignored.\", self)\n            else:\n                raise\n        if orig_msg \\\n                or '--dry-run' in cmd \\\n                or prev_sha == self.get_hexsha() \\\n                or ('--amend' in cmd and '--no-edit' in cmd) \\\n                or (not is_interactive()) \\\n                or self.config.obtain('datalad.save.no-message') != 'interactive':\n            # we had a message given, or nothing was committed, or prev. commit\n            # was amended, or we are not connected to a terminal, or no\n            # interactive message input is desired:\n            # we can go home\n            return\n\n        # handle interactive message entry by running another `git-commit`\n        self._git_runner.run(\n            cmd + ['--amend', '--edit'],\n            protocol=NoCapture,\n            stdin=None,\n            env=env,\n        )\n\n    # TODO usage is only in the tests, consider making a test helper and\n    # remove from GitRepo API\n    def get_indexed_files(self):\n        \"\"\"Get a list of files in git's index\n\n        Returns\n        -------\n        list\n            list of paths rooting in git's base dir\n        \"\"\"\n\n        return [\n            str(r.relative_to(self.pathobj))\n            for r in self.get_content_info(\n                paths=None, ref=None, untracked='no')\n        ]\n\n    def format_commit(self, fmt, commitish=None):\n        \"\"\"Return `git show` output for `commitish`.\n\n        Parameters\n        ----------\n        fmt : str\n            A format string accepted by `git show`.\n        commitish: str, optional\n          Any commit identifier (defaults to \"HEAD\").\n\n        Returns\n        -------\n        str or, if there are not commits yet, None.\n        \"\"\"\n        # use git-log and not git-show due to faster performance with\n        # complex commits (e.g. octopus merges)\n        # https://github.com/datalad/datalad/issues/4801\n        cmd = ['log', '-1', '-z', '--format=' + fmt]\n        if commitish is not None:\n            cmd.append(commitish + \"^{commit}\")\n        # make sure Git takes our argument as a revision\n        cmd.append('--')\n        try:\n            stdout = self.call_git(\n                cmd, expect_stderr=True, expect_fail=True,\n                read_only=True)\n        except CommandError as e:\n            if 'bad revision' in e.stderr:\n                raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n            elif 'does not have any commits yet' in e.stderr:\n                return None\n            else:\n                raise e\n        # This trailing null is coming from the -z above, which avoids the\n        # newline that Git would append to the output. We could drop -z and\n        # strip the newline directly, but then we'd have to worry about\n        # compatibility across platforms.\n        return stdout.rsplit(\"\\0\", 1)[0]\n\n    def get_hexsha(self, commitish=None, short=False):\n        \"\"\"Return a hexsha for a given commitish.\n\n        Parameters\n        ----------\n        commitish : str, optional\n          Any identifier that refers to a commit (defaults to \"HEAD\").\n        short : bool, optional\n          Return the abbreviated form of the hexsha.\n\n        Returns\n        -------\n        str or, if no commitish was given and there are no commits yet, None.\n\n        Raises\n        ------\n        ValueError\n          If a commitish was given, but no corresponding commit could be\n          determined.\n        \"\"\"\n        # use --quiet because the 'Needed a single revision' error message\n        # that is the result of running this in a repo with no commits\n        # isn't useful to report\n        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(\n            commitish if commitish else 'HEAD')\n        ]\n        if short:\n            cmd.append('--short')\n        try:\n            return self.call_git_oneline(cmd, read_only=True)\n        except CommandError as e:\n            if commitish is None:\n                return None\n            raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n\n    @normalize_paths(match_return_type=False)\n    def get_last_commit_hexsha(self, files):\n        \"\"\"Return the hash of the last commit the modified any of the given\n        paths\"\"\"\n        try:\n            commit = self.call_git(\n                ['rev-list', '-n1', 'HEAD'],\n                files=files,\n                expect_fail=True,\n                read_only=True,\n            )\n            commit = commit.strip()\n            return commit if commit else None\n        except CommandError:\n            if self.get_hexsha() is None:\n                # unborn branch, don't freak out\n                return None\n            raise\n\n    def get_revisions(self, revrange=None, fmt=\"%H\", options=None):\n        \"\"\"Return list of revisions in `revrange`.\n\n        Parameters\n        ----------\n        revrange : str or list of str or None, optional\n            Revisions or revision ranges to walk. If None, revision defaults to\n            HEAD unless a revision-modifying option like `--all` or\n            `--branches` is included in `options`.\n        fmt : string, optional\n            Format accepted by `--format` option of `git log`. This should not\n            contain new lines because the output is split on new lines.\n        options : list of str, optional\n            Options to pass to `git log`.  This should not include `--format`.\n\n        Returns\n        -------\n        List of revisions (str), formatted according to `fmt`.\n        \"\"\"\n        if revrange is None:\n            revrange = []\n        elif isinstance(revrange, str):\n            revrange = [revrange]\n\n        cmd = [\"log\", \"--format={}\".format(fmt)]\n        cmd.extend((options or []) + revrange + [\"--\"])\n        try:\n            stdout = self.call_git(cmd, expect_fail=True, read_only=True)\n        except CommandError as e:\n            if \"does not have any commits\" in e.stderr:\n                return []\n            raise\n        return stdout.splitlines()\n\n    def commit_exists(self, commitish):\n        \"\"\"Does `commitish` exist in the repo?\n\n        Parameters\n        ----------\n        commitish : str\n            A commit or an object that can be dereferenced to one.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        # Note: The peeling operator \"^{commit}\" is required so that rev-parse\n        # doesn't succeed if passed a full hexsha that is valid but doesn't\n        # exist.\n        return self.call_git_success(\n            [\"rev-parse\", \"--verify\", commitish + \"^{commit}\"],\n            read_only=True,\n        )\n\n    def get_merge_base(self, commitishes):\n        \"\"\"Get a merge base hexsha\n\n        Parameters\n        ----------\n        commitishes: str or list of str\n          List of commitishes (branches, hexshas, etc) to determine the merge\n          base of. If a single value provided, returns merge_base with the\n          current branch.\n\n        Returns\n        -------\n        str or None\n          If no merge-base for given commits, or specified treeish doesn't\n          exist, None returned\n        \"\"\"\n        if isinstance(commitishes, str):\n            commitishes = [commitishes]\n        if not commitishes:\n            raise ValueError(\"Provide at least a single value\")\n        elif len(commitishes) == 1:\n            commitishes = commitishes + [self.get_active_branch()]\n\n        try:\n            base = self.call_git_oneline(['merge-base'] + commitishes,\n                                         read_only=True)\n        except CommandError as exc:\n            if exc.code == 1 and not (exc.stdout or exc.stderr):\n                # No merge base was found (unrelated commits).\n                return None\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                return None\n            raise\n\n        return base\n\n    def is_ancestor(self, reva, revb):\n        \"\"\"Is `reva` an ancestor of `revb`?\n\n        Parameters\n        ----------\n        reva, revb : str\n            Revisions.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        return self.call_git_success(\n            [\"merge-base\", \"--is-ancestor\", reva, revb],\n            read_only=True)\n\n    def get_commit_date(self, branch=None, date='authored'):\n        \"\"\"Get the date stamp of the last commit (in a branch or head otherwise)\n\n        Parameters\n        ----------\n        date: {'authored', 'committed'}\n          Which date to return.  \"authored\" will be the date shown by \"git show\"\n          and the one possibly specified via --date to `git commit`\n\n        Returns\n        -------\n        int or None\n          None if no commit\n        \"\"\"\n        if date == 'committed':\n            format = '%ct'\n        elif date == 'authored':\n            format = '%at'\n        else:\n            raise ValueError('unknow date type: {}'.format(date))\n        d = self.format_commit(format, commitish=branch)\n        return int(d) if d else None\n\n    def get_active_branch(self):\n        \"\"\"Get the name of the active branch\n\n        Returns\n        -------\n        str or None\n          Returns None if there is no active branch, i.e. detached HEAD,\n          and the branch name otherwise.\n        \"\"\"\n        try:\n            out = self.call_git([\"symbolic-ref\", \"HEAD\"], expect_fail=True,\n                                read_only=True)\n        except CommandError as e:\n            if 'HEAD is not a symbolic ref' in e.stderr:\n                lgr.debug(\"detached HEAD in %s\", self)\n                return None\n            else:\n                raise e\n        return out.strip()[11:]  # strip refs/heads/\n\n    def get_corresponding_branch(self, branch=None):\n        \"\"\"Always returns None, a plain GitRepo has no managed branches\"\"\"\n        return None\n\n    def get_branches(self):\n        \"\"\"Get all branches of the repo.\n\n        Returns\n        -------\n        [str]\n            Names of all branches of this repository.\n        \"\"\"\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')\n        ]\n\n    def get_remote_branches(self):\n        \"\"\"Get all branches of all remotes of the repo.\n\n        Returns\n        -----------\n        [str]\n            Names of all remote branches.\n        \"\"\"\n        # TODO: Reconsider melting with get_branches()\n\n        # TODO: treat entries like this: origin/HEAD -> origin/master'\n        # currently this is done in collection\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')\n        ]\n\n    def get_remotes(self, with_urls_only=False):\n        \"\"\"Get known remotes of the repository\n\n        Parameters\n        ----------\n        with_urls_only : bool, optional\n          return only remotes which have urls\n\n        Returns\n        -------\n        remotes : list of str\n          List of names of the remotes\n        \"\"\"\n        from datalad.utils import unique\n\n        self.config.reload()\n        remotes = unique([x[7:] for x in self.config.sections()\n                          if x.startswith(\"remote.\")])\n\n        if with_urls_only:\n            remotes = [\n                r for r in remotes\n                if self.config.get('remote.%s.url' % r)\n            ]\n        return remotes\n\n    # TODO this is practically unused outside the tests, consider turning\n    # into a test helper and trim from the API\n    def get_files(self, branch=None):\n        \"\"\"Get a list of files in git.\n\n        Lists the files in the (remote) branch.\n\n        Parameters\n        ----------\n        branch: str\n          Name of the branch to query. Default: active branch.\n\n        Returns\n        -------\n        [str]\n          list of files.\n        \"\"\"\n        return [\n            str(p.relative_to(self.pathobj))\n            for p in self.get_content_info(\n                paths=None, ref=branch, untracked='no')\n            ]\n\n    def add_remote(self, name, url, options=None):\n        \"\"\"Register remote pointing to a url\n        \"\"\"\n        cmd = ['remote', 'add']\n        if options:\n            cmd += options\n        cmd += [name, url]\n\n        # for historical reasons this method returns stdout and\n        # stderr, keeping that for now\n        result = self._call_git(cmd)\n        self.config.reload()\n        return result\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n\n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n                raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\n            else:\n                raise e\n\n        # config.reload necessary, because the associated remote config\n        # will vanish\n        self.config.reload()\n        return\n\n    def _maybe_open_ssh_connection(self, remote, prefer_push=True):\n        \"\"\"Open connection if `remote` has an SSH URL.\n\n        Doing so enables SSH caching, preventing datalad-sshrun subprocesses\n        from opening (and then closing) their own.\n\n        Parameters\n        ----------\n        remote : str\n        prefer_push : bool, optional\n            Use `remote.<remote>.pushurl` if there is one, falling back to\n            `remote.<remote>.url`.\n        \"\"\"\n        if remote:\n            url = None\n            if prefer_push:\n                url = self.get_remote_url(remote, push=True)\n            url = url or self.get_remote_url(remote)\n            if url and is_ssh(url):\n                ssh_manager.get_connection(url).open()\n\n    def update_remote(self, name=None, verbose=False):\n        \"\"\"\n        \"\"\"\n        options = [\"-v\"] if verbose else []\n        self._maybe_open_ssh_connection(name)\n        name = [name] if name else []\n        self.call_git(\n            ['remote'] + name + ['update'] + options,\n            expect_stderr=True\n        )\n\n    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,\n              **kwargs):\n        \"\"\"Fetches changes from a remote (or all remotes).\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to fetch from. If no remote is given and\n          `all_` is not set, the tracking branch is fetched.\n        refspec : str or list, optional\n          refspec(s) to fetch.\n        all_ : bool, optional\n          fetch all remotes (and all of their branches).\n          Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-fetch.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-fetch.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        return list(\n            self.fetch_(\n                remote=remote,\n                refspec=refspec,\n                all_=all_,\n                git_options=git_options,\n            )\n        )\n\n    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `fetch`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],\n            action='fetch',\n            urlvars=('remote.{}.url', 'remote.{}.url'),\n            protocol=GitProgress,\n            info_cls=FetchInfo,\n            info_from='stderr',\n            add_remote=False,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def push(self, remote=None, refspec=None, all_remotes=False,\n             all_=False, git_options=None, **kwargs):\n        \"\"\"Push changes to a remote (or all remotes).\n\n        If remote and refspec are specified, and remote has\n        `remote.{remote}.datalad-push-default-first` configuration variable\n        set (e.g. by `create-sibling-github`), we will first push the first\n        refspec separately to possibly ensure that the first refspec is chosen\n        by remote as the \"default branch\".\n        See https://github.com/datalad/datalad/issues/4997\n        Upon successful push if this variable was set in the local git config,\n        we unset it, so subsequent pushes would proceed normally.\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to push to. If no remote is given and\n          `all_` is not set, the tracking branch is pushed.\n        refspec : str or list, optional\n          refspec(s) to push.\n        all_ : bool, optional\n          push to all remotes. Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-push.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-push.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        if all_remotes:\n            # be nice to the elderly\n            all_ = True\n\n        push_refspecs = [refspec]\n        cfg = self.config  # shortcut\n        cfg_push_var = \"remote.{}.datalad-push-default-first\".format(remote)\n        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):\n            refspec = ensure_list(refspec)\n            lgr.debug(\"As indicated by %s pushing first refspec %s separately first\",\n                      cfg_push_var, refspec[0])\n            push_refspecs = [[refspec[0]], refspec[1:]]\n\n        push_res = []\n        for refspecs in push_refspecs:\n            push_res.extend(\n                self.push_(\n                    remote=remote,\n                    refspec=refspecs,\n                    all_=all_,\n                    git_options=git_options,\n                )\n            )\n        # note: above push_ should raise exception if errors out\n        if '--dry-run' not in git_options \\\n            and cfg.get_from_source('local', cfg_push_var) is not None:\n            lgr.debug(\"Removing %s variable from local git config after successful push\", cfg_push_var)\n            cfg.unset(cfg_push_var, 'local')\n        return push_res\n\n    def push_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `push`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],\n            action='push',\n            urlvars=('remote.{}.pushurl', 'remote.{}.url'),\n            protocol=StdOutCaptureWithGitProgress,\n            info_cls=PushInfo,\n            info_from='stdout',\n            add_remote=True,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def _fetch_push_helper(\n            self,\n            base_cmd,     # arg list\n            action,       # label fetch|push\n            urlvars,      # variables to query for URLs\n            protocol,     # processor for output\n            info_cls,     # Push|FetchInfo\n            info_from,    # stdout, stderr\n            add_remote,   # whether to add a 'remote' field to the info dict\n            remote=None, refspec=None, all_=False, git_options=None):\n\n        git_options = ensure_list(git_options)\n\n        cmd = base_cmd + git_options\n\n        if remote is None:\n            if refspec:\n                # conflicts with using tracking branch or push all remotes\n                # For now: Just fail.\n                # TODO: May be check whether it fits to tracking branch\n                raise ValueError(\n                    \"refspec specified without a remote. ({})\".format(refspec))\n            if all_:\n                remotes_to_process = self.get_remotes(with_urls_only=True)\n            else:\n                # No explicit remote to fetch.\n                # => get tracking branch:\n                tb_remote, refspec = self.get_tracking_branch()\n                if tb_remote is not None:\n                    remotes_to_process = [tb_remote]\n                else:\n                    # No remote, no tracking branch\n                    # => fail\n                    raise ValueError(\n                        \"Neither a remote is specified to {} \"\n                        \"from nor a tracking branch is set up.\".format(action))\n        else:\n            if all_:\n                raise ValueError(\n                    \"Option 'all_' conflicts with specified remote \"\n                    \"'{}'.\".format(remote))\n            remotes_to_process = [remote]\n\n        if refspec:\n            # prep for appending to cmd\n            refspec = ensure_list(refspec)\n\n        # no need for progress report, when there is just one remote\n        log_remote_progress = len(remotes_to_process) > 1\n        if log_remote_progress:\n            pbar_id = '{}remotes-{}'.format(action, id(self))\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start %sing remotes for %s', action, self,\n                total=len(remotes_to_process),\n                label=action.capitalize(),\n                unit=' Remotes',\n            )\n        try:\n            for remote in remotes_to_process:\n                r_cmd = cmd + [remote]\n                if refspec:\n                    r_cmd += refspec\n\n                if log_remote_progress:\n                    log_progress(\n                        lgr.info,\n                        pbar_id,\n                        '{}ing remote %s'.format(action.capitalize()),\n                        remote,\n                        update=1,\n                        increment=True,\n                    )\n                # best effort to enable SSH connection caching\n                url = self.config.get(\n                    # make two attempts to get a URL\n                    urlvars[0].format(remote),\n                    self.config.get(\n                        urlvars[1].format(remote),\n                        None)\n                )\n                if url and is_ssh(url):\n                    ssh_manager.get_connection(url).open()\n                try:\n                    out = self._git_runner.run(\n                        r_cmd,\n                        protocol=protocol,\n                    )\n                    output = out[info_from] or ''\n                except CommandError as e:\n                    output = None\n                    # intercept some errors that we express as an error report\n                    # in the info dicts\n                    if re.match(\n                            '.*^error: failed to (push|fetch) some refs',\n                            e.stderr,\n                            re.DOTALL | re.MULTILINE):\n                        output = getattr(e, info_from)\n                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()\n                                          if l.startswith('hint: ')])\n                        if output is None:\n                            output = ''\n                    if not output:\n                        raise\n\n                for line in output.splitlines():\n                    try:\n                        # push info doesn't identify a remote, add it here\n                        pi = info_cls._from_line(line)\n                        if add_remote:\n                            pi['remote'] = remote\n                        # There were errors, but Git provided hints\n                        if 'error' in pi['operations']:\n                            pi['hints'] = hints or None\n                        yield pi\n                    except Exception:\n                        # it is not progress and no push info\n                        # don't hide it completely\n                        lgr.debug('git-%s reported: %s', action, line)\n        finally:\n            if log_remote_progress:\n                log_progress(\n                    lgr.info,\n                    pbar_id,\n                    'Finished %sing remotes for %s', action, self,\n                )\n\n    def get_remote_url(self, name, push=False):\n        \"\"\"Get the url of a remote.\n\n        Reads the configuration of remote `name` and returns its url or None,\n        if there is no url configured.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        push: bool\n          if True, get the pushurl instead of the fetch url.\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        return self.config.get(var, None)\n\n    def set_remote_url(self, name, url, push=False):\n        \"\"\"Set the URL a remote is pointing to\n\n        Sets the URL of the remote `name`. Requires the remote to already exist.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        url: str\n        push: bool\n          if True, set the push URL, otherwise the fetch URL\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        self.config.set(var, url, scope='local', reload=True)\n\n    def get_branch_commits_(self, branch=None, limit=None, stop=None):\n        \"\"\"Return commit hexshas for a branch\n\n        Parameters\n        ----------\n        branch: str, optional\n          If not provided, assumes current branch\n        limit: None | 'left-only', optional\n          Limit which commits to report.  If None -- all commits (merged or not),\n          if 'left-only' -- only the commits from the left side of the tree upon\n          merges\n        stop: str, optional\n          hexsha of the commit at which stop reporting (matched one is not\n          reported either)\n\n        Yields\n        ------\n        str\n        \"\"\"\n        cmd = ['rev-list']\n        if limit == 'left-only':\n            cmd.append('--left-only')\n        if not branch:\n            branch = self.get_active_branch()\n        cmd.append(branch)\n        # and trailing -- marker to make sure that Git never confused the branch\n        # with a potentially existing directory of the same name\n        cmd.append('--')\n        for r in self.call_git_items_(cmd):\n            if stop and stop == r:\n                return\n            yield r\n\n    def checkout(self, name, options=None):\n        \"\"\"\n        \"\"\"\n        # TODO: May be check for the need of -b options herein?\n        cmd = ['checkout']\n        if options:\n            cmd += options\n        cmd += [str(name)]\n\n        self.call_git(cmd, expect_stderr=True)\n        # checkout can change committed config, or create branch config\n        self.config.reload()\n\n    # TODO: Before implementing annex merge, find usages and check for a needed\n    # change to call super().merge\n    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):\n        if options is None:\n            options = []\n        if msg:\n            options = options + [\"-m\", msg]\n        options += ['--allow-unrelated-histories']\n        self.call_git(\n            ['merge'] + options + [name],\n            **kwargs\n        )\n\n    def remove_branch(self, branch):\n        self.call_git(['branch', '-D', branch])\n\n    def cherry_pick(self, commit):\n        \"\"\"Cherry pick `commit` to the current branch.\n\n        Parameters\n        ----------\n        commit : str\n            A single commit.\n        \"\"\"\n        self.call_git([\"cherry-pick\", commit])\n\n    @property\n    def dirty(self):\n        \"\"\"Is the repository dirty?\n\n        Note: This provides a quick answer when you simply want to know if\n        there are any untracked changes or modifications in this repository or\n        its submodules. For finer-grained control and more detailed reporting,\n        use status() instead.\n        \"\"\"\n        stdout = self.call_git(\n            [\"status\", \"--porcelain\",\n             # Ensure the result isn't influenced by status.showUntrackedFiles.\n             \"--untracked-files=normal\",\n             # Ensure the result isn't influenced by diff.ignoreSubmodules.\n             \"--ignore-submodules=none\"])\n        if bool(stdout.strip()):\n            # The quick `git status`-based check can give a different answer\n            # than `datalad status` for submodules on an adjusted branch.\n            st = self.diffstatus(fr=\"HEAD\" if self.get_hexsha() else None,\n                                 to=None, untracked=\"normal\")\n            return any(r.get(\"state\") != \"clean\" for r in st.values())\n        return False\n\n    @property\n    def untracked_files(self):\n        \"\"\"Legacy interface, do not use! Use the status() method instead.\n\n        Despite its name, it also reports on untracked datasets, and\n        yields their names with trailing path separators.\n        \"\"\"\n        return [\n            '{}{}'.format(\n                str(p.relative_to(self.pathobj)),\n                os.sep if props['type'] != 'file' else ''\n            )\n            for p, props in self.status(\n                    untracked='all', eval_submodule_state='no').items()\n            if props.get('state', None) == 'untracked'\n        ]\n\n    def gc(self, allow_background=False, auto=False):\n        \"\"\"Perform house keeping (garbage collection, repacking)\"\"\"\n        cmd_options = []\n        if not allow_background:\n            cmd_options += ['-c', 'gc.autodetach=0']\n        cmd_options += ['gc', '--aggressive']\n        if auto:\n            cmd_options += ['--auto']\n        self.call_git(cmd_options)\n\n    def _parse_gitmodules(self):\n        # TODO read .gitconfig from Git blob?\n        gitmodules = self.pathobj / '.gitmodules'\n        if not gitmodules.exists():\n            return {}\n        # pull out file content\n        out = self.call_git(\n            ['config', '-z', '-l', '--file', '.gitmodules'],\n            read_only=True)\n        # abuse our config parser\n        # disable multi-value report, because we could not deal with them\n        # anyways, and they should not appear in a normal .gitmodules file\n        # but could easily appear when duplicates are included. In this case,\n        # we better not crash\n        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)\n        mods = {}\n        for k, v in db.items():\n            if not k.startswith('submodule.'):\n                # we don't know what this is\n                lgr.warning(\"Skip unrecognized .gitmodule specification: %s=%s\", k, v)\n                continue\n            k_l = k.split('.')\n            # module name is everything after 'submodule.' that is not the variable\n            # name\n            mod_name = '.'.join(k_l[1:-1])\n            mod = mods.get(mod_name, {})\n            # variable name is the last 'dot-free' segment in the key\n            mod[k_l[-1]] = v\n            mods[mod_name] = mod\n\n        out = {}\n        # bring into traditional shape\n        for name, props in mods.items():\n            if 'path' not in props:\n                lgr.warning(\"Failed to get '%s.path', skipping this submodule\", name)\n                continue\n            modprops = {'gitmodule_{}'.format(k): v\n                        for k, v in props.items()\n                        if not k.startswith('__')}\n            # Keep as PurePosixPath for possible normalization of / in the path etc\n            modpath = PurePosixPath(props['path'])\n            modprops['gitmodule_name'] = name\n            out[modpath] = modprops\n        return out\n\n    def get_submodules_(self, paths=None):\n        \"\"\"Yield submodules in this repository.\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`. Paths must be relative\n            to the resolved repository root, and must be normed to match the\n            reporting done by Git, i.e. no parent dir components\n            (ala \"some/../this\").\n\n        Returns\n        -------\n        A generator that yields a dictionary with information for each\n        submodule.\n        \"\"\"\n        if not (self.pathobj / \".gitmodules\").exists():\n            return\n\n        modinfo = self._parse_gitmodules()\n        if not modinfo:\n            # we exit early, if there is nothing on record (even though\n            # a .gitmodules file exists).\n            # without this conditional exit, we would be able to discover\n            # subprojects even when they are not recorded in .gitmodules,\n            # but at the cost of running an unconstrained ls-files call\n            # below\n            return\n\n        posix_mod_paths = [m.as_posix() for m in modinfo]\n        if paths:\n            # harmonize them into relative to the repository\n            posix_paths = []\n            for path in paths:\n                path = ut.PurePath(path)\n                if path.is_absolute():\n                    try:\n                        path = path.relative_to(self.pathobj)\n                    except ValueError as exc:\n                        lgr.debug(\n                            \"Path %s it not underneath %s, skipping since nothing should match it: %s\",\n                            path, self.pathobj, CapturedException(exc)\n                        )\n                        continue\n                posix_paths.append(path.as_posix())\n\n            # constrain the report by the given paths, make sure all paths are POSIX\n            posix_mod_paths = list(get_filtered_paths_(\n                posix_mod_paths,\n                posix_paths,\n                include_within_path=True,\n            ))\n\n        for r in self.call_git_items_(\n            ['ls-files', '--stage', '-z'],\n            sep='\\0',\n            files=posix_mod_paths,\n            read_only=True,\n            keep_ends=True,\n        ):\n            if not r.startswith('160000'):\n                # make sure this method never talks about non-dataset\n                # content\n                continue\n            props, rpath = r.split('\\t')\n            mode, gitsha, stage = props.split(' ')\n            if stage not in ('0', '2'):\n                # we either have non-merge situation, or a simple merge\n                # situation (i.e. stage=0). the reported gitsha always\n                # matches what we have locally.\n                # or we are in a three-way merge, in which case stage=2\n                # is what we want to report, because it matches the\n                # current HEAD (see git-read-tree manpage).\n                # there is either a stage 2 or stage 0, never both\n                continue\n            # remove the expected line separator from the path\n            rpath = rpath[:-1]\n            path = PurePosixPath(rpath)\n            yield dict(\n                path=self.pathobj / rpath, # full path returned here\n                type='dataset',\n                gitshasum=gitsha,\n                **modinfo.get(path, {})\n            )\n\n    def get_submodules(self, sorted_=True, paths=None):\n        \"\"\"Return list of submodules.\n\n        Parameters\n        ----------\n        sorted_ : bool, optional\n            Sort submodules by path name.\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`.\n\n        Returns\n        -------\n        List of submodule namedtuples if `compat` is true or otherwise a list\n        of dictionaries as returned by `get_submodules_`.\n        \"\"\"\n        xs = self.get_submodules_(paths=paths)\n\n        if sorted_:\n            xs = sorted(xs, key=lambda x: x[\"path\"])\n        return list(xs)\n\n    def update_ref(self, ref, value, oldvalue=None, symbolic=False):\n        \"\"\"Update the object name stored in a ref \"safely\".\n\n        Just a shim for `git update-ref` call if not symbolic, and\n        `git symbolic-ref` if symbolic\n\n        Parameters\n        ----------\n        ref : str\n          Reference, such as `ref/heads/BRANCHNAME` or HEAD.\n        value : str\n          Value to update to, e.g. hexsha of a commit when updating for a\n          branch ref, or branch ref if updating HEAD\n        oldvalue: str\n          Value to update from. Safeguard to be verified by git. This is only\n          valid if `symbolic` is not True.\n        symbolic : None\n          To instruct if ref is symbolic, e.g. should be used in case of\n          ref=HEAD\n        \"\"\"\n        if symbolic:\n            if oldvalue:\n                raise ValueError(\"oldvalue and symbolic must not be given both\")\n            cmd = ['symbolic-ref', ref, value]\n        else:\n            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])\n        self.call_git(cmd)\n\n    def tag(self, tag, message=None, commit=None, options=None):\n        \"\"\"Tag a commit\n\n        Parameters\n        ----------\n        tag : str\n          Custom tag label. Must be a valid tag name.\n        message : str, optional\n          If provided, adds ['-m', <message>] to the list of `git tag`\n          arguments.\n        commit : str, optional\n          If provided, will be appended as last argument to the `git tag` call,\n          and can be used to identify the commit that shall be tagged, if\n          not HEAD.\n        options : list, optional\n          Additional command options, inserted prior a potential `commit`\n          argument.\n        \"\"\"\n        # TODO: call in save.py complains about extensive logging. When does it\n        # happen in what way? Figure out, whether to just silence it or raise or\n        # whatever else.\n        args = ['tag']\n        if message:\n            args += ['-m', message]\n        if options is not None:\n            args.extend(options)\n        args.append(tag)\n        if commit:\n            args.append(commit)\n        self.call_git(args)\n\n    def get_tags(self, output=None):\n        \"\"\"Get list of tags\n\n        Parameters\n        ----------\n        output : str, optional\n          If given, limit the return value to a list of values matching that\n          particular key of the tag properties.\n\n        Returns\n        -------\n        list\n          Each item is a dictionary with information on a tag. At present\n          this includes 'hexsha', and 'name', where the latter is the string\n          label of the tag, and the former the hexsha of the object the tag\n          is attached to. The list is sorted by the creator date (committer\n          date for lightweight tags and tagger date for annotated tags), with\n          the most recent commit being the last element.\n        \"\"\"\n        tags = [\n            dict(\n                name=t['refname:strip=2'],\n                hexsha=t['object'] if t['object'] else t['objectname'],\n            )\n            for t in self.for_each_ref_(\n                fields=['refname:strip=2', 'objectname', 'object'],\n                pattern='refs/tags',\n                sort='creatordate')\n        ]\n        if output:\n            return [t[output] for t in tags]\n        else:\n            return tags\n\n    def describe(self, commitish=None, **kwargs):\n        \"\"\" Quick and dirty implementation to call git-describe\n\n        Parameters\n        ----------\n        kwargs:\n            transformed to cmdline options for git-describe;\n            see __init__ for description of the transformation\n        \"\"\"\n        # TODO: be more precise what failure to expect when and raise actual\n        # errors\n        cmd = ['describe'] + to_options(**kwargs)\n        if commitish is not None:\n            cmd.append(commitish)\n        try:\n            describe = self.call_git(cmd, expect_fail=True)\n            return describe.strip()\n        # TODO: WTF \"catch everything\"?\n        except:\n            return None\n\n    def get_tracking_branch(self, branch=None, remote_only=False):\n        \"\"\"Get the tracking branch for `branch` if there is any.\n\n        Parameters\n        ----------\n        branch: str\n            local branch to look up. If none is given, active branch is used.\n        remote_only : bool\n            Don't return a value if the upstream remote is set to \".\" (meaning\n            this repository).\n\n        Returns\n        -------\n        tuple\n            (remote or None, refspec or None) of the tracking branch\n        \"\"\"\n        if branch is None:\n            branch = self.get_corresponding_branch() or self.get_active_branch()\n            if branch is None:\n                return None, None\n\n        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)\n        if remote_only and track_remote == \".\":\n            return None, None\n        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)\n        return track_remote, track_branch\n\n    @property\n    def count_objects(self):\n        \"\"\"return dictionary with count, size(in KiB) information of git objects\n        \"\"\"\n\n        count_cmd = ['count-objects', '-v']\n        count_str = self.call_git(count_cmd)\n        count = {key: int(value)\n                 for key, value in [item.split(': ')\n                                    for item in count_str.split('\\n')\n                                    if len(item.split(': ')) == 2]}\n        return count\n\n    def get_git_attributes(self):\n        \"\"\"Query gitattributes which apply to top level directory\n\n        It is a thin compatibility/shortcut wrapper around more versatile\n        get_gitattributes which operates on a list of paths and returns\n        a dictionary per each path\n\n        Returns\n        -------\n        dict:\n          a dictionary with attribute name and value items relevant for the\n          top ('.') directory of the repository, and thus most likely the\n          default ones (if not overwritten with more rules) for all files within\n          repo.\n        \"\"\"\n        return self.get_gitattributes('.')['.']\n\n    def get_gitattributes(self, path, index_only=False):\n        \"\"\"Query gitattributes for one or more paths\n\n        Parameters\n        ----------\n        path: path or list\n          Path(s) to query. Paths may be relative or absolute.\n        index_only: bool\n          Flag whether to consider only gitattribute setting that are reflected\n          in the repository index, not just in the work tree content.\n\n        Returns\n        -------\n        dict:\n          Each key is a queried path (always relative to the repository root),\n          each value is a dictionary with attribute\n          name and value items. Attribute values are either True or False,\n          for set and unset attributes, or are the literal attribute value.\n        \"\"\"\n        path = ensure_list(path)\n        cmd = [\"check-attr\", \"-z\", \"--all\"]\n        if index_only:\n            cmd.append('--cached')\n        # make sure we have one entry for each query path to\n        # simplify work with the result\n        attributes = {p: {} for p in path}\n        attr = []\n        for item in self.call_git_items_(cmd, files=path, sep='\\0',\n                                         read_only=True):\n            attr.append(item)\n            if len(attr) < 3:\n                continue\n            # we have a full record\n            p, name, value = attr\n            attrs = attributes[p]\n            attrs[name] = \\\n                True if value == 'set' else False if value == 'unset' else value\n            # done, reset item\n            attr = []\n        return {relpath(k, self.path) if isabs(k) else k: v\n                for k, v in attributes.items()}\n\n    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):\n        \"\"\"Set gitattributes\n\n        By default appends additional lines to `attrfile`. Note, that later\n        lines in `attrfile` overrule earlier ones, which may or may not be\n        what you want. Set `mode` to 'w' to replace the entire file by\n        what you provided in `attrs`.\n\n        Parameters\n        ----------\n        attrs : list\n          Each item is a 2-tuple, where the first element is a path pattern,\n          and the second element is a dictionary with attribute key/value\n          pairs. The attribute dictionary must use the same semantics as those\n          returned by `get_gitattributes()`. Path patterns can use absolute paths,\n          in which case they will be normalized relative to the directory\n          that contains the target .gitattributes file (see `attrfile`).\n        attrfile: path\n          Path relative to the repository root of the .gitattributes file the\n          attributes shall be set in.\n        mode: str\n          'a' to append .gitattributes, 'w' to replace it\n        \"\"\"\n\n        git_attributes_file = op.join(self.path, attrfile)\n        attrdir = op.dirname(git_attributes_file)\n        if not op.exists(attrdir):\n            os.makedirs(attrdir)\n\n        with open(git_attributes_file, mode + '+') as f:\n            # for append, fix existing files that do not end with \\n\n            if mode == 'a' and f.tell():\n                f.seek(max(0, f.tell() - len(os.linesep)))\n                if not f.read().endswith('\\n'):\n                    f.write('\\n')\n\n            for pattern, attr in sorted(attrs, key=lambda x: x[0]):\n                # normalize the pattern relative to the target .gitattributes file\n                npath = _normalize_path(\n                    op.join(self.path, op.dirname(attrfile)), pattern)\n                # paths in gitattributes always have to be POSIX\n                npath = Path(npath).as_posix()\n                attrline = u''\n                if npath.count(' '):\n                    # quote patterns with spaces\n                    attrline += u'\"{}\"'.format(npath.replace('\"', '\\\\\"'))\n                else:\n                    attrline += npath\n                for a in sorted(attr):\n                    val = attr[a]\n                    if val is True:\n                        attrline += ' {}'.format(a)\n                    elif val is False:\n                        attrline += ' -{}'.format(a)\n                    else:\n                        attrline += ' {}={}'.format(a, val)\n                f.write('{}\\n'.format(attrline))\n\n    def get_content_info(self, paths=None, ref=None, untracked='all'):\n        \"\"\"Get identifier and type information from repository content.\n\n        This is simplified front-end for `git ls-files/tree`.\n\n        Both commands differ in their behavior when queried about subdataset\n        paths. ls-files will not report anything, ls-tree will report on the\n        subdataset record. This function uniformly follows the behavior of\n        ls-tree (report on the respective subdataset mount).\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath) or None\n          Specific paths, relative to the resolved repository root, to query\n          info for. Paths must be normed to match the reporting done by Git,\n          i.e. no parent dir components (ala \"some/../this\").\n          If `None`, info is reported for all content.\n        ref : gitref or None\n          If given, content information is retrieved for this Git reference\n          (via ls-tree), otherwise content information is produced for the\n          present work tree (via ls-files). With a given reference, the\n          reported content properties also contain a 'bytesize' record,\n          stating the size of a file in bytes.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when no `ref` was given:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n\n          `gitshasum`\n            SHASUM of the item as tracked by Git, or None, if not\n            tracked. This could be different from the SHASUM of the file\n            in the worktree, if it was modified.\n\n        Raises\n        ------\n        ValueError\n          In case of an invalid Git reference (e.g. 'HEAD' in an empty\n          repository)\n        \"\"\"\n        lgr.debug('%s.get_content_info(...)', self)\n        # TODO limit by file type to replace code in subdatasets command\n        info = dict()\n\n        if paths:  # is not None separate after\n            # path matching will happen against what Git reports\n            # and Git always reports POSIX paths\n            # any incoming path has to be relative already, so we can simply\n            # convert unconditionally\n            # note: will be list-ified below\n            posix_paths = [ut.PurePath(p).as_posix() for p in paths]\n        elif paths is not None:\n            return info\n        else:\n            posix_paths = None\n\n        if posix_paths and (not ref or external_versions[\"cmd:git\"] >= \"2.29.0\"):\n            # If a path points within a submodule, we need to map it to the\n            # containing submodule before feeding it to ls-files or ls-tree.\n            #\n            # Before Git 2.29.0, ls-tree and ls-files differed in how they\n            # reported paths within submodules: ls-files provided no output,\n            # and ls-tree listed the submodule. Now they both return no output.\n            submodules = [s[\"path\"].relative_to(self.pathobj).as_posix()\n                          for s in self.get_submodules_()]\n            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well\n            posix_paths = get_parent_paths(posix_paths, submodules)\n\n        # this will not work in direct mode, but everything else should be\n        # just fine\n        if not ref:\n            # make sure no operations are pending before we figure things\n            # out in the worktree\n            self.precommit()\n\n            # --exclude-standard will make sure to honor and standard way\n            # git can be instructed to ignore content, and will prevent\n            # crap from contaminating untracked file reports\n            cmd = ['ls-files', '--stage', '-z']\n            # untracked report mode, using labels from `git diff` option style\n            if untracked == 'all':\n                cmd += ['--exclude-standard', '-o']\n            elif untracked == 'normal':\n                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']\n            elif untracked == 'no':\n                pass\n            else:\n                raise ValueError(\n                    'unknown value for `untracked`: {}'.format(untracked))\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\\t(?P<fname>.*)$')\n        else:\n            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\\s]*(?P<size>[0-9-]+)\\t(?P<fname>.*)$')\n\n        lgr.debug('Query repo: %s', cmd)\n        try:\n            stdout = self.call_git(\n                cmd,\n                files=posix_paths,\n                expect_fail=True,\n                read_only=True)\n        except CommandError as exc:\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                raise InvalidGitReferenceError(ref)\n            raise\n        lgr.debug('Done query repo: %s', cmd)\n\n        self._get_content_info_line_helper(\n            ref,\n            info,\n            stdout.split('\\0'),\n            props_re)\n\n        lgr.debug('Done %s.get_content_info(...)', self)\n        return info\n\n    def _get_content_info_line_helper(self, ref, info, lines, props_re):\n        \"\"\"Internal helper of get_content_info() to parse Git output\"\"\"\n        mode_type_map = {\n            '100644': 'file',\n            '100755': 'file',\n            '120000': 'symlink',\n            '160000': 'dataset',\n        }\n        for line in lines:\n            if not line:\n                continue\n            inf = {}\n            props = props_re.match(line)\n            if not props:\n                # Kludge: Filter out paths starting with .git/ to work around\n                # an `ls-files -o` bug that was fixed in Git 2.25.\n                #\n                # TODO: Drop this condition when GIT_MIN_VERSION is at least\n                # 2.25.\n                if line.startswith(\".git/\"):\n                    lgr.debug(\"Filtering out .git/ file: %s\", line)\n                    continue\n                # not known to Git, but Git always reports POSIX\n                path = ut.PurePosixPath(line)\n                inf['gitshasum'] = None\n            else:\n                # again Git reports always in POSIX\n                path = ut.PurePosixPath(props.group('fname'))\n\n            # revisit the file props after this path has not been rejected\n            if props:\n                inf['gitshasum'] = props.group('sha')\n                inf['type'] = mode_type_map.get(\n                    props.group('type'), props.group('type'))\n\n                if ref and inf['type'] == 'file':\n                    inf['bytesize'] = int(props.group('size'))\n\n            # join item path with repo path to get a universally useful\n            # path representation with auto-conversion and tons of other\n            # stuff\n            path = self.pathobj.joinpath(path)\n            if 'type' not in inf:\n                # be nice and assign types for untracked content\n                inf['type'] = 'symlink' if path.is_symlink() \\\n                    else 'directory' if path.is_dir() else 'file'\n            info[path] = inf\n\n    def status(self, paths=None, untracked='all', eval_submodule_state='full'):\n        \"\"\"Simplified `git status` equivalent.\n\n        Parameters\n        ----------\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list. If a query path points\n          into a subdataset, a report is made on the subdataset record\n          within the queried dataset only (no recursion).\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        lgr.debug('Query status of %r for %s paths',\n                  self, len(paths) if paths is not None else 'all')\n        return self.diffstatus(\n            fr='HEAD' if self.get_hexsha() else None,\n            to=None,\n            paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state)\n\n    def diff(self, fr, to, paths=None, untracked='all',\n             eval_submodule_state='full'):\n        \"\"\"Like status(), but reports changes between to arbitrary revisions\n\n        Parameters\n        ----------\n        fr : str or None\n          Revision specification (anything that Git understands). Passing\n          `None` considers anything in the target state as new.\n        to : str or None\n          Revision specification (anything that Git understands), or None\n          to compare to the state of the work tree.\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when `to` is None:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        return {k: v for k, v in self.diffstatus(\n            fr=fr, to=to, paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state).items()\n            if v.get('state', None) != 'clean'}\n\n    def diffstatus(self, fr, to, paths=None, untracked='all',\n                   eval_submodule_state='full', _cache=None):\n        \"\"\"Like diff(), but reports the status of 'clean' content too.\n\n        It supports an additional submodule evaluation state 'global'.\n        If given, it will return a single 'modified'\n        (vs. 'clean') state label for the entire repository, as soon as\n        it can.\n        \"\"\"\n        def _get_cache_key(label, paths, ref, untracked=None):\n            return self.path, label, tuple(paths) if paths else None, \\\n                ref, untracked\n\n        if _cache is None:\n            _cache = {}\n\n        if paths is not None:\n            # at this point we must normalize paths to the form that\n            # Git would report them, to easy matching later on\n            paths = map(ut.Path, paths)\n            paths = [\n                p.relative_to(self.pathobj) if p.is_absolute() else p\n                for p in paths\n            ]\n\n        # TODO report more info from get_content_info() calls in return\n        # value, those are cheap and possibly useful to a consumer\n        # we need (at most) three calls to git\n        if to is None:\n            # everything we know about the worktree, including os.stat\n            # for each file\n            key = _get_cache_key('ci', paths, None, untracked)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(\n                    paths=paths, ref=None, untracked=untracked)\n                _cache[key] = to_state\n            # we want Git to tell us what it considers modified and avoid\n            # reimplementing logic ourselves\n            key = _get_cache_key('mod', paths, None)\n            if key in _cache:\n                modified = _cache[key]\n            else:\n                # from Git 2.31.0 onwards ls-files has --deduplicate\n                # by for backward compatibility keep doing deduplication here\n                modified = set(\n                    self.pathobj.joinpath(ut.PurePosixPath(p))\n                    for p in self.call_git_items_(\n                        # we must also look for deleted files, for the logic\n                        # below to work. Only from Git 2.31.0 would they be\n                        # included with `-m` alone\n                        ['ls-files', '-z', '-m', '-d'],\n                        # low-level code cannot handle pathobjs\n                        files=[str(p) for p in paths] if paths is not None else None,\n                        sep='\\0',\n                        read_only=True)\n                    if p)\n                _cache[key] = modified\n        else:\n            key = _get_cache_key('ci', paths, to)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(paths=paths, ref=to)\n                _cache[key] = to_state\n            # we do not need worktree modification detection in this case\n            modified = None\n        # origin state\n        key = _get_cache_key('ci', paths, fr)\n        if key in _cache:\n            from_state = _cache[key]\n        else:\n            if fr:\n                from_state = self.get_content_info(paths=paths, ref=fr)\n            else:\n                # no ref means from nothing\n                from_state = {}\n            _cache[key] = from_state\n\n        status = dict()\n        for f, to_state_r in to_state.items():\n            props = self._diffstatus_get_state_props(\n                f,\n                from_state.get(f, None),\n                to_state_r,\n                # are we comparing against a recorded commit or the worktree\n                to is not None,\n                # if we have worktree modification info, report if\n                # path is reported as modified in it\n                modified and f in modified,\n                eval_submodule_state)\n            # potential early exit in \"global\" eval mode\n            if eval_submodule_state == 'global' and \\\n                    props.get('state', None) not in ('clean', None):\n                # any modification means globally 'modified'\n                return 'modified'\n            status[f] = props\n\n        for f, from_state_r in from_state.items():\n            if f not in to_state:\n                # we new this, but now it is gone and Git is not complaining\n                # about it being missing -> properly deleted and deletion\n                # stages\n                status[f] = dict(\n                    state='deleted',\n                    type=from_state_r['type'],\n                    # report the shasum to distinguish from a plainly vanished\n                    # file\n                    gitshasum=from_state_r['gitshasum'],\n                )\n                if eval_submodule_state == 'global':\n                    return 'modified'\n\n        if to is not None or eval_submodule_state == 'no':\n            # if we have `to` we are specifically comparing against\n            # a recorded state, and this function only attempts\n            # to label the state of a subdataset, not investigate\n            # specifically what the changes in subdatasets are\n            # this is done by a high-level command like rev-diff\n            # so the comparison within this repo and the present\n            # `state` label are all we need, and they are done already\n            if eval_submodule_state == 'global':\n                return 'clean'\n            else:\n                return status\n\n        # loop over all subdatasets and look for additional modifications\n        for f, st in status.items():\n            f = str(f)\n            if 'state' in st or not st['type'] == 'dataset':\n                # no business here\n                continue\n            if not GitRepo.is_valid_repo(f):\n                # submodule is not present, no chance for a conflict\n                st['state'] = 'clean'\n                continue\n            # we have to recurse into the dataset and get its status\n            subrepo = repo_from_path(f)\n            # get the HEAD commit, or the one of the corresponding branch\n            # only that one counts re super-sub relationship\n            # save() syncs the corresponding branch each time\n            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())\n            st['gitshasum'] = subrepo_commit\n            # subdataset records must be labeled clean up to this point\n            # test if current commit in subdataset deviates from what is\n            # recorded in the dataset\n            st['state'] = 'modified' \\\n                if st['prev_gitshasum'] != subrepo_commit \\\n                else 'clean'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n            if eval_submodule_state == 'commit':\n                continue\n            # the recorded commit did not change, so we need to make\n            # a more expensive traversal\n            st['state'] = subrepo.diffstatus(\n                # we can use 'HEAD' because we know that the commit\n                # did not change. using 'HEAD' will facilitate\n                # caching the result\n                fr='HEAD',\n                to=None,\n                paths=None,\n                untracked=untracked,\n                eval_submodule_state='global',\n                _cache=_cache) if st['state'] == 'clean' else 'modified'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n\n        if eval_submodule_state == 'global':\n            return 'clean'\n        else:\n            return status\n\n    def _diffstatus_get_state_props(self, f, from_state, to_state,\n                                    against_commit,\n                                    modified_in_worktree,\n                                    eval_submodule_state):\n        \"\"\"Helper to determine diff properties for a single path\n\n        Parameters\n        ----------\n        f : Path\n        from_state : dict\n        to_state : dict\n        against_commit : bool\n          Flag whether `to_state` reflects a commit or the worktree.\n        modified_in_worktree : bool\n          Flag whether a worktree modification is reported. This is ignored\n          when `against_commit` is True.\n        eval_submodule_state : {'commit', 'no', ...}\n        \"\"\"\n        if against_commit:\n            # we can ignore any worktree modification reported when\n            # comparing against a commit\n            modified_in_worktree = False\n\n        props = {}\n        if 'type' in to_state:\n            props['type'] = to_state['type']\n\n        to_sha = to_state['gitshasum']\n        from_sha = from_state['gitshasum'] if from_state else None\n\n        # determine the state of `f` from from_state and to_state records, if\n        # it can be determined conclusively from it. If not, it will\n        # stay None for now\n        state = None\n        if not from_state:\n            # this is new, or rather not known to the previous state\n            state = 'added' if to_sha else 'untracked'\n        elif to_sha == from_sha and not modified_in_worktree:\n            # something that is seemingly unmodified, based on the info\n            # gathered so far\n            if to_state['type'] == 'dataset':\n                if against_commit or eval_submodule_state == 'commit':\n                    # we compare against a recorded state, just based on\n                    # the shas we can be confident, otherwise the state\n                    # of a subdataset isn't fully known yet, because\n                    # `modified_in_worktree` will only reflect changes\n                    # in the commit of a subdataset without looking into\n                    # it for uncommitted changes. Such tests are done\n                    # later and based on further conditionals for\n                    # performance reasons\n                    state = 'clean'\n            else:\n                # no change in git record, and no change on disk\n                # at this point we know that the reported object ids\n                # for this file are identical in the to and from\n                # records.  If to is None, we're comparing to the\n                # working tree and a deleted file will still have an\n                # identical id, so we need to check whether the file is\n                # gone before declaring it clean. This working tree\n                # check is irrelevant and wrong if to is a ref.\n                state = 'clean' \\\n                    if against_commit or (f.exists() or f.is_symlink()) \\\n                    else 'deleted'\n        else:\n            # change in git record, or on disk\n            # for subdatasets leave the 'modified' judgement to the caller\n            # for supporting corner cases, such as adjusted branch\n            # which require inspection of a subdataset\n            # TODO we could have a new file that is already staged\n            # but had subsequent modifications done to it that are\n            # unstaged. Such file would presently show up as 'added'\n            # ATM I think this is OK, but worth stating...\n            state = ('modified'\n                     if against_commit or to_state['type'] != 'dataset'\n                     else None\n                    ) if f.exists() or f.is_symlink() else 'deleted'\n            # TODO record before and after state for diff-like use\n            # cases\n\n        if state in ('clean', 'added', 'modified', None):\n            # assign present gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['gitshasum'] = to_sha\n            if 'bytesize' in to_state:\n                # if we got this cheap, report it\n                props['bytesize'] = to_state['bytesize']\n            elif state == 'clean' and 'bytesize' in from_state:\n                # no change, we can take this old size info\n                props['bytesize'] = from_state['bytesize']\n        if state in ('clean', 'modified', 'deleted', None):\n            # assign previous gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['prev_gitshasum'] = from_sha\n        if state:\n            # only report a state if we could determine any\n            # outside code tests for existence of the property\n            # and not (always) for the value\n            props['state'] = state\n        return props\n\n    def _save_pre(self, paths, _status, **kwargs):\n        # helper to get an actionable status report\n        if paths is not None and not paths and not _status:\n            return\n        if _status is None:\n            if 'untracked' not in kwargs:\n                kwargs['untracked'] = 'normal'\n            status = self.status(\n                paths=paths,\n                **{k: kwargs[k] for k in kwargs\n                   if k in ('untracked', 'eval_submodule_state')})\n        else:\n            # we want to be able to add items down the line\n            # make sure to detach from prev. owner\n            status = _status.copy()\n        return status\n\n    def get_staged_paths(self):\n        \"\"\"Returns a list of any stage repository path(s)\n\n        This is a rather fast call, as it will not depend on what is going on\n        in the worktree.\n        \"\"\"\n        try:\n            return list(self.call_git_items_(\n                ['diff', '--name-only', '--staged'],\n                expect_stderr=True))\n        except CommandError as e:\n            lgr.debug(CapturedException(e))\n            return []\n\n    def _save_post(self, message, files, partial_commit, amend=False,\n                   allow_empty=False):\n        # helper to commit changes reported in status\n\n        # TODO remove pathobj stringification when commit() can\n        # handle it\n        to_commit = [str(f.relative_to(self.pathobj))\n                     for f in files] \\\n                    if partial_commit else None\n        if not partial_commit or to_commit or allow_empty or \\\n                (amend and message):\n            # we directly call GitRepo.commit() to avoid a whole slew\n            # if direct-mode safeguards and workarounds in the AnnexRepo\n            # implementation (which also run an additional dry-run commit\n            GitRepo.commit(\n                self,\n                files=to_commit,\n                msg=message,\n                options=to_options(amend=amend, allow_empty=allow_empty),\n                # do not raise on empty commit\n                # it could be that the `add` in this save-cycle has already\n                # brought back a 'modified' file into a clean state\n                careless=True,\n            )\n\n    def save(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Save dataset content.\n\n        Parameters\n        ----------\n        message : str or None\n          A message to accompany the changeset in the log. If None,\n          a default message is used.\n        paths : list or None\n          Any content with path matching any of the paths given in this\n          list will be saved. Matching will be performed against the\n          dataset status (GitRepo.status()), or a custom status provided\n          via `_status`. If no paths are provided, ALL non-clean paths\n          present in the repo status or `_status` will be saved.\n        _status : dict or None\n          If None, Repo.status() will be queried for the given `ds`. If\n          a dict is given, its content will be used as a constraint.\n          For example, to save only modified content, but no untracked\n          content, set `paths` to None and provide a `_status` that has\n          no entries for untracked content.\n        **kwargs :\n          Additional arguments that are passed to underlying Repo methods.\n          Supported:\n\n          - git : bool (passed to Repo.add()\n          - eval_submodule_state : {'full', 'commit', 'no'}\n            passed to Repo.status()\n          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()\n          - amend : bool (passed to GitRepo.commit)\n        \"\"\"\n        return list(\n            self.save_(\n                message=message,\n                paths=paths,\n                _status=_status,\n                **kwargs\n            )\n        )\n\n    def save_(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Like `save()` but working as a generator.\"\"\"\n        from datalad.interface.results import get_status_dict\n\n        status_state = _get_save_status_state(\n            self._save_pre(paths, _status, **kwargs) or {}\n        )\n        amend = kwargs.get('amend', False)\n\n        # TODO: check on those None's -- may be those are also \"nothing to worry about\"\n        # and we could just return?\n        if not any(status_state.values()) and not (message and amend):\n            # all clean, nothing todo\n            lgr.debug('Nothing to save in %r, exiting early', self)\n            return\n\n        # three things are to be done:\n        # - remove (deleted if not already staged)\n        # - add (modified/untracked)\n        # - commit (with all paths that have been touched, to bypass\n        #   potential pre-staged bits)\n\n        staged_paths = self.get_staged_paths()\n        need_partial_commit = bool(staged_paths)\n        if need_partial_commit and hasattr(self, \"call_annex\"):\n            # so we have some staged content. let's check which ones\n            # are symlinks -- those could be annex key links that\n            # are broken after a `git-mv` operation\n            # https://github.com/datalad/datalad/issues/4967\n            # call `git-annex pre-commit` on them to rectify this before\n            # saving the wrong symlinks\n            added = status_state['added']\n            tofix = [\n                sp for sp in staged_paths\n                if added.get(self.pathobj / sp, {}).get(\"type\") == \"symlink\"\n            ]\n            if tofix:\n                self.call_annex(['pre-commit'], files=tofix)\n\n        submodule_change = False\n\n        if status_state['deleted']:\n            vanished_subds = [\n                str(f.relative_to(self.pathobj))\n                for f, props in status_state['deleted'].items()\n                if props.get('type') == 'dataset'\n            ]\n            if vanished_subds:\n                # we submodule removal we use `git-rm`, because the clean-up\n                # is more complex than just an index update -- make no\n                # sense to have a duplicate implementation.\n                # we do not yield here, but only altogether below -- we are just\n                # processing gone components, should always be quick.\n                self._call_git(['rm', '-q'], files=vanished_subds)\n                submodule_change = True\n            # remove anything from the index that was found to be gone\n            self._call_git(\n                ['update-index', '--remove'],\n                files=[\n                    str(f.relative_to(self.pathobj))\n                    for f, props in status_state['deleted'].items()\n                    # do not update the index, if there is already\n                    # something staged for this path (e.g.,\n                    # a directory was removed and a file staged\n                    # in its place)\n                    if not props.get('gitshasum')\n                    # we already did the submodules\n                    and props.get('type') != 'dataset'\n                ]\n            )\n            # now yield all deletions\n            for p, props in status_state['deleted'].items():\n                yield get_status_dict(\n                    action='delete',\n                    refds=self.pathobj,\n                    type=props.get('type'),\n                    path=p,\n                    status='ok',\n                    logger=lgr)\n\n        # TODO this additional query should not be, based on status as given\n        # if anyhow possible, however, when paths are given, status may\n        # not contain all required information. In case of path=None AND\n        # _status=None, we should be able to avoid this, because\n        # status should have the full info already\n        # looks for contained repositories\n        untracked_dirs = [\n            f.relative_to(self.pathobj)\n            for f, props in status_state['untracked'].items()\n            if props.get('type', None) == 'directory']\n        to_add_submodules = []\n        if untracked_dirs:\n            to_add_submodules = [\n                sm for sm, sm_props in\n                self.get_content_info(\n                    untracked_dirs,\n                    ref=None,\n                    # request exhaustive list, so that everything that is\n                    # still reported as a directory must be its own repository\n                    untracked='all').items()\n                if sm_props.get('type', None) == 'directory']\n            to_add_submodules = _prune_deeper_repos(to_add_submodules)\n\n        to_stage_submodules = {\n            f: props\n            for f, props in status_state['modified_or_untracked'].items()\n            if props.get('type', None) == 'dataset'}\n        if to_stage_submodules:\n            lgr.debug(\n                '%i submodule path(s) to stage in %r %s',\n                len(to_stage_submodules), self,\n                to_stage_submodules\n                if len(to_stage_submodules) < 10 else '')\n            to_add_submodules += list(to_stage_submodules)\n\n        if to_add_submodules:\n            for r in self._save_add_submodules(to_add_submodules):\n                if r.get('status', None) == 'ok':\n                    submodule_change = True\n                yield r\n\n        if submodule_change:\n            # this will alter the config, reload\n            self.config.reload()\n            # need to include .gitmodules in what needs committing\n            f = self.pathobj.joinpath('.gitmodules')\n            status_state['modified_or_untracked'][f] = \\\n                status_state['modified'][f] = \\\n                dict(type='file', state='modified')\n            # now stage .gitmodules\n            self._call_git(['update-index', '--add'], files=['.gitmodules'])\n            # and report on it\n            yield get_status_dict(\n                action='add',\n                refds=self.pathobj,\n                type='file',\n                path=f,\n                status='ok',\n                logger=lgr)\n\n        to_add = {\n            # TODO remove pathobj stringification when add() can\n            # handle it\n            str(f.relative_to(self.pathobj)): props\n            for f, props in status_state['modified_or_untracked'].items()\n            if not (f in to_add_submodules or f in to_stage_submodules)}\n        if to_add:\n            compat_config = \\\n                self.config.obtain(\"datalad.save.windows-compat-warning\")\n            to_add, problems = self._check_for_win_compat(to_add, compat_config)\n            lgr.debug(\n                '%i path(s) to add to %s %s',\n                len(to_add), self, to_add if len(to_add) < 10 else '')\n\n            if to_add:\n                yield from self._save_add(\n                        to_add,\n                        git_opts=None,\n                        **{k: kwargs[k] for k in kwargs\n                           if k in (('git',) if hasattr(self, 'uuid')\n                                    else tuple())})\n            if problems:\n                from datalad.interface.results import get_status_dict\n                msg = \\\n                    'Incompatible name for Windows systems; disable with ' \\\n                    'datalad.save.windows-compat-warning.',\n                for path in problems:\n                    yield get_status_dict(\n                        action='save',\n                        refds=self.pathobj,\n                        type='file',\n                        path=(self.pathobj / ut.PurePosixPath(path)),\n                        status='impossible',\n                        message=msg,\n                        logger=lgr)\n\n\n        # https://github.com/datalad/datalad/issues/6558\n        # file could have become a directory. Unfortunately git\n        # would then mistakenly refuse to commit if that old path is also\n        # given to commit, so we better filter it out\n        if status_state['deleted'] and status_state['added']:\n            # check if any \"deleted\" is a directory now. Then for those\n            # there should be some other path under that directory in 'added'\n            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:\n                # this could potentially be expensive if lots of files become\n                # directories, but it is unlikely to happen often\n                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish\n                # path_is_subpath faster, also if comparing to \"in f.parents\"\n                f_str = str(f)\n                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):\n                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post\n\n        # Note, that allow_empty is always ok when we amend. Required when we\n        # amend an empty commit while the amendment is empty, too (though\n        # possibly different message). If an empty commit was okay before, it's\n        # okay now.\n        status_state.pop('modified_or_untracked')  # pop the hybrid state\n        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,\n                        allow_empty=amend)\n        # TODO yield result for commit, prev helper checked hexsha pre\n        # and post...\n\n    def _check_for_win_compat(self, files, config):\n        \"\"\"Check file names for illegal characters or reserved names on Windows\n\n        In the case that a non-Windows-compatible file is detected, warn users\n        about potential interoperability issues.\n\n        Parameters\n        ----------\n        files\n          list of files to add\n        config\n          value of self.config.obtain(\"datalad.save.windows-compat-warning\"),\n          used to choose appropriate behavior. \"none\" performs no check,\n          \"warning\" warns in case of incompatibilities, and \"error\" results in\n          an error result in case of incompatibilities\n        \"\"\"\n        # don't perform any check when the configuration is set to 'none'\n        if config == 'none':\n            return files, None\n\n        from collections import defaultdict\n        problems = defaultdict(list)\n        for file in files:\n            for part in Path(file).parts:\n                # check every component of the path for incompatibilities\n                if Path(part).stem.upper() in RESERVED_NAMES_WIN:\n                    problems['Elements using a reserved filename:'].append(part)\n                    problems['paths'].append(file)\n                if re.search(ILLEGAL_CHARS_WIN, part):\n                    problems['Elements with illegal characters:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith('.'):\n                    problems['Elements ending with a dot:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith(' '):\n                    problems['Elements ending with a space:'].append(part)\n                    problems['paths'].append(file)\n        if not problems:\n            return files, None\n        msg = \\\n            \"Some elements of your dataset are not compatible with \" \\\n            \"Windows systems. Disable this check by changing \" \\\n            \"datalad.save.windows-compat-warning or consider renaming \" \\\n            \"the following elements: \"\n        for k, v in problems.items():\n            # use the key as an explanation, and report filenames only once\n            msg += f\"\\n{k} {[*{*v}]}\" if k != 'paths' else ''\n        if config == 'warning':\n            lgr.warning(msg)\n            return files, None\n\n        elif config == 'error':\n            # take the problematic files out of to_add\n            for path in [*{*problems['paths']}]:\n                files.pop(path)\n            return files, [*{*problems['paths']}]\n\n    def _save_add(self, files, git_opts=None):\n        \"\"\"Simple helper to add files in save()\"\"\"\n        from datalad.interface.results import get_status_dict\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.largefiles to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.largefiles=nothing', 'add'] +\n                ensure_list(git_opts) + ['--verbose'],\n                files=list(files.keys()),\n                pathspec_from_file=True,\n            )\n            # get all the entries\n            for r in self._process_git_get_output(*add_out):\n                yield get_status_dict(\n                    action=r.get('command', 'add'),\n                    refds=self.pathobj,\n                    type='file',\n                    path=(self.pathobj / ut.PurePosixPath(r['file']))\n                    if 'file' in r else None,\n                    status='ok' if r.get('success', None) else 'error',\n                    key=r.get('key', None),\n                    # while there is no git-annex underneath here, we\n                    # tend to fake its behavior, so we can also support\n                    # this type of messaging\n                    message='\\n'.join(r['error-messages'])\n                    if 'error-messages' in r else None,\n                    logger=lgr)\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n    def _save_add_submodules(self, paths):\n        \"\"\"Add new submodules, or updates records of existing ones\n\n        This method does not use `git submodule add`, but aims to be more\n        efficient by limiting the scope to mere in-place registration of\n        multiple already present repositories.\n\n        Parameters\n        ----------\n        paths : list(Path)\n\n        Yields\n        ------\n        dict\n          Result records\n        \"\"\"\n        from datalad.interface.results import get_status_dict\n\n        # first gather info from all datasets in read-only fashion, and then\n        # update index, .gitmodules and .git/config at once\n        info = []\n        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843\n        # We must not add already known submodules explicitly since \"untracked\"\n        # can be assigned even for known ones (TODO: add issue, might have been closed)?\n        # Not sure if operating on relative paths would provide any speed up so use full\n        known_sub_paths = {s['path'] for s in self.get_submodules_()}\n        for path in paths:\n            already_known = path in known_sub_paths\n            rpath = str(path.relative_to(self.pathobj).as_posix())\n            subm = repo_from_path(path)\n            # if there is a corresponding branch, we want to record it's state.\n            # we rely on the corresponding branch being synced already.\n            # `save` should do that each time it runs.\n            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())\n            if not subm_commit:\n                yield get_status_dict(\n                    action='add_submodule',\n                    ds=self,\n                    path=path,\n                    status='error',\n                    message=('cannot add subdataset %s with no commits', subm),\n                    logger=lgr)\n                continue\n            # make an attempt to configure a submodule source URL based on the\n            # discovered remote configuration\n            remote, branch = subm.get_tracking_branch()\n            url = subm.get_remote_url(remote) if remote else None\n            if url is None:\n                url = './{}'.format(rpath)\n            subm_id = subm.config.get('datalad.dataset.id', None)\n            info.append(\n                dict(\n                     # if we have additional information on this path, pass it on.\n                     # if not, treat it as an untracked directory\n                     paths[path] if isinstance(paths, dict)\n                     else dict(type='directory', state='untracked'),\n                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,\n                     url=url, known=already_known))\n\n        # bypass any convenience or safe-manipulator for speed reasons\n        # use case: saving many new subdatasets in a single run\n        with (self.pathobj / '.gitmodules').open('a') as gmf, \\\n             (self.pathobj / '.git' / 'config').open('a') as gcf:\n            for i in info:\n                # we update the subproject commit unconditionally\n                self.call_git([\n                    'update-index', '--add', '--replace', '--cacheinfo', '160000',\n                    i['commit'], i['rpath']\n                ])\n                # only write the .gitmodules/.config changes when this is not yet\n                # a subdataset and not yet already known\n                # TODO: we could update the URL, and branch info at this point,\n                # even for previously registered subdatasets\n                if not i['known'] and (i['type'] != 'dataset' or (\n                        i['type'] == 'dataset' and i['state'] == 'untracked')):\n                    gmprops = dict(path=i['rpath'], url=i['url'])\n                    if i['id']:\n                        gmprops['datalad-id'] = i['id']\n                    write_config_section(\n                        gmf, 'submodule', i['rpath'], gmprops)\n                    write_config_section(\n                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))\n\n                # This mirrors the result structure yielded for\n                # to_stage_submodules below.\n                yield get_status_dict(\n                    action='add',\n                    refds=self.pathobj,\n                    type='dataset',\n                    key=None,\n                    path=i['path'],\n                    status='ok',\n                    logger=lgr)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 776,
                            "end_lineno": 3765,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "d506f925df22667910adb647463856cbcee325c5^",
    "new_commit_hash": "d506f925df22667910adb647463856cbcee325c5",
    "commit_message": "Merge pull request #7312 from yarikoptic/bf-donot-allow-unrelated\n\nBF: GitRepo.merge do not allow merging unrelated unconditionally",
    "commit_date": "2023-03-17T14:33:32+01:00",
    "metadata": {}
}
{
    "repo_name": "datalad",
    "new_commit_hash": "d506f925df22667910adb647463856cbcee325c5",
    "test_file_codes": [
        "# ex: set sts=4 ts=4 sw=4 et:\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n#\n#   See COPYING file distributed along with the datalad package for the\n#   copyright and license terms.\n#\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n\"\"\"Test update action\n\n\"\"\"\n\nimport os\nimport os.path as op\nfrom os.path import exists\nfrom os.path import join as opj\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom datalad import cfg as dl_cfg\nfrom datalad.api import (\n    clone,\n    install,\n    remove,\n    update,\n)\nfrom datalad.distribution.update import _process_how_args\nfrom datalad.support.annexrepo import AnnexRepo\nfrom datalad.support.external_versions import external_versions\nfrom datalad.support.gitrepo import GitRepo\nfrom datalad.tests.utils_pytest import (\n    DEFAULT_BRANCH,\n    DEFAULT_REMOTE,\n    SkipTest,\n    assert_false,\n    assert_in,\n    assert_in_results,\n    assert_is_instance,\n    assert_not_in,\n    assert_raises,\n    assert_repo_status,\n    assert_result_count,\n    assert_status,\n    create_tree,\n    eq_,\n    known_failure_windows,\n    maybe_adjust_repo,\n    neq_,\n    ok_,\n    ok_file_has_content,\n    skip_if_adjusted_branch,\n    slow,\n    with_tempfile,\n)\nfrom datalad.utils import (\n    Path,\n    chpwd,\n    knows_annex,\n    rmtree,\n)\n\nfrom datalad.distribution.dataset import Dataset\n\n\n@slow\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_update_simple(origin=None, src_path=None, dst_path=None):\n    ca = dict(result_renderer='disabled')\n    # a remote dataset with a subdataset underneath\n    origds = Dataset(origin).create(**ca)\n    # naming is weird, but a legacy artifact\n    _ = origds.create('subm 1', **ca)\n    _ = origds.create('2', **ca)\n\n    # prepare src\n    source = install(src_path, source=origin, recursive=True)\n    # forget we cloned it by removing remote, which should lead to\n    # setting tracking branch to target:\n    source.repo.remove_remote(DEFAULT_REMOTE)\n    # also forget the declared absolute location of the submodules, and turn them\n    # relative to this/a clone\n    for sub in source.subdatasets(result_xfm=lambda x: x['gitmodule_name']):\n        source.subdatasets(path=sub, set_property=[('url', './{}'.format(sub))])\n\n    # dataset without sibling will not need updates\n    assert_status('notneeded', source.update())\n    # deprecation message doesn't ruin things\n    assert_status('notneeded', source.update(fetch_all=True))\n    # but error if unknown sibling is given\n    assert_status('impossible', source.update(sibling='funky', on_failure='ignore'))\n\n    # get a clone to update later on:\n    dest = install(dst_path, source=src_path, recursive=True)\n    # test setup done;\n    # assert all fine\n    assert_repo_status(dst_path)\n    assert_repo_status(src_path)\n\n    # update yields nothing => up-to-date\n    assert_status('ok', dest.update())\n    assert_repo_status(dst_path)\n\n    # modify remote:\n    with open(opj(src_path, \"update.txt\"), \"w\") as f:\n        f.write(\"Additional content\")\n    source.save(path=\"update.txt\", message=\"Added update.txt\")\n    assert_repo_status(src_path)\n\n    # update without `merge` only fetches:\n    assert_status('ok', dest.update())\n    # modification is not known to active branch:\n    assert_not_in(\"update.txt\",\n                  dest.repo.get_files(dest.repo.get_active_branch()))\n    # modification is known to branch <default remote>/<default branch>\n    assert_in(\"update.txt\",\n              dest.repo.get_files(DEFAULT_REMOTE + \"/\" + DEFAULT_BRANCH))\n\n    # merge:\n    assert_status('ok', dest.update(merge=True))\n    # modification is now known to active branch:\n    assert_in(\"update.txt\",\n              dest.repo.get_files(dest.repo.get_active_branch()))\n    # it's known to annex, but has no content yet:\n    annexprops = dest.repo.get_file_annexinfo(\"update.txt\",\n                                              eval_availability=True)\n    annexprops['key']  # blows if unknown\n    eq_(False, annexprops['has_content'])\n\n    # check subdataset path constraints, baseline (parent + 2 subds)\n    assert_result_count(dest.update(recursive=True),\n                        3, status='ok', type='dataset')\n    # no recursion and invalid path still updates the parent\n    res = dest.update(path='whatever')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # invalid path with recursion also does\n    res = dest.update(recursive=True, path='whatever')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # valid path and no recursion only updates the parent\n    res = dest.update(path='subm 1')\n    assert_result_count(res, 1, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    # valid path and recursion updates matching\n    res = dest.update(recursive=True, path='subm 1')\n    assert_result_count(res, 2, status='ok', type='dataset')\n    assert_result_count(res, 1, status='ok', path=dest.path)\n    assert_result_count(res, 1, status='ok', path=str(dest.pathobj / 'subm 1'))\n    # additional invalid path doesn't hurt\n    res = dest.update(recursive=True, path=['subm 1', 'mike'])\n    assert_result_count(res, 2, status='ok', type='dataset')\n    # full match\n    res = dest.update(recursive=True, path=['subm 1', '2'])\n    assert_result_count(res, 3, status='ok', type='dataset')\n\n    # test that update doesn't crash if we specify only a single path (submod) to\n    # operate on\n    with chpwd(dest.path):\n        # in 0.11.x it would be a single result since \"pwd\" dataset is not\n        # considered, and would be relative path (as specified).\n        # In 0.12.0 - it would include implicit pwd dataset, and paths would be absolute\n        res_update = update(path=['subm 1'], recursive=True)\n        assert_result_count(res_update, 2)\n        for p in dest.path, str(dest.pathobj / 'subm 1'):\n            assert_in_results(res_update, path=p, action='update', status='ok', type='dataset')\n\n        # and with merge we would also try to save (but there would be no changes)\n        res_merge = update(path=['subm 1'], recursive=True, merge=True)\n        assert_result_count(res_merge, 2, action='update')\n        # 2 of \"updates\" really.\n        assert_in_results(res_merge, action='update', status='ok', type='dataset')\n        assert_in_results(res_merge, action='save', status='notneeded', type='dataset')\n\n    # smoke-test if recursive update doesn't fail if submodule is removed\n    # and that we can run it from within a dataset without providing it\n    # explicitly\n    assert_result_count(\n        dest.remove('subm 1'), 1,\n        status='ok', action='remove', path=opj(dest.path, 'subm 1'))\n    with chpwd(dest.path):\n        assert_result_count(\n            update(recursive=True), 2,\n            status='ok', type='dataset')\n    assert_result_count(\n        dest.update(merge=True, recursive=True), 2,\n        action='update', status='ok', type='dataset')\n\n    # and now test recursive update with merging in differences\n    create_tree(opj(source.path, '2'), {'load.dat': 'heavy'})\n    source.save(opj('2', 'load.dat'),\n                message=\"saving changes within subm2\",\n                recursive=True)\n    assert_result_count(\n        dest.update(merge=True, recursive=True), 2,\n        action='update', status='ok', type='dataset')\n    # and now we can get new file\n    dest.get(opj('2', 'load.dat'))\n    ok_file_has_content(opj(dest.path, '2', 'load.dat'), 'heavy')\n\n\n@with_tempfile\n@with_tempfile\ndef test_update_git_smoke(src_path=None, dst_path=None):\n    # Apparently was just failing on git repos for basic lack of coverage, hence this quick test\n    ds = Dataset(src_path).create(annex=False)\n    target = install(\n        dst_path, source=src_path,\n        result_xfm='datasets', return_type='item-or-list')\n    create_tree(ds.path, {'file.dat': '123'})\n    ds.save('file.dat')\n    assert_result_count(\n        target.update(recursive=True, merge=True), 1,\n        action='update', status='ok', type='dataset')\n    ok_file_has_content(opj(target.path, 'file.dat'), '123')\n\n\n@slow  # ~9s\n@with_tempfile(mkdir=True)\ndef test_update_fetch_all(path=None):\n    path = Path(path)\n    remote_1 = str(path / \"remote_1\")\n    remote_2 = str(path / \"remote_2\")\n\n    ds = Dataset(path / \"src\").create()\n    src = ds.repo.path\n\n    ds_rmt1 = clone(source=src, path=remote_1)\n    ds_rmt2 = clone(source=src, path=remote_2)\n\n    ds.siblings('add', name=\"sibling_1\", url=remote_1)\n    ds.siblings('add', name=\"sibling_2\", url=remote_2)\n\n    # modify the remotes:\n    (ds_rmt1.pathobj / \"first.txt\").write_text(\"some file load\")\n    ds_rmt1.save()\n\n    # TODO: Modify an already present file!\n\n    (ds_rmt2.pathobj / \"second.txt\").write_text(\"different file load\")\n    ds_rmt2.save()\n\n    # Let's init some special remote which we couldn't really update/fetch\n    if not dl_cfg.get('datalad.tests.dataladremote'):\n        ds.repo.init_remote(\n            'datalad',\n            ['encryption=none', 'type=external', 'externaltype=datalad'])\n    # fetch all remotes\n    assert_result_count(\n        ds.update(), 1, status='ok', type='dataset')\n\n    # no merge, so changes are not in active branch:\n    assert_not_in(\"first.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    assert_not_in(\"second.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    # but we know the changes in remote branches:\n    assert_in(\"first.txt\", ds.repo.get_files(\"sibling_1/\" + DEFAULT_BRANCH))\n    assert_in(\"second.txt\", ds.repo.get_files(\"sibling_2/\" + DEFAULT_BRANCH))\n\n    # no merge strategy for multiple remotes yet:\n    # more clever now, there is a tracking branch that provides a remote\n    #assert_raises(NotImplementedError, ds.update, merge=True)\n\n    # merge a certain remote:\n    assert_result_count(\n        ds.update(sibling='sibling_1', merge=True),\n        1, action='update', status='ok', type='dataset')\n\n    # changes from sibling_2 still not present:\n    assert_not_in(\"second.txt\",\n                  ds.repo.get_files(ds.repo.get_active_branch()))\n    # changes from sibling_1 merged:\n    assert_in(\"first.txt\",\n              ds.repo.get_files(ds.repo.get_active_branch()))\n    # it's known to annex, but has no content yet:\n    annexprops = ds.repo.get_file_annexinfo(\n        \"first.txt\", eval_availability=True)\n    annexprops['key']  # blows if unknown\n    eq_(False, annexprops['has_content'])\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_newthings_coming_down(originpath=None, destpath=None):\n    origin = GitRepo(originpath, create=True)\n    create_tree(originpath, {'load.dat': 'heavy'})\n    Dataset(originpath).save('load.dat')\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    assert_is_instance(ds.repo, GitRepo)\n    assert_in(DEFAULT_REMOTE, ds.repo.get_remotes())\n    # turn origin into an annex\n    origin = AnnexRepo(originpath, create=True)\n    # clone doesn't know yet\n    assert_false(knows_annex(ds.path))\n    # but after an update it should\n    # no merge, only one sibling, no parameters should be specific enough\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    assert(knows_annex(ds.path))\n    # no branches appeared\n    eq_(ds.repo.get_branches(), [DEFAULT_BRANCH])\n    # now merge, and get an annex\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    assert_in('git-annex', ds.repo.get_branches())\n    assert_is_instance(ds.repo, AnnexRepo)\n    # should be fully functional\n    testfname = opj(ds.path, 'load.dat')\n    assert_false(ds.repo.file_has_content(testfname))\n    ds.get('.')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    # check that a new tag comes down\n    origin.tag('first!')\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(ds.repo.get_tags(output='name')[0], 'first!')\n\n    # and now we destroy the remote annex\n    origin.call_git(['config', '--remove-section', 'annex'])\n    rmtree(opj(origin.path, '.git', 'annex'), chmod_files=True)\n    origin.call_git(['branch', '-D', 'git-annex'])\n    origin = GitRepo(originpath)\n    assert_false(knows_annex(originpath))\n\n    # and update the local clone\n    # for now this should simply not fail (see gh-793), later might be enhanced to a\n    # graceful downgrade\n    before_branches = ds.repo.get_branches()\n    ok_(any(\"git-annex\" in b\n            for b in ds.repo.get_remote_branches()))\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(before_branches, ds.repo.get_branches())\n    # annex branch got pruned\n    assert_false(any(\"git-annex\" in b\n                     for b in ds.repo.get_remote_branches()))\n    # check that a new tag comes down even if repo types mismatch\n    origin.tag('second!')\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    eq_(ds.repo.get_tags(output='name')[-1], 'second!')\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_update_volatile_subds(originpath=None, otherpath=None, destpath=None):\n    origin = Dataset(originpath).create()\n    repo = origin.repo\n    if repo.is_managed_branch() and repo.git_annex_version <= \"8.20201129\":\n        # Fails before git-annex's fd161da2c (adjustTree: Consider submodule\n        # deletions, 2021-01-06).\n        raise SkipTest(\n            \"On adjusted branch, test requires fix in more recent git-annex\")\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    # as a submodule\n    sname = 'subm 1'\n    osm1 = origin.create(sname)\n    assert_result_count(ds.update(), 1, status='ok', type='dataset')\n    # nothing without a merge, no inappropriate magic\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # and we should be able to do update with recursive invocation\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        1, action='update', status='ok', type='dataset')\n    # known, and placeholder exists\n    assert_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    ok_(exists(opj(ds.path, sname)))\n\n    # remove from origin\n    origin.remove(sname, reckless='availability')\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # gone locally, wasn't checked out\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    assert_false(exists(opj(ds.path, sname)))\n\n    # re-introduce at origin\n    osm1 = origin.create(sname)\n    create_tree(osm1.path, {'load.dat': 'heavy'})\n    origin.save(opj(osm1.path, 'load.dat'))\n    assert_result_count(ds.update(merge=True),\n                        1, action='update', status='ok', type='dataset')\n    # grab new content of uninstall subdataset, right away\n    ds.get(opj(ds.path, sname, 'load.dat'))\n    ok_file_has_content(opj(ds.path, sname, 'load.dat'), 'heavy')\n\n    # modify ds and subds at origin\n    create_tree(origin.path, {'mike': 'this', sname: {'probe': 'little'}})\n    origin.save(recursive=True)\n    assert_repo_status(origin.path)\n\n    # updates for both datasets should come down the pipe\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        2, action='update', status='ok', type='dataset')\n    assert_repo_status(ds.path)\n\n    # now remove just-installed subdataset from origin again\n    origin.remove(sname, reckless='kill')\n    assert_not_in(sname, origin.subdatasets(result_xfm='relpaths'))\n    assert_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    # merge should disconnect the installed subdataset, but leave the actual\n    # ex-subdataset alone\n    assert_result_count(ds.update(merge=True, recursive=True),\n                        1, action='update', type='dataset')\n    assert_not_in(sname, ds.subdatasets(result_xfm='relpaths'))\n    ok_file_has_content(opj(ds.path, sname, 'load.dat'), 'heavy')\n    ok_(Dataset(opj(ds.path, sname)).is_installed())\n\n    # now remove the now disconnected subdataset for further tests\n    remove(dataset=op.join(ds.path, sname), reckless='kill')\n    assert_repo_status(ds.path)\n\n    # new separate subdataset, not within the origin dataset\n    otherds = Dataset(otherpath).create()\n    # install separate dataset as a submodule\n    ds.install(source=otherds.path, path='other')\n    create_tree(otherds.path, {'brand': 'new'})\n    otherds.save()\n    assert_repo_status(otherds.path)\n    # pull in changes\n    res = ds.update(merge=True, recursive=True)\n    assert_result_count(\n        res, 2, status='ok', action='update', type='dataset')\n    # the next is to check for #2858\n    assert_repo_status(ds.path)\n\n\n@with_tempfile(mkdir=True)\n@with_tempfile(mkdir=True)\ndef test_reobtain_data(originpath=None, destpath=None):\n    origin = Dataset(originpath).create()\n    ds = install(\n        source=originpath, path=destpath,\n        result_xfm='datasets', return_type='item-or-list')\n    # no harm\n    assert_result_count(ds.update(merge=True, reobtain_data=True),\n                        1, action=\"update\", status=\"ok\")\n    # content\n    create_tree(origin.path, {'load.dat': 'heavy'})\n    origin.save(opj(origin.path, 'load.dat'))\n    # update does not bring data automatically\n    assert_result_count(ds.update(merge=True, reobtain_data=True),\n                        1, action=\"update\", status=\"ok\")\n    assert_in('load.dat', ds.repo.get_annexed_files())\n    assert_false(ds.repo.file_has_content('load.dat'))\n    # now get data\n    ds.get('load.dat')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    # new content at origin\n    create_tree(origin.path, {'novel': 'but boring'})\n    origin.save()\n    # update must not bring in data for new file\n    result = ds.update(merge=True, reobtain_data=True)\n    assert_in_results(result, action='get', status='notneeded')\n\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'heavy')\n    assert_in('novel', ds.repo.get_annexed_files())\n    assert_false(ds.repo.file_has_content('novel'))\n    # modify content at origin\n    os.remove(opj(origin.path, 'load.dat'))\n    create_tree(origin.path, {'load.dat': 'light'})\n    origin.save()\n    # update must update file with existing data, but leave empty one alone\n    res = ds.update(merge=True, reobtain_data=True)\n    assert_result_count(res, 1, status='ok', type='dataset', action='update')\n    assert_result_count(res, 1, status='ok', type='file', action='get')\n    ok_file_has_content(opj(ds.path, 'load.dat'), 'light')\n    assert_false(ds.repo.file_has_content('novel'))\n\n\n@with_tempfile(mkdir=True)\ndef test_multiway_merge(path=None):\n    # prepare ds with two siblings, but no tracking branch\n    ds = Dataset(op.join(path, 'ds_orig')).create()\n    r1 = AnnexRepo(path=op.join(path, 'ds_r1'), git_opts={'bare': True})\n    r2 = GitRepo(path=op.join(path, 'ds_r2'), git_opts={'bare': True})\n    ds.siblings(action='add', name='r1', url=r1.path)\n    ds.siblings(action='add', name='r2', url=r2.path)\n    assert_status('ok', ds.push(to='r1'))\n    # push unlike publish reports on r2 not being an annex remote with a\n    # 'notneeded'\n    assert_status(('ok', 'notneeded'), ds.push(to='r2'))\n    # just a fetch should be no issue\n    assert_status('ok', ds.update())\n    # ATM we do not support multi-way merges\n    assert_status('impossible', ds.update(merge=True, on_failure='ignore'))\n\n\ndef test_unrelated_history_merge(tmp_path):\n    # prepare two independent datasets and try merging one into another\n    ds = Dataset(tmp_path / 'ds').create()\n    repo = AnnexRepo(tmp_path / 'repo')\n    f = (tmp_path / 'repo' / 'file.dat')\n    f.write_text(\"data\")\n    repo.add(str(f))\n    repo.commit()\n    # ATM we do not do any checks and allow such addition\n    assert_status('ok', ds.siblings(action='add', name='repo', url=repo.path))\n    res = ds.update(how='merge', on_failure='ignore')\n    # ATM we do not have any special handling, just that the first result record\n    # would have that error in \"message\" field\n    assert_status('error', res[0])\n    assert_in('refusing to merge unrelated histories', res[0]['message'])\n\n\n# `git annex sync REMOTE` rather than `git merge TARGET` is used on an\n# adjusted branch, so we don't give an error if TARGET can't be\n# determined.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_no_merge_target(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    assert_repo_status(ds_src.path)\n    ds_clone.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n    res = ds_clone.update(merge=True, on_failure=\"ignore\")\n    assert_in_results(res, status=\"impossible\", action=\"update\")\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge TARGET` being used.\n@skip_if_adjusted_branch\n@slow  # 17sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_conflict(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s0 = Dataset(path / \"clone\" / \"s0\")\n    ds_clone_s1 = Dataset(path / \"clone\" / \"s1\")\n\n    (ds_src.pathobj / \"foo\").write_text(\"src content\")\n    ds_src.save(to_git=True)\n\n    (ds_clone.pathobj / \"foo\").write_text(\"clone content\")\n    ds_clone.save(to_git=True)\n\n    # Top-level merge failure\n    res = ds_clone.update(merge=True, on_failure=\"ignore\")\n    assert_in_results(res, action=\"merge\", status=\"error\")\n    assert_in_results(res, action=\"update\", status=\"error\")\n    # Deal with the conflicts. Note that save() won't handle this gracefully\n    # because it will try to commit with a pathspec, which git doesn't allow\n    # during a merge.\n    ds_clone.repo.call_git([\"checkout\", \"--theirs\", \"--\", \"foo\"])\n    ds_clone.repo.call_git([\"add\", \"--\", \"foo\"])\n    ds_clone.repo.call_git([\"commit\", \"--no-edit\"])\n    assert_repo_status(ds_clone.path)\n\n    # Top-level and subdataset merge failure\n    (ds_src_s0.pathobj / \"foo\").write_text(\"src s0 content\")\n    (ds_src_s1.pathobj / \"foo\").write_text(\"no conflict\")\n    ds_src.save(recursive=True, to_git=True)\n\n    (ds_clone_s0.pathobj / \"foo\").write_text(\"clone s0 content\")\n    ds_clone.save(recursive=True, to_git=True)\n    res = ds_clone.update(merge=True, recursive=True, on_failure=\"ignore\")\n    assert_result_count(res, 2, action=\"merge\", status=\"error\")\n    assert_result_count(res, 2, action=\"update\", status=\"error\")\n    assert_in_results(res, action=\"merge\", status=\"ok\",\n                      path=ds_clone_s1.path)\n    assert_in_results(res, action=\"update\", status=\"ok\",\n                      path=ds_clone_s1.path)\n    # No saving happens if there's a top-level conflict.\n    assert_repo_status(ds_clone.path,\n                       modified=[ds_clone_s0.path, ds_clone_s1.path])\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge TARGET` being used.\n@skip_if_adjusted_branch\n@slow  # 13sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_conflict_in_subdataset_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_src_sub_conflict = ds_src.create(\"sub_conflict\")\n    ds_src_sub_noconflict = ds_src.create(\"sub_noconflict\")\n    ds_src.save()\n\n    # Set up a scenario where one subdataset has a conflict between the remote\n    # and local version, but the parent dataset does not have a conflict\n    # because it hasn't recorded the subdataset state.\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_sub_conflict = Dataset(path / \"clone\" / \"sub_conflict\")\n    ds_clone_sub_noconflict = Dataset(path / \"clone\" / \"sub_noconflict\")\n\n    (ds_src_sub_conflict.pathobj / \"foo\").write_text(\"src content\")\n    ds_src_sub_conflict.save(to_git=True)\n\n    (ds_clone_sub_conflict.pathobj / \"foo\").write_text(\"clone content\")\n    ds_clone_sub_conflict.save(to_git=True)\n\n    (ds_src_sub_noconflict.pathobj / \"foo\").write_text(\"src content\")\n    ds_src_sub_noconflict.save()\n\n    res = ds_clone.update(merge=True, recursive=True, on_failure=\"ignore\")\n    assert_in_results(res, action=\"merge\", status=\"error\",\n                      path=ds_clone_sub_conflict.path)\n    assert_in_results(res, action=\"merge\", status=\"ok\",\n                      path=ds_clone_sub_noconflict.path)\n    assert_in_results(res, action=\"save\", status=\"ok\",\n                      path=ds_clone.path)\n    # We saved the subdataset without a conflict...\n    assert_repo_status(ds_clone_sub_noconflict.path)\n    # ... but the one with the conflict leaves it for the caller to handle.\n    ok_(ds_clone_sub_conflict.repo.call_git(\n        [\"ls-files\", \"--unmerged\", \"--\", \"foo\"], read_only=True).strip())\n\n\n# `git annex sync REMOTE` is used on an adjusted branch, but this error\n# depends on `git merge --ff-only ...` being used.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_ff_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"src\").create()\n    ds_clone_ff = install(source=ds_src.path, path=path / \"clone_ff\",\n                          result_xfm=\"datasets\")\n\n    ds_clone_nonff = install(source=ds_src.path, path=path / \"clone_nonff\",\n                             result_xfm=\"datasets\")\n\n    (ds_clone_nonff.pathobj / \"foo\").write_text(\"local change\")\n    ds_clone_nonff.save(recursive=True)\n\n    (ds_src.pathobj / \"bar\").write_text(\"remote change\")\n    ds_src.save(recursive=True)\n\n    assert_in_results(\n        ds_clone_ff.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"ok\")\n\n    # ff-only prevents a non-fast-forward ...\n    assert_in_results(\n        ds_clone_nonff.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"error\")\n    # ... that would work with \"any\".\n    assert_in_results(\n        ds_clone_nonff.update(merge=\"any\", on_failure=\"ignore\"),\n        action=\"merge\", status=\"ok\")\n\n\n@slow  # 11sec on Yarik's laptop\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_other_branch(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    on_adjusted = ds_src.repo.is_managed_branch()\n    ds_src_subds = ds_src.create(\"subds\")\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n\n    ds_src_subds.repo.call_git([\"checkout\", \"-b\", \"other\"])\n    (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    res = ds_clone.update(merge=True, follow=\"parentds\", recursive=True,\n                          on_failure=\"ignore\")\n    if on_adjusted:\n        # Our git-annex sync based on approach on adjusted branches is\n        # incompatible with follow='parentds'.\n        assert_in_results(res, action=\"update\", status=\"impossible\")\n        return\n    else:\n        assert_in_results(res, action=\"update\", status=\"ok\")\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n    ok_(ds_clone_subds.repo.is_under_annex(\"foo\"))\n\n    (ds_src_subds.pathobj / \"bar\").write_text(\"bar content\")\n    ds_src.save(recursive=True)\n    ds_clone_subds.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n    ds_clone.update(merge=True, follow=\"parentds\", recursive=True)\n    if not on_adjusted:\n        eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n\n# This test depends on the source repo being an un-adjusted branch.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_adjusted_warning(path=None):\n    path = Path(path)\n\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_subds = ds_src.create(\"subds\")\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n    maybe_adjust_repo(ds_clone_subds.repo)\n    # Note: Were we to save ds_clone here, we would get a merge conflict in the\n    # top repo for the submodule (even if using 'git annex sync' rather than\n    # 'git merge').\n\n    ds_src_subds.repo.call_git([\"checkout\", DEFAULT_BRANCH + \"^0\"])\n    (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    assert_in_results(\n        ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                        on_failure=\"ignore\"),\n        status=\"impossible\",\n        path=ds_clone_subds.path,\n        action=\"update\")\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n\n@slow  # 12 + 21sec on Yarik's laptop\n@pytest.mark.parametrize(\"on_adjusted\", [True, False])\n# Skip non-adjusted case for systems that only support adjusted branches.\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_merge_follow_parentds_subdataset_detached(path=None, *, on_adjusted):\n    if on_adjusted and DEFAULT_REMOTE != \"origin\" and \\\n       external_versions['cmd:annex'] <= \"8.20210330\":\n        raise SkipTest(\n            \"'git annex init' with adjusted branch currently fails \"\n            \"due to hard-coded 'origin'\")\n\n    # Note: For the adjusted case, this is not much more than a smoke test that\n    # on an adjusted branch we fail sensibly. The resulting state is not easy\n    # to reason about nor desirable.\n    path = Path(path)\n    # $path/source/s0/s1\n    # The additional dataset level is to gain some confidence that this works\n    # for nested datasets.\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s1 = ds_src_s0.create(\"s1\")\n    ds_src.save(recursive=True)\n    if on_adjusted:\n        # Note: We adjust after creating all the datasets above to avoid a bug\n        # fixed in git-annex 7.20191024, specifically bbdeb1a1a (sync: Fix\n        # crash when there are submodules and an adjusted branch is checked\n        # out, 2019-10-23).\n        for ds in [ds_src, ds_src_s0, ds_src_s1]:\n            maybe_adjust_repo(ds.repo)\n        ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n\n    ds_src_s1.repo.checkout(DEFAULT_BRANCH + \"^0\")\n    (ds_src_s1.pathobj / \"foo\").write_text(\"foo content\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    res = ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    if on_adjusted:\n        # The top-level update is okay because there is no parent revision to\n        # update to.\n        assert_in_results(\n            res,\n            status=\"ok\",\n            path=ds_clone.path,\n            action=\"update\")\n        # The subdataset, on the other hand, is impossible.\n        assert_in_results(\n            res,\n            status=\"impossible\",\n            path=ds_clone_s1.path,\n            action=\"update\")\n        return\n    assert_repo_status(ds_clone.path)\n\n    # We brought in the revision and got to the same state of the remote.\n    # Blind saving here without bringing in the current subdataset revision\n    # would have resulted in a new commit in ds_clone that reverting the\n    # last subdataset ID recorded in ds_src.\n    eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n\n    # Record a revision in the parent and then move HEAD away from it so that\n    # the explicit revision fetch fails.\n    (ds_src_s1.pathobj / \"bar\").write_text(\"bar content\")\n    ds_src.save(recursive=True)\n    ds_src_s1.repo.checkout(DEFAULT_BRANCH)\n    # This is the default, but just in case:\n    ds_src_s1.repo.config.set(\"uploadpack.allowAnySHA1InWant\", \"false\",\n                              scope=\"local\")\n    # Configure the fetcher to use v0 because Git defaults to v2 as of\n    # v2.26.0, which allows fetching unadvertised objects regardless\n    # of the value of uploadpack.allowAnySHA1InWant.\n    ds_clone_s1.repo.config.set(\"protocol.version\", \"0\", scope=\"local\")\n    res = ds_clone.update(merge=True, recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    # The fetch with the explicit ref fails because it isn't advertised.\n    assert_in_results(\n        res,\n        status=\"impossible\",\n        path=ds_clone_s1.path,\n        action=\"update\")\n\n    # Back to the detached head.\n    ds_src_s1.repo.checkout(\"HEAD@{1}\")\n    # Set up a case where update() will not resolve the sibling.\n    ds_clone_s1.repo.call_git([\"branch\", \"--unset-upstream\"])\n    ds_clone_s1.config.reload(force=True)\n    ds_clone_s1.repo.call_git([\"remote\", \"add\", \"other\", ds_src_s1.path])\n    res = ds_clone.update(recursive=True, follow=\"parentds\",\n                          on_failure=\"ignore\")\n    # In this case, update() won't abort if we call with merge=False, but\n    # it does if the revision wasn't brought down in the `fetch(all_=True)`\n    # call.\n    assert_in_results(\n        res,\n        status=\"impossible\",\n        path=ds_clone_s1.path,\n        action=\"update\")\n\n\n@with_tempfile(mkdir=True)\ndef test_update_unborn_master(path=None):\n    ds_a = Dataset(op.join(path, \"ds-a\")).create()\n    ds_a.repo.call_git([\"branch\", \"-m\", DEFAULT_BRANCH, \"other\"])\n    ds_a.repo.checkout(DEFAULT_BRANCH, options=[\"--orphan\"])\n    ds_b = install(source=ds_a.path, path=op.join(path, \"ds-b\"))\n\n    ds_a.repo.checkout(\"other\")\n    (ds_a.pathobj / \"foo\").write_text(\"content\")\n    ds_a.save()\n\n    # clone() will try to switch away from an unborn branch if there\n    # is another ref available.  Reverse these efforts so that we can\n    # test that update() fails reasonably here because we should still\n    # be able to update from remotes that datalad didn't clone.\n    ds_b.repo.update_ref(\"HEAD\", \"refs/heads/\" + DEFAULT_BRANCH,\n                         symbolic=True)\n    assert_false(ds_b.repo.commit_exists(\"HEAD\"))\n    assert_status(\"impossible\",\n                  ds_b.update(merge=True, on_failure=\"ignore\"))\n\n    ds_b.repo.checkout(\"other\")\n    assert_status(\"ok\",\n                  ds_b.update(merge=True, on_failure=\"ignore\"))\n    eq_(ds_a.repo.get_hexsha(), ds_b.repo.get_hexsha())\n\n\n@slow  # ~25s\n@with_tempfile(mkdir=True)\ndef test_update_follow_parentds_lazy(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s0 = ds_src.create(\"s0\")\n    ds_src_s0_s0 = ds_src_s0.create(\"s0\")\n    ds_src_s0.create(\"s1\")\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src.create(\"s2\")\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_clone_s0 = Dataset(ds_clone.pathobj / \"s0\")\n    ds_clone_s0_s0 = Dataset(ds_clone.pathobj / \"s0\" / \"s0\")\n    ds_clone_s0_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s1\")\n    ds_clone_s2 = Dataset(ds_clone.pathobj / \"s2\")\n\n    (ds_src_s0_s0.pathobj / \"foo\").write_text(\"in s0 s0\")\n    ds_src_s0_s0.save()\n    (ds_src_s1.pathobj / \"foo\").write_text(\"in s1\")\n    ds_src.save(recursive=True)\n    # State:\n    # .\n    # |-- s0\n    # |   |-- s0\n    # |   `-- s1  * matches registered commit\n    # |-- s1\n    # `-- s2      * matches registered commit\n    res = ds_clone.update(follow=\"parentds-lazy\", merge=True, recursive=True,\n                          on_failure=\"ignore\")\n    on_adjusted = ds_clone.repo.is_managed_branch()\n    # For adjusted branches, follow=parentds* bails with an impossible result,\n    # so the s0 update doesn't get brought in and s0_s0 also matches the\n    # registered commit.\n    n_notneeded_expected = 3 if on_adjusted else 2\n    assert_result_count(res, n_notneeded_expected,\n                        action=\"update\", status=\"notneeded\")\n    assert_in_results(res, action=\"update\", status=\"notneeded\",\n                      path=ds_clone_s0_s1.repo.path)\n    assert_in_results(res, action=\"update\", status=\"notneeded\",\n                      path=ds_clone_s2.repo.path)\n    if on_adjusted:\n        assert_in_results(res, action=\"update\", status=\"notneeded\",\n                          path=ds_clone_s0_s0.repo.path)\n        assert_repo_status(ds_clone.path,\n                           modified=[ds_clone_s0.repo.path,\n                                     ds_clone_s1.repo.path])\n    else:\n        assert_repo_status(ds_clone.path)\n\n\n@slow  # ~10s\n@with_tempfile(mkdir=True)\ndef test_update_follow_parentds_lazy_other_branch(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_sub = ds_src.create(\"sub\")\n    ds_src_sub.repo.checkout(DEFAULT_BRANCH, options=[\"-bother\"])\n    (ds_src_sub.pathobj / \"foo\").write_text(\"on other branch\")\n    ds_src_sub.save()\n    ds_src_sub.repo.checkout(DEFAULT_BRANCH)\n    ds_src.save(recursive=True)\n    assert_repo_status(ds_src.path)\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    ds_src_sub.repo.checkout(\"other\")\n    ds_src.save(recursive=True)\n\n    with patch(\"datalad.support.gitrepo.GitRepo.fetch\") as fetch_cmd:\n        ds_clone.update(follow=\"parentds\", merge=\"ff-only\",\n                        recursive=True, on_failure=\"ignore\")\n        eq_(fetch_cmd.call_count, 2)\n\n    # With parentds-lazy, an unneeded fetch call in the subdataset is dropped.\n    with patch(\"datalad.support.gitrepo.GitRepo.fetch\") as fetch_cmd:\n        ds_clone.update(follow=\"parentds-lazy\", merge=\"ff-only\",\n                        recursive=True, on_failure=\"ignore\")\n        eq_(fetch_cmd.call_count, 1)\n\n    if not ds_clone.repo.is_managed_branch():\n        # Now the real thing.\n        ds_clone.update(follow=\"parentds-lazy\", merge=\"ff-only\",\n                        recursive=True)\n        ok_(op.lexists(str(ds_clone.pathobj / \"sub\" / \"foo\")))\n\n\n@with_tempfile(mkdir=True)\ndef test_update_adjusted_incompatible_with_ff_only(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    maybe_adjust_repo(ds_clone.repo)\n\n    assert_in_results(\n        ds_clone.update(merge=\"ff-only\", on_failure=\"ignore\"),\n        action=\"update\", status=\"impossible\")\n    assert_in_results(\n        ds_clone.update(on_failure=\"ignore\"),\n        action=\"update\", status=\"ok\")\n\n\n@pytest.mark.parametrize(\"follow,action\", [\n    # Ideally each combination would be checked, but this test is a bit slow.\n    (\"parentds\", \"reset\"),\n    (\"sibling\", \"checkout\"),\n])\n@slow  # ~10s\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_update_how_subds_different(path=None, *, follow, action):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_sub = ds_src.create(\"sub\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n    (ds_clone.pathobj / \"foo\").write_text(\"foo\")\n    ds_clone.save()\n    ds_clone_sub = Dataset(ds_clone.pathobj / \"sub\")\n\n    (ds_src_sub.pathobj / \"bar\").write_text(\"bar\")\n    ds_src.save(recursive=True)\n\n    # Add unrecorded state to make --follow=sibling/parentds differ.\n    (ds_src_sub.pathobj / \"baz\").write_text(\"baz\")\n    ds_src_sub.save()\n\n    ds_clone_repo = ds_clone.repo\n    ds_clone_hexsha_pre = ds_clone_repo.get_hexsha()\n\n    ds_clone_sub_repo = ds_clone_sub.repo\n    ds_clone_sub_branch_pre = ds_clone_sub_repo.get_active_branch()\n\n    res = ds_clone.update(follow=follow, how=\"merge\", how_subds=action,\n                          recursive=True)\n\n    assert_result_count(res, 1, action=\"merge\", status=\"ok\",\n                        path=ds_clone.path)\n    assert_result_count(res, 1, action=f\"update.{action}\", status=\"ok\",\n                        path=ds_clone_sub.path)\n\n    ds_clone_hexsha_post = ds_clone_repo.get_hexsha()\n    neq_(ds_clone_hexsha_pre, ds_clone_hexsha_post)\n    neq_(ds_src.repo.get_hexsha(), ds_clone_hexsha_post)\n    ok_(ds_clone_repo.is_ancestor(ds_clone_hexsha_pre, ds_clone_hexsha_post))\n\n    eq_(ds_clone_sub.repo.get_hexsha(),\n        ds_src_sub.repo.get_hexsha(None if follow == \"sibling\" else \"HEAD~\"))\n    ds_clone_sub_branch_post = ds_clone_sub_repo.get_active_branch()\n\n    if action == \"checkout\":\n        neq_(ds_clone_sub_branch_pre, ds_clone_sub_branch_post)\n        assert_false(ds_clone_sub_branch_post)\n    else:\n        eq_(ds_clone_sub_branch_pre, ds_clone_sub_branch_post)\n\n\n@slow  # ~15s\n@skip_if_adjusted_branch\n@with_tempfile(mkdir=True)\ndef test_update_reset_dirty(path=None):\n    path = Path(path)\n    ds_src = Dataset(path / \"source\").create()\n    ds_src_s1 = ds_src.create(\"s1\")\n    ds_src_s2 = ds_src.create(\"s2\")\n    ds_src.save()\n\n    ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                       recursive=True, result_xfm=\"datasets\")\n\n    (ds_src_s1.pathobj / \"foo\").write_text(\"foo\")\n    (ds_src_s2.pathobj / \"bar\").write_text(\"bar\")\n    ds_src.save(recursive=True)\n\n    ds_clone_s1 = Dataset(ds_clone.pathobj / \"s1\")\n    ds_clone_s2 = Dataset(ds_clone.pathobj / \"s2\")\n    (ds_clone_s1.pathobj / \"dirt\").write_text(\"\")\n\n    res = ds_clone.update(follow=\"sibling\", how=\"reset\", recursive=True,\n                          on_failure=\"ignore\")\n\n    assert_result_count(res, 1, path=ds_clone.path,\n                        action=f\"update.reset\", status=\"error\")\n    assert_result_count(res, 1, path=ds_clone_s1.path,\n                        action=f\"update.reset\", status=\"error\")\n    assert_result_count(res, 1, path=ds_clone_s2.path,\n                        action=f\"update.reset\", status=\"ok\")\n\n    # s2 was reset...\n    eq_(ds_src_s2.repo.get_hexsha(), ds_clone_s2.repo.get_hexsha())\n    # ... but s1 and the top-level dataset stayed behind due to the dirty tree.\n    eq_(ds_src.repo.get_hexsha(\"HEAD~\"), ds_clone.repo.get_hexsha())\n    eq_(ds_src_s1.repo.get_hexsha(\"HEAD~\"), ds_clone_s1.repo.get_hexsha())\n\n    assert_repo_status(ds_clone.path,\n                       modified=[ds_clone_s1.repo.path,\n                                 ds_clone_s2.repo.path])\n\n\ndef test_process_how_args():\n    # --merge maps onto --how values. It has no equivalent of --how-subds,\n    # --which just gets set to --how's value when unspecified.\n    eq_(_process_how_args(merge=False, how=None, how_subds=None),\n        (None, None))\n    eq_(_process_how_args(merge=True, how=None, how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=\"any\", how=None, how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=\"ff-only\", how=None, how_subds=None),\n        (\"ff-only\", \"ff-only\"))\n\n    # Values other than the default --merge=False can not be mixed with\n    # non-default how values.\n    with assert_raises(ValueError):\n        _process_how_args(merge=True, how=\"merge\", how_subds=None)\n    with assert_raises(ValueError):\n        _process_how_args(merge=True, how=None, how_subds=\"merge\")\n\n    # --how-subds inherits the value of --how...\n    eq_(_process_how_args(merge=False, how=\"fetch\", how_subds=None),\n        (None, None))\n    eq_(_process_how_args(merge=False, how=\"merge\", how_subds=None),\n        (\"merge\", \"merge\"))\n    eq_(_process_how_args(merge=False, how=\"ff-only\", how_subds=None),\n        (\"ff-only\", \"ff-only\"))\n    # ... unless --how-subds is explicitly specified.\n    eq_(_process_how_args(merge=False, how=\"merge\", how_subds=\"fetch\"),\n        (\"merge\", None))\n\n\n@with_tempfile(mkdir=True)\ndef test_update_fetch_failure(path=None):\n    path = Path(path)\n\n    ds_a = Dataset(path / \"ds_a\").create()\n    s1 = ds_a.create(\"s1\")\n    ds_a.create(\"s2\")\n\n    ds_b = install(source=ds_a.path, path=str(path / \"ds-b\"), recursive=True)\n\n    # Rename s1 to make fetch fail.\n    s1.pathobj.rename(s1.pathobj.parent / \"s3\")\n\n    res = ds_b.update(recursive=True, on_failure=\"ignore\")\n    assert_in_results(\n        res,\n        status=\"error\",\n        path=str(ds_b.pathobj / \"s1\"),\n        action=\"update\")\n    assert_in_results(\n        res,\n        status=\"ok\",\n        path=str(ds_b.pathobj / \"s2\"),\n        action=\"update\")\n    assert_in_results(\n        res,\n        status=\"ok\",\n        path=ds_b.path,\n        action=\"update\")\n",
        "import pytest\nimport tempfile\nimport os\n\n\n@pytest.fixture\ndef path():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        yield tmpdir\n\n\n@pytest.fixture(autouse=True)\ndef setup_git_config():\n    with tempfile.TemporaryDirectory() as home:\n        old_home = os.environ.get(\"HOME\")\n        os.environ[\"HOME\"] = home\n        os.system('git config --global user.name \"DataLad Tester\"')\n        os.system('git config --global user.email \"test@example.com\"')\n        yield\n        if old_home:\n            os.environ[\"HOME\"] = old_home\n"
    ],
    "test_file_names": [
        "test_1.py",
        "conftest.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Datalad installation attempts...\nVerifying installation...\nDatalad version: 0.18.2+44.gd506f925d\n✅ Installation successful!\nSuccessfully installed Datalad using Python 3.9\n",
    "setup_res_stderr": "Using CPython 3.9.21\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 25 packages in 103ms\nInstalled 25 packages in 4.95s\n + coverage==7.6.10\n + docopt==0.6.2\n + exceptiongroup==1.2.2\n + grabbit==0.2.6\n + importlib-resources==6.4.5\n + iniconfig==2.0.0\n + nibabel==5.3.2\n + num2words==0.5.14\n + numpy==2.0.2\n + packaging==24.2\n + pandas==2.2.3\n + patsy==1.0.1\n + pluggy==1.5.0\n + pybids==0.6.5\n + pytest==8.3.4\n + pytest-cov==6.0.0\n + python-dateutil==2.9.0.post0\n + pytz==2024.2\n + scipy==1.13.1\n + setuptools==75.6.0\n + six==1.17.0\n + tomli==2.2.1\n + typing-extensions==4.12.2\n + tzdata==2024.2\n + zipp==3.21.0\nResolved 65 packages in 4.95s\nPrepared 1 package in 5.51s\nInstalled 53 packages in 825ms\n + annexremote==1.6.6\n + argcomplete==3.5.3\n + backports-tarfile==1.2.0\n + beautifulsoup4==4.12.3\n + boto==2.49.0\n + certifi==2024.12.14\n + cffi==1.17.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + citeproc-py==0.6.0\n + cryptography==44.0.0\n + datalad==0.18.2+44.gd506f925d (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/datalad_d506f925df22667910adb647463856cbcee325c5)\n + distro==1.9.0\n + duecredit==0.10.2\n + fasteners==0.19\n + httpretty==1.1.4\n + humanize==4.11.0\n + idna==3.10\n + importlib-metadata==8.5.0\n + iso8601==2.1.0\n + jaraco-classes==3.4.0\n + jaraco-context==6.0.1\n + jaraco-functools==4.1.0\n + jeepney==0.8.0\n + keyring==25.6.0\n + keyrings-alt==5.0.2\n + looseversion==1.3.0\n + lxml==5.3.0\n + more-itertools==10.5.0\n + msgpack==1.1.0\n + multidict==6.1.0\n + mypy==1.14.1\n + mypy-extensions==1.0.0\n + patool==1.15.0\n + platformdirs==4.3.6\n + propcache==0.2.1\n + pycparser==2.22\n + pyperclip==1.9.0\n + pytest-fail-slow==0.6.0\n + python-gitlab==5.3.0\n + pyyaml==6.0.2\n + requests==2.32.3\n + requests-ftp==0.3.1\n + requests-toolbelt==1.0.0\n + secretstorage==3.3.3\n + soupsieve==2.6\n + tqdm==4.67.1\n + types-python-dateutil==2.9.0.20241206\n + types-requests==2.32.0.20241016\n + urllib3==2.3.0\n + vcrpy==4.3.0\n + wrapt==1.17.0\n + yarl==1.18.3\nResolved 103 packages in 292ms\nInstalled 38 packages in 606ms\n + alabaster==0.7.16\n + asv==0.6.4\n + asv-runner==0.2.1\n + attrs==24.3.0\n + babel==2.16.0\n + build==1.2.2.post1\n + click==8.1.8\n + click-log==0.4.0\n + distlib==0.3.9\n + docutils==0.21.2\n + execnet==2.1.1\n + filelock==3.16.1\n + gprof2dot==2024.6.6\n + imagesize==1.4.1\n + jinja2==3.1.5\n + json5==0.10.0\n + markdown-it-py==3.0.0\n + markupsafe==3.0.2\n + mdurl==0.1.2\n + psutil==6.1.1\n + pygments==2.18.0\n + pympler==1.1\n + pypandoc==1.14\n + pyproject-hooks==1.2.0\n + pytest-xdist==3.6.1\n + scriv==1.5.1\n + snowballstemmer==2.2.0\n + sphinx==7.4.7\n + sphinx-rtd-theme==3.0.2\n + sphinxcontrib-applehelp==2.0.0\n + sphinxcontrib-devhelp==2.0.0\n + sphinxcontrib-htmlhelp==2.1.0\n + sphinxcontrib-jquery==4.1\n + sphinxcontrib-jsmath==1.0.1\n + sphinxcontrib-qthelp==2.0.0\n + sphinxcontrib-serializinghtml==2.0.0\n + tabulate==0.9.0\n + virtualenv==20.28.0\n",
    "new_commit_res_code": 1,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/datalad_d506f925df22667910adb647463856cbcee325c5\nconfigfile: tox.ini\nplugins: fail-slow-0.6.0, xdist-3.6.1, cov-6.0.0\ncollected 25 items\n\nr2e_tests/test_1.py F.FF....F...FFsFF.F......                            [100%]\n\n=================================== FAILURES ===================================\n______________________________ test_update_simple ______________________________\n\nself = AnnexRepo('/tmp/datalad_temp_test_update_simple97tozd_g')\nname = 'dl-test-remote'\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n    \n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n>           self.call_git(['remote', 'remove', name])\n\ndatalad/support/gitrepo.py:1863: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af80dfa760>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none remote remove dl-test-remote' failed with exitcode 2 [err: 'error: No such remote: 'dl-test-remote'']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n\nDuring handling of the above exception, another exception occurred:\n\norigin = '/tmp/datalad_temp_test_update_simplenygec770'\nsrc_path = '/tmp/datalad_temp_test_update_simple97tozd_g'\ndst_path = '/tmp/datalad_temp_test_update_simple2u7e5u98'\n\n    @slow\n    @with_tempfile(mkdir=True)\n    @with_tempfile(mkdir=True)\n    @with_tempfile(mkdir=True)\n    def test_update_simple(origin=None, src_path=None, dst_path=None):\n        ca = dict(result_renderer='disabled')\n        # a remote dataset with a subdataset underneath\n        origds = Dataset(origin).create(**ca)\n        # naming is weird, but a legacy artifact\n        _ = origds.create('subm 1', **ca)\n        _ = origds.create('2', **ca)\n    \n        # prepare src\n        source = install(src_path, source=origin, recursive=True)\n        # forget we cloned it by removing remote, which should lead to\n        # setting tracking branch to target:\n>       source.repo.remove_remote(DEFAULT_REMOTE)\n\nr2e_tests/test_1.py:81: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = AnnexRepo('/tmp/datalad_temp_test_update_simple97tozd_g')\nname = 'dl-test-remote'\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n    \n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n>               raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\nE               datalad.support.exceptions.RemoteNotAvailableError: Remote 'dl-test-remote' is not available. Command failed:\nE               RemoteNotAvailableError: 'git remote remove' [No such remote] [err: 'error: No such remote: 'dl-test-remote'']\n\ndatalad/support/gitrepo.py:1866: RemoteNotAvailableError\n----------------------------- Captured stdout call -----------------------------\ninstall(ok): /tmp/datalad_temp_test_update_simple97tozd_g (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_simple97tozd_g/2 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_simple97tozd_g/subm 1 (dataset)\naction summary:\n  install (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_simple97tozd_g \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_simplenygec770 to /tmp/datalad_temp_test_update_simple97tozd_g \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simple97tozd_g) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_simple97tozd_g) to get /tmp/datalad_temp_test_update_simple97tozd_g \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_simple97tozd_g/2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_simplenygec770/2 to /tmp/datalad_temp_test_update_simple97tozd_g/2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simple97tozd_g/2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_simple97tozd_g/subm 1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_simplenygec770/subm 1 to /tmp/datalad_temp_test_update_simple97tozd_g/subm 1 \n[INFO] Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_simple97tozd_g/subm 1') \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_simplenygec770\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_simplenygec770\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_simple97tozd_g\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_simplenygec770 to /tmp/datalad_temp_test_update_simple97tozd_g\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simple97tozd_g)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_simple97tozd_g) to get /tmp/datalad_temp_test_update_simple97tozd_g\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_simple97tozd_g/2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_simplenygec770/2 to /tmp/datalad_temp_test_update_simple97tozd_g/2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simple97tozd_g/2)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_simple97tozd_g/2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_simple97tozd_g/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_simplenygec770/subm 1 to /tmp/datalad_temp_test_update_simple97tozd_g/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_simple97tozd_g/subm 1')\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_simple97tozd_g/subm 1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________________________ test_update_fetch_all _____________________________\n\npath = PosixPath('/tmp/datalad_temp_test_update_fetch_allkgjn2nan')\n\n    @slow  # ~9s\n    @with_tempfile(mkdir=True)\n    def test_update_fetch_all(path=None):\n        path = Path(path)\n        remote_1 = str(path / \"remote_1\")\n        remote_2 = str(path / \"remote_2\")\n    \n        ds = Dataset(path / \"src\").create()\n        src = ds.repo.path\n    \n        ds_rmt1 = clone(source=src, path=remote_1)\n        ds_rmt2 = clone(source=src, path=remote_2)\n    \n        ds.siblings('add', name=\"sibling_1\", url=remote_1)\n        ds.siblings('add', name=\"sibling_2\", url=remote_2)\n    \n        # modify the remotes:\n        (ds_rmt1.pathobj / \"first.txt\").write_text(\"some file load\")\n        ds_rmt1.save()\n    \n        # TODO: Modify an already present file!\n    \n        (ds_rmt2.pathobj / \"second.txt\").write_text(\"different file load\")\n        ds_rmt2.save()\n    \n        # Let's init some special remote which we couldn't really update/fetch\n        if not dl_cfg.get('datalad.tests.dataladremote'):\n>           ds.repo.init_remote(\n                'datalad',\n                ['encryption=none', 'type=external', 'externaltype=datalad'])\n\nr2e_tests/test_1.py:246: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/annexrepo.py:2002: in init_remote\n    self.call_annex(['initremote'] + [name] + options)\ndatalad/support/annexrepo.py:1267: in call_annex\n    return self._call_annex(\ndatalad/support/annexrepo.py:944: in _call_annex\n    return runner.run(\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.gitrunner.GitWitlessRunner object at 0x75af80a52780>\ncmd = ['git', '-c', 'diff.ignoreSubmodules=none', 'annex', 'initremote', 'datalad', ...]\nprotocol = <class 'datalad.runner.coreprotocols.StdOutErrCapture'>, stdin = None\ncwd = None, env = None, timeout = None, exception_on_error = True, kwargs = {}\napplied_cwd = PosixPath('/tmp/datalad_temp_test_update_fetch_allkgjn2nan/src')\napplied_env = {'ANTHROPIC_API_KEY': 'sk-ant-api03-kztdHBP1RruInQ2uzw8m4wJLbhPHBrb-qHX2mhAKmwoMFvM_dI2oWb5V6JtS45MScuW5CCjPusIrt9Zatx...3b4166bef66ef252935c/server/bin/helpers/browser.sh', 'COHERE_API_KEY': '66TDU3MmN5ELJT1NHWklGG2PJjK2zwVSQbVq42ol', ...}\nthreaded_runner = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af808bb5e0>\nresults_or_iterator = {'code': 1, 'stderr': \"  Cannot run /home/gcpuser/r2e-edits-internal/.venv/bin/git-annex-remote-datalad -- Make sure i...ecial remote git-annex-remote-datalad\\ngit-annex: initremote: 1 failed\\n\", 'stdout': 'initremote datalad \\n\\nfailed\\n'}\nresults = {'code': 1, 'stderr': \"  Cannot run /home/gcpuser/r2e-edits-internal/.venv/bin/git-annex-remote-datalad -- Make sure i...ecial remote git-annex-remote-datalad\\ngit-annex: initremote: 1 failed\\n\", 'stdout': 'initremote datalad \\n\\nfailed\\n'}\nreturn_code = 1\n\n    def run(self,\n            cmd: list | str,\n            protocol: type[WitlessProtocol] | None = None,\n            stdin: bytes | IO | Queue | None = None,\n            cwd: PathLike | str | None = None,\n            env: dict | None = None,\n            timeout: float | None = None,\n            exception_on_error: bool = True,\n            **kwargs) -> dict | _ResultGenerator:\n        \"\"\"Execute a command and communicate with it.\n    \n        Parameters\n        ----------\n        cmd : list or str\n          Sequence of program arguments. Passing a single string causes\n          execution via the platform shell.\n        protocol : WitlessProtocol, optional\n          Protocol class handling interaction with the running process\n          (e.g. output capture). A number of pre-crafted classes are\n          provided (e.g `KillOutput`, `NoCapture`, `GitProgress`).\n          If the protocol has the GeneratorMixIn-mixin, the run-method\n          will return an iterator and can therefore be used in a for-clause.\n        stdin : file-like, bytes, Queue, or None\n          If stdin is a file-like, it will be directly used as stdin for the\n          subprocess. The caller is responsible for writing to it and closing it.\n          If stdin is a bytes, it will be fed to stdin of the subprocess.\n          If all data is written, stdin will be closed.\n          If stdin is a Queue, all elements (bytes) put into the Queue will\n          be passed to stdin until None is read from the queue. If None is read,\n          stdin of the subprocess is closed.\n        cwd : str or path-like, optional\n          If given, commands are executed with this path as PWD,\n          the PWD of the parent process is used otherwise. Overrides\n          any `cwd` given to the constructor.\n        env : dict, optional\n          Environment to be used for command execution. If given, it will\n          completely replace any environment provided to theconstructor. If\n          `cwd` is given, 'PWD' in the environment is set to its value.\n          This must be a complete environment definition, no values\n          from the current environment will be inherited. Overrides\n          any `env` given to the constructor.\n        timeout: float, optional\n          None or the seconds after which a timeout callback is\n          invoked, if no progress was made in communicating with\n          the sub-process, or if waiting for the subprocess exit\n          took more than the specified time. See the protocol and\n          `ThreadedRunner` descriptions for a more detailed discussion\n          on timeouts.\n        exception_on_error : bool, optional\n          This argument is first interpreted if the protocol is a subclass\n          of `GeneratorMixIn`. If it is `True` (default), a\n          `CommandErrorException` is raised by the generator if the\n          sub process exited with a return code not equal to zero. If the\n          parameter is `False`, no exception is raised. In both cases the\n          return code can be read from the attribute `return_code` of\n          the generator. Then this argument interpreted within this function\n          to not raise `CommandError` if value is False in case of non-0 exit.\n        kwargs :\n          Passed to the Protocol class constructor.\n    \n        Returns\n        -------\n          dict | _ResultGenerator\n    \n            If the protocol is not a subclass of `GeneratorMixIn`, the\n            result of protocol._prepare_result will be returned.\n    \n            If the protocol is a subclass of `GeneratorMixIn`, a Generator, i.e.\n            a `_ResultGenerator`, will be returned. This allows to use this\n            method in constructs like:\n    \n                for protocol_output in runner.run():\n                    ...\n    \n            Where the iterator yields whatever protocol.pipe_data_received\n            sends into the generator.\n            If all output was yielded and the process has terminated, the\n            generator will raise StopIteration(return_code), where\n            return_code is the return code of the process. The return code\n            of the process will also be stored in the \"return_code\"-attribute\n            of the runner. So you could write:\n    \n               gen = runner.run()\n               for file_descriptor, data in gen:\n                   ...\n    \n               # get the return code of the process\n               result = gen.return_code\n    \n        Raises\n        ------\n        CommandError\n          On execution failure (non-zero exit code) this exception is\n          raised which provides the command (cmd), stdout, stderr,\n          exit code (status), and a message identifying the failed\n          command, as properties.\n        FileNotFoundError\n          When a given executable does not exist.\n        \"\"\"\n        if protocol is None:\n            # by default let all subprocess stream pass through\n            protocol = NoCapture\n    \n        applied_cwd = cwd or self.cwd\n        applied_env = self._get_adjusted_env(\n            env=env or self.env,\n            cwd=applied_cwd,\n        )\n    \n        lgr.debug(\n            'Run %r (protocol_class=%s) (cwd=%s)',\n            cmd,\n            protocol.__name__,\n            applied_cwd\n        )\n    \n        threaded_runner = ThreadedRunner(\n            cmd=cmd,\n            protocol_class=protocol,\n            stdin=stdin,\n            protocol_kwargs=kwargs,\n            timeout=timeout,\n            exception_on_error=exception_on_error,\n            cwd=applied_cwd,\n            env=applied_env\n        )\n    \n        results_or_iterator = threaded_runner.run()\n        if issubclass(protocol, GeneratorMixIn):\n            return results_or_iterator\n    \n        results = cast(dict, results_or_iterator)\n        # log before any exception is raised\n        lgr.debug(\"Finished %r with status %s\", cmd, results['code'])\n    \n        # make it such that we always blow if a protocol did not report\n        # a return code at all or it was non-0 and we were not asked ignore\n        # errors\n        return_code = results.get('code', None)\n        if return_code is None or (return_code and exception_on_error):\n            # the runner has a better idea, doc string warns Protocol\n            # implementations not to return these\n            results.pop('cmd', None)\n            results.pop('cwd', None)\n>           raise CommandError(\n                # whatever the results were, we carry them forward\n                cmd=cmd,\n                cwd=applied_cwd,\n                **results,\n            )\nE           datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none annex initremote datalad encryption=none type=external externaltype=datalad -c annex.dotfiles=true' failed with exitcode 1 under /tmp/datalad_temp_test_update_fetch_allkgjn2nan/src [out: 'initremote datalad \nE           \nE           failed'] [err: 'Cannot run /home/gcpuser/r2e-edits-internal/.venv/bin/git-annex-remote-datalad -- Make sure it's executable and that its dependencies are installed.\nE           git-annex: unable to use external special remote git-annex-remote-datalad\nE           git-annex: initremote: 1 failed']\n\ndatalad/runner/runner.py:223: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2 (dataset)\n.: sibling_1(+) [/tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1 (git)]\n.: sibling_2(+) [/tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2 (git)]\nadd(ok): first.txt (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): second.txt (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_allkgjn2nan/src to /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_allkgjn2nan/src to /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_allkgjn2nan/src to /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_allkgjn2nan/src to /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_allkgjn2nan/remote_2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_newthings_coming_down __________________________\n\noriginpath = '/tmp/datalad_temp_test_newthings_coming_down_ge0oqrz'\ndestpath = '/tmp/datalad_temp_test_newthings_coming_downps_rklze'\n\n    @with_tempfile(mkdir=True)\n    @with_tempfile(mkdir=True)\n    def test_newthings_coming_down(originpath=None, destpath=None):\n        origin = GitRepo(originpath, create=True)\n        create_tree(originpath, {'load.dat': 'heavy'})\n        Dataset(originpath).save('load.dat')\n        ds = install(\n            source=originpath, path=destpath,\n            result_xfm='datasets', return_type='item-or-list')\n        assert_is_instance(ds.repo, GitRepo)\n>       assert_in(DEFAULT_REMOTE, ds.repo.get_remotes())\n\nr2e_tests/test_1.py:294: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nfirst = 'dl-test-remote', second = ['origin'], msg = None\n\n    def assert_in(first, second, msg=None):\n        if msg is None:\n>           assert first in second\nE           AssertionError: assert 'dl-test-remote' in ['origin']\n\ndatalad/tests/utils_pytest.py:99: AssertionError\n----------------------------- Captured stdout call -----------------------------\nadd(ok): load.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_newthings_coming_downps_rklze (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] Attempting a clone into /tmp/datalad_temp_test_newthings_coming_downps_rklze \n[INFO] Attempting to clone from /tmp/datalad_temp_test_newthings_coming_down_ge0oqrz to /tmp/datalad_temp_test_newthings_coming_downps_rklze \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_newthings_coming_downps_rklze) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_newthings_coming_down_ge0oqrz\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_newthings_coming_downps_rklze\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_newthings_coming_down_ge0oqrz to /tmp/datalad_temp_test_newthings_coming_downps_rklze\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_newthings_coming_downps_rklze)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_merge_no_merge_target __________________________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc')\n\n    @skip_if_adjusted_branch\n    @with_tempfile(mkdir=True)\n    def test_merge_no_merge_target(path=None):\n        path = Path(path)\n        ds_src = Dataset(path / \"source\").create()\n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        assert_repo_status(ds_src.path)\n>       ds_clone.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n\nr2e_tests/test_1.py:521: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af80793ee0>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout -bnew dl-test-branch' failed with exitcode 128 [err: 'fatal: 'dl-test-branch' is not a commit and a branch 'new' cannot be created from it']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/source to /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone) to get /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/source to /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone) to get /tmp/datalad_temp_test_merge_no_merge_targeteh22ilbc/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.distribution.get:log.py:432 Installing: done\n______________ test_merge_follow_parentds_subdataset_other_branch ______________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j')\n\n    @slow  # 11sec on Yarik's laptop\n    @with_tempfile(mkdir=True)\n    def test_merge_follow_parentds_subdataset_other_branch(path=None):\n        path = Path(path)\n        ds_src = Dataset(path / \"source\").create()\n        on_adjusted = ds_src.repo.is_managed_branch()\n        ds_src_subds = ds_src.create(\"subds\")\n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n    \n        ds_src_subds.repo.call_git([\"checkout\", \"-b\", \"other\"])\n        (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n        ds_src.save(recursive=True)\n        assert_repo_status(ds_src.path)\n    \n        res = ds_clone.update(merge=True, follow=\"parentds\", recursive=True,\n                              on_failure=\"ignore\")\n        if on_adjusted:\n            # Our git-annex sync based on approach on adjusted branches is\n            # incompatible with follow='parentds'.\n            assert_in_results(res, action=\"update\", status=\"impossible\")\n            return\n        else:\n            assert_in_results(res, action=\"update\", status=\"ok\")\n        eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n        ok_(ds_clone_subds.repo.is_under_annex(\"foo\"))\n    \n        (ds_src_subds.pathobj / \"bar\").write_text(\"bar content\")\n        ds_src.save(recursive=True)\n>       ds_clone_subds.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n\nr2e_tests/test_1.py:686: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af80d03d30>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout -bnew dl-test-branch' failed with exitcode 128 [err: 'fatal: 'dl-test-branch' is not a commit and a branch 'new' cannot be created from it']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subds (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds (dataset)\naction summary:\n  install (ok: 2)\nadd(ok): foo (file)\nsave(ok): subds (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): subds (dataset) [Merged e36d303dad0e6834567024d63d013b707ad3136e]\nupdate.annex_merge(ok): subds (dataset) [Merged annex branch]\nupdate(ok): subds (dataset)\naction summary:\n  merge (ok: 2)\n  save (notneeded: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 2)\nadd(ok): bar (file)\nsave(ok): subds (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone/subds)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/clone\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch3ym_v7_j/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________ test_merge_follow_parentds_subdataset_adjusted_warning ____________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq')\n\n    @skip_if_adjusted_branch\n    @with_tempfile(mkdir=True)\n    def test_merge_follow_parentds_subdataset_adjusted_warning(path=None):\n        path = Path(path)\n    \n        ds_src = Dataset(path / \"source\").create()\n        ds_src_subds = ds_src.create(\"subds\")\n    \n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n        maybe_adjust_repo(ds_clone_subds.repo)\n        # Note: Were we to save ds_clone here, we would get a merge conflict in the\n        # top repo for the submodule (even if using 'git annex sync' rather than\n        # 'git merge').\n    \n>       ds_src_subds.repo.call_git([\"checkout\", DEFAULT_BRANCH + \"^0\"])\n\nr2e_tests/test_1.py:709: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af804e6580>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout 'dl-test-branch^0'' failed with exitcode 1 [err: 'error: pathspec 'dl-test-branch^0' did not match any file(s) known to git']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subds (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds (dataset)\naction summary:\n  install (ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningu3ca5bvq/clone/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________ test_merge_follow_parentds_subdataset_detached[False] _____________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj')\n\n    @slow  # 12 + 21sec on Yarik's laptop\n    @pytest.mark.parametrize(\"on_adjusted\", [True, False])\n    # Skip non-adjusted case for systems that only support adjusted branches.\n    @skip_if_adjusted_branch\n    @with_tempfile(mkdir=True)\n    def test_merge_follow_parentds_subdataset_detached(path=None, *, on_adjusted):\n        if on_adjusted and DEFAULT_REMOTE != \"origin\" and \\\n           external_versions['cmd:annex'] <= \"8.20210330\":\n            raise SkipTest(\n                \"'git annex init' with adjusted branch currently fails \"\n                \"due to hard-coded 'origin'\")\n    \n        # Note: For the adjusted case, this is not much more than a smoke test that\n        # on an adjusted branch we fail sensibly. The resulting state is not easy\n        # to reason about nor desirable.\n        path = Path(path)\n        # $path/source/s0/s1\n        # The additional dataset level is to gain some confidence that this works\n        # for nested datasets.\n        ds_src = Dataset(path / \"source\").create()\n        ds_src_s0 = ds_src.create(\"s0\")\n        ds_src_s1 = ds_src_s0.create(\"s1\")\n        ds_src.save(recursive=True)\n        if on_adjusted:\n            # Note: We adjust after creating all the datasets above to avoid a bug\n            # fixed in git-annex 7.20191024, specifically bbdeb1a1a (sync: Fix\n            # crash when there are submodules and an adjusted branch is checked\n            # out, 2019-10-23).\n            for ds in [ds_src, ds_src_s0, ds_src_s1]:\n                maybe_adjust_repo(ds.repo)\n            ds_src.save(recursive=True)\n        assert_repo_status(ds_src.path)\n    \n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        ds_clone_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n    \n>       ds_src_s1.repo.checkout(DEFAULT_BRANCH + \"^0\")\n\nr2e_tests/test_1.py:760: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af80da1220>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout 'dl-test-branch^0'' failed with exitcode 1 [err: 'error: pathspec 'dl-test-branch^0' did not match any file(s) known to git']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  save (notneeded: 2, ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1 (dataset)\naction summary:\n  install (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0/s1 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/source/s0/s1 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detachedcbl2ktrj/clone/s0/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_update_unborn_master ___________________________\n\npath = '/tmp/datalad_temp_test_update_unborn_masteramk6h4v_'\n\n    @with_tempfile(mkdir=True)\n    def test_update_unborn_master(path=None):\n        ds_a = Dataset(op.join(path, \"ds-a\")).create()\n>       ds_a.repo.call_git([\"branch\", \"-m\", DEFAULT_BRANCH, \"other\"])\n\nr2e_tests/test_1.py:832: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af808c8610>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none branch -m dl-test-branch other' failed with exitcode 128 [err: 'error: refname refs/heads/dl-test-branch not found\nE               fatal: Branch rename failed']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n________________ test_update_follow_parentds_lazy_other_branch _________________\n\npath = PosixPath('/tmp/datalad_temp_test_update_follow_parentds_lazy_other_branchcg9qy_z9')\n\n    @slow  # ~10s\n    @with_tempfile(mkdir=True)\n    def test_update_follow_parentds_lazy_other_branch(path=None):\n        path = Path(path)\n        ds_src = Dataset(path / \"source\").create()\n        ds_src_sub = ds_src.create(\"sub\")\n>       ds_src_sub.repo.checkout(DEFAULT_BRANCH, options=[\"-bother\"])\n\nr2e_tests/test_1.py:917: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x75af80d57370>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout -bother dl-test-branch' failed with exitcode 128 [err: 'fatal: 'dl-test-branch' is not a commit and a branch 'other' cannot be created from it']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy_other_branchcg9qy_z9/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n==================================== PASSES ====================================\n____________________________ test_update_git_smoke _____________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_git_smoket55rog0j (dataset)\nadd(ok): file.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_git_smoket55rog0j \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_git_smokehr2y_c0i to /tmp/datalad_temp_test_update_git_smoket55rog0j \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_git_smoket55rog0j) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_git_smoket55rog0j) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_git_smoket55rog0j\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_git_smokehr2y_c0i to /tmp/datalad_temp_test_update_git_smoket55rog0j\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_git_smoket55rog0j)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_git_smokehr2y_c0i\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_git_smoket55rog0j)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_update_volatile_subds __________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_volatile_subdstr7112sc (dataset)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subm 1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nupdate(ok): . (dataset)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nsubdataset(ok): subm 1 (dataset)\nuninstall(ok): subm 1 (dataset)\nremove(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  remove (ok: 1)\n  save (ok: 1)\n  uninstall (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subm 1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): load.dat (file)\nsave(ok): subm 1 (dataset)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1 (dataset) [Installed subdataset in order to get /tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1/load.dat]\nget(ok): subm 1/load.dat (file) [from origin...]\naction summary:\n  get (ok: 1)\n  install (ok: 1)\nadd(ok): probe (file)\nsave(ok): subm 1 (dataset)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nadd(ok): mike (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 4)\n  save (ok: 2)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): subm 1 (dataset) [Merged origin/master]\nupdate.annex_merge(ok): subm 1 (dataset) [Merged annex branch]\nupdate(ok): subm 1 (dataset)\naction summary:\n  merge (ok: 2)\n  save (notneeded: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 2)\nuninstall(ok): subm 1 (dataset)\nremove(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  remove (ok: 1)\n  save (ok: 1)\n  uninstall (ok: 1)\nsubdataset(ok): subm 1 (dataset)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nuninstall(ok): . (dataset)\ncreate(ok): . (dataset)\ninstall(ok): other (dataset)\nadd(ok): other (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  install (ok: 1)\n  save (ok: 2)\nadd(ok): brand (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): other (dataset) [Merged origin/master]\nupdate.annex_merge(ok): other (dataset) [Merged annex branch]\nupdate(ok): other (dataset)\nadd(ok): other (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  merge (ok: 2)\n  save (ok: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_volatile_subdstr7112sc \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds30a42nf6 to /tmp/datalad_temp_test_update_volatile_subdstr7112sc \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds30a42nf6/subm 1 to /tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1 \n[INFO] Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1') \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset('/tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1') \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_volatile_subdstr7112sc/other \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds5q349802 to /tmp/datalad_temp_test_update_volatile_subdstr7112sc/other \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc/other) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc/other) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_volatile_subdstr7112sc\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds30a42nf6 to /tmp/datalad_temp_test_update_volatile_subdstr7112sc\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (1/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (2/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (3/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (4/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6/subm 1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  20% (1/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  40% (2/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  60% (3/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  80% (4/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (5/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (5/5), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds30a42nf6/subm 1 to /tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1')\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Start annex action: {'command': 'get', 'note': 'from origin...', 'input': ['load.dat'], 'key': 'MD5E-s5--7cfe64ea44dc3bbeb63b29ff3039a481.dat', 'file': 'load.dat'}\nINFO     datalad.annex:log.py:432 100%\nINFO     datalad.annex:log.py:432 Finished annex action: None\nINFO     datalad.annex:log.py:432 Finished annex get\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6/subm 1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset('/tmp/datalad_temp_test_update_volatile_subdstr7112sc/subm 1')\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdstr7112sc\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds30a42nf6\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (1/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (2/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (3/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (4/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_volatile_subdstr7112sc/other\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds5q349802 to /tmp/datalad_temp_test_update_volatile_subdstr7112sc/other\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc/other)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdstr7112sc\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdstr7112sc\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds5q349802\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subdstr7112sc/other)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdstr7112sc\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n______________________________ test_reobtain_data ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_reobtain_datan8c9olk7 (dataset)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nadd(ok): load.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nget(ok): load.dat (file) [from origin...]\nadd(ok): novel (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  get (notneeded: 1)\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nadd(ok): load.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nget(ok): load.dat (file) [from origin...]\nget(ok): load.dat (file) [from origin...]\nupdate(ok): . (dataset)\naction summary:\n  get (ok: 1)\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_reobtain_datan8c9olk7 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_reobtain_data8k14udq8 to /tmp/datalad_temp_test_reobtain_datan8c9olk7 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Ensuring content availability for 1 previously available files \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Ensuring content availability for 1 previously available files \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_reobtain_datan8c9olk7\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_reobtain_data8k14udq8 to /tmp/datalad_temp_test_reobtain_datan8c9olk7\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_reobtain_data8k14udq8\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Start annex action: {'command': 'get', 'note': 'from origin...', 'input': ['load.dat'], 'key': 'MD5E-s5--7cfe64ea44dc3bbeb63b29ff3039a481.dat', 'file': 'load.dat'}\nINFO     datalad.annex:log.py:432 100%\nINFO     datalad.annex:log.py:432 Finished annex action: None\nINFO     datalad.annex:log.py:432 Finished annex get\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_reobtain_data8k14udq8\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:607 Ensuring content availability for 1 previously available files\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_reobtain_data8k14udq8\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   9% (1/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  18% (2/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  27% (3/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  36% (4/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  45% (5/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  54% (6/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  63% (7/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  72% (8/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  81% (9/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (10/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (11/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (11/11), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datan8c9olk7)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:607 Ensuring content availability for 1 previously available files\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Start annex action: {'command': 'get', 'note': 'from origin...', 'input': ['load.dat'], 'key': 'MD5E-s5--2ac43aa43bf473f9a9c09b4b608619d3.dat', 'file': 'load.dat'}\nINFO     datalad.annex:log.py:432 100%\nINFO     datalad.annex:log.py:432 Finished annex action: None\nINFO     datalad.annex:log.py:432 Finished annex get\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_____________________________ test_multiway_merge ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\n.: r1(+) [/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_r1 (git)]\n.: r2(-) [/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_r2 (git)]\npublish(ok): . (dataset) [refs/heads/git-annex->r1:refs/heads/git-annex 9d74399..cf62095]\npublish(ok): . (dataset) [refs/heads/master->r1:refs/heads/master [new branch]]\naction summary:\n  publish (ok: 2)\npublish(ok): . (dataset) [refs/heads/master->r2:refs/heads/master [new branch]]\npublish(ok): . (dataset) [refs/heads/git-annex->r2:refs/heads/git-annex [new branch]]\naction summary:\n  copy (notneeded: 1)\n  publish (ok: 2)\nupdate(ok): . (dataset)\nupdate(impossible): . (dataset) [Multiple siblings, please specify from which to update.]\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Determine push target \n[INFO] Push refspecs \n[INFO] Transfer data \n[INFO] Update availability information \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Start writing objects \n[INFO] Finished push of Dataset(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig) \n[INFO] Determine push target \n[INFO] Push refspecs \n[INFO] Transfer data \n[INFO] Update availability information \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Start writing objects \n[INFO] Finished push of Dataset(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig) \n[INFO] Start fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig) \n[INFO] Fetching remote r2 \n[INFO] Fetching remote r1 \n[INFO] Finished fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Determine push target\nINFO     datalad.core.distributed.push:log.py:432 Push refspecs\nINFO     datalad.core.distributed.push:log.py:432 Transfer data\nINFO     datalad.core.distributed.push:log.py:432 Update availability information\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 Counting objects:   6% (1/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  13% (2/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  20% (3/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  26% (4/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  33% (5/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  40% (6/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  46% (7/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  53% (8/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  60% (9/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  66% (10/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  73% (11/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  80% (12/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  86% (13/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  93% (14/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (15/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (15/15), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  11% (1/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  22% (2/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  33% (3/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  44% (4/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  55% (5/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  66% (6/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  77% (7/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  88% (8/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (9/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (9/9), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.gitrepo:log.py:432 Start writing objects\nINFO     datalad.gitrepo:log.py:432 Writing objects:   8% (1/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  16% (2/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  25% (3/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  33% (4/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  41% (5/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  50% (6/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  58% (7/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  66% (8/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  75% (9/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  83% (10/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  91% (11/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (12/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (12/12), 1.13 KiB | 1.13 MiB/s, done.\nINFO     datalad.gitrepo:log.py:432 Finished writing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Finished push of Dataset(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Determine push target\nINFO     datalad.core.distributed.push:log.py:432 Push refspecs\nINFO     datalad.core.distributed.push:log.py:432 Transfer data\nINFO     datalad.core.distributed.push:log.py:432 Update availability information\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 Counting objects:   5% (1/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  11% (2/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  17% (3/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  23% (4/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  29% (5/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  35% (6/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  41% (7/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  47% (8/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  52% (9/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  58% (10/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  64% (11/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  70% (12/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  76% (13/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  82% (14/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  88% (15/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  94% (16/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (17/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (17/17), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 Compressing objects:   8% (1/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  16% (2/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  25% (3/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  33% (4/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  41% (5/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  50% (6/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  58% (7/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  66% (8/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  75% (9/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  83% (10/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  91% (11/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (12/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (12/12), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.gitrepo:log.py:432 Start writing objects\nINFO     datalad.gitrepo:log.py:432 Writing objects:   5% (1/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  11% (2/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  17% (3/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  23% (4/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  29% (5/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  35% (6/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  41% (7/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  47% (8/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  52% (9/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  58% (10/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  64% (11/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  70% (12/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  76% (13/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  82% (14/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  88% (15/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  94% (16/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (17/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (17/17), 1.35 KiB | 1.35 MiB/s, done.\nINFO     datalad.gitrepo:log.py:432 Finished writing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Finished push of Dataset(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig)\nINFO     datalad.gitrepo:log.py:432 Start fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig)\nINFO     datalad.gitrepo:log.py:432 Fetching remote r2\nINFO     datalad.gitrepo:log.py:432 Fetching remote r1\nINFO     datalad.gitrepo:log.py:432 Finished fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_mergenek67f4y/ds_orig)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_________________________ test_unrelated_history_merge _________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\n.: repo(+) [/tmp/pytest-of-gcpuser/pytest-624/test_unrelated_history_merge0/repo (git)]\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge repo/master' failed with exitcode 128 [err: 'fatal: refusing to merge unrelated histories'])]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of repo/master failed]\naction summary:\n  merge (error: 1)\n  update (error: 1)\n  update.annex_merge (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/pytest-of-gcpuser/pytest-624/test_unrelated_history_merge0/ds) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Finished annex add\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/pytest-of-gcpuser/pytest-624/test_unrelated_history_merge0/ds)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   7% (1/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  15% (2/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  23% (3/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (4/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  38% (5/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  46% (6/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  53% (7/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  61% (8/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  69% (9/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  76% (10/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  84% (11/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  92% (12/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (13/13)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (13/13), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  11% (1/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  22% (2/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  33% (3/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  44% (4/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  55% (5/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  66% (6/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  77% (7/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  88% (8/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (9/9)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (9/9), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_____________________________ test_merge_conflict ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_conflictvnvd71et/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1 (dataset)\naction summary:\n  install (ok: 3)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of origin/master failed]\naction summary:\n  merge (error: 1)\n  update (error: 1)\n  update.annex_merge (ok: 1)\nadd(ok): foo (file)\nsave(ok): s1 (dataset)\nadd(ok): foo (file)\nsave(ok): s0 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 5)\n  save (ok: 3)\nadd(ok): foo (file)\nsave(ok): s0 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (notneeded: 1, ok: 2)\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of origin/master failed]\nmerge(error): s0 (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): s0 (dataset) [Merged annex branch]\nupdate(error): s0 (dataset) [Update of origin/master failed]\nmerge(ok): s1 (dataset) [Merged origin/master]\nupdate.annex_merge(ok): s1 (dataset) [Merged annex branch]\nupdate(ok): s1 (dataset)\naction summary:\n  merge (error: 2, ok: 1)\n  update (error: 2, ok: 1)\n  update.annex_merge (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflictvnvd71et/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflictvnvd71et/src to /tmp/datalad_temp_test_merge_conflictvnvd71et/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone) to get /tmp/datalad_temp_test_merge_conflictvnvd71et/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflictvnvd71et/src/s0 to /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflictvnvd71et/src/s1 to /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[WARNING] Not saving because top-level dataset /tmp/datalad_temp_test_merge_conflictvnvd71et/clone had an update failure in subdataset \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflictvnvd71et/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflictvnvd71et/src to /tmp/datalad_temp_test_merge_conflictvnvd71et/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone) to get /tmp/datalad_temp_test_merge_conflictvnvd71et/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflictvnvd71et/src/s0 to /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflictvnvd71et/src/s1 to /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflictvnvd71et/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s0)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflictvnvd71et/clone/s1)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nWARNING  datalad.distribution.update:update.py:434 Not saving because top-level dataset /tmp/datalad_temp_test_merge_conflictvnvd71et/clone had an update failure in subdataset\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________________ test_merge_conflict_in_subdataset_only ____________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub_conflict (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub_conflict (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): sub_noconflict (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub_noconflict (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict (dataset)\naction summary:\n  install (ok: 3)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(error): sub_conflict (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): sub_conflict (dataset) [Merged annex branch]\nupdate(error): sub_conflict (dataset) [Update of origin/master failed]\nmerge(ok): sub_noconflict (dataset) [Merged origin/master]\nupdate.annex_merge(ok): sub_noconflict (dataset) [Merged annex branch]\nupdate(ok): sub_noconflict (dataset)\nadd(ok): sub_noconflict (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  merge (error: 1, ok: 2)\n  save (ok: 1)\n  update (error: 1, ok: 2)\n  update.annex_merge (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src to /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone) to get /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src/sub_conflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src/sub_noconflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src to /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone) to get /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src/sub_conflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src/sub_noconflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src/sub_conflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/src/sub_noconflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_conflict)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone/sub_noconflict)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_onlyl0ftpe8l/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n______________________________ test_merge_ff_only ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff (dataset)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): bar (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge --ff-only origin/master' failed with exitcode 128 [err: 'fatal: Not possible to fast-forward, aborting.'])]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of origin/master failed]\naction summary:\n  merge (error: 1)\n  update (error: 1)\n  update.annex_merge (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_ff_only51b7ctwi/src to /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_ff_only51b7ctwi/src to /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_ff_only51b7ctwi/src to /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_ff_only51b7ctwi/src to /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_ff_only51b7ctwi/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_ff)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_only51b7ctwi/clone_nonff)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_______________________ test_update_follow_parentds_lazy _______________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s2 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  save (notneeded: 5, ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1 (dataset)\naction summary:\n  install (ok: 6)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): s1 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): s0 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 6)\n  save (notneeded: 3, ok: 3)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): s0 (dataset) [Merged 2d9f8d0769616239103644c56e92f05f081c1c9c]\nupdate.annex_merge(ok): s0 (dataset) [Merged annex branch]\nupdate(ok): s0 (dataset)\nmerge(ok): s0/s0 (dataset) [Merged f33344c4479d10b6e5edbccaab8b4b244c7674e6]\nupdate.annex_merge(ok): s0/s0 (dataset) [Merged annex branch]\nupdate(ok): s0/s0 (dataset)\nmerge(ok): s1 (dataset) [Merged 9e5aaacc08f354899bf28b09a39d3189125e659b]\nupdate.annex_merge(ok): s1 (dataset) [Merged annex branch]\nupdate(ok): s1 (dataset)\naction summary:\n  merge (ok: 4)\n  save (notneeded: 2)\n  update (notneeded: 2, ok: 4)\n  update.annex_merge (ok: 4)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone) to get /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s2 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s2\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s0\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone) to get /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s2 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0/s0)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s1)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone/s0\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy2e3qva4z/clone\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n________________ test_update_adjusted_incompatible_with_ff_only ________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone (dataset)\nupdate(impossible): . (dataset) [Updating via 'ff-only' is incompatible with adjusted branches]\nupdate(ok): . (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/source to /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone) to get /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/source to /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone) to get /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only1kg_ixv9/clone)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_______________ test_update_how_subds_different[parentds-reset] ________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub (dataset)\naction summary:\n  install (ok: 2)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): bar (file)\nsave(ok): sub (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nadd(ok): baz (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nupdate.reset(ok): sub (dataset) [Reset to 52819a6e7666f3ae5fc755c9811c0f895d01d305]\nupdate(ok): sub (dataset)\naction summary:\n  merge (ok: 1)\n  save (notneeded: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 1)\n  update.reset (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source to /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone) to get /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source/sub to /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source to /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone) to get /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source/sub to /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone/sub)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   5% (1/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  11% (2/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  16% (3/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  22% (4/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  27% (5/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (6/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  38% (7/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  44% (8/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (9/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  55% (10/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  61% (11/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (12/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  72% (13/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  77% (14/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  83% (15/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  88% (16/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  94% (17/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:   7% (1/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (2/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  21% (3/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (4/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  35% (5/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (6/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (7/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (8/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  64% (9/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (10/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  78% (11/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (12/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  92% (13/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different7hawfk69/clone\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n______________ test_update_how_subds_different[sibling-checkout] _______________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub (dataset)\naction summary:\n  install (ok: 2)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): bar (file)\nsave(ok): sub (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nadd(ok): baz (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nupdate.checkout(ok): sub (dataset) [Checkout origin/master]\nupdate(ok): sub (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  merge (ok: 1)\n  save (ok: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 1)\n  update.checkout (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source to /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone) to get /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source/sub to /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source to /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone) to get /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source/sub to /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone/sub)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   5% (1/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  11% (2/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  16% (3/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  22% (4/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  27% (5/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (6/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  38% (7/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  44% (8/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (9/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  55% (10/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  61% (11/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (12/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  72% (13/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  77% (14/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  83% (15/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  88% (16/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  94% (17/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:   7% (1/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (2/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  21% (3/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (4/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  35% (5/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (6/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (7/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (8/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  64% (9/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (10/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  78% (11/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (12/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  92% (13/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentz8twtuf0/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n___________________________ test_update_reset_dirty ____________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s2 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2 (dataset)\naction summary:\n  install (ok: 3)\nadd(ok): bar (file)\nsave(ok): s2 (dataset)\nadd(ok): foo (file)\nsave(ok): s1 (dataset)\nadd(ok): s1 (dataset)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 5)\n  save (ok: 3)\nupdate.reset(error): . (dataset) [Refusing to reset dirty working tree]\nupdate(error): . (dataset) [Update of origin/master failed]\nupdate.reset(error): s1 (dataset) [Refusing to reset dirty working tree]\nupdate(error): s1 (dataset) [Update of origin/master failed]\nupdate.reset(ok): s2 (dataset) [Reset to origin/master]\nupdate(ok): s2 (dataset)\naction summary:\n  update (error: 2, ok: 1)\n  update.reset (error: 2, ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source to /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone) to get /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source/s1 to /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source/s2 to /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[WARNING] Not saving because top-level dataset /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone had an update failure in subdataset \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source to /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone) to get /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source/s1 to /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source/s2 to /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s1)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone/s2)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nWARNING  datalad.distribution.update:update.py:434 Not saving because top-level dataset /tmp/datalad_temp_test_update_reset_dirty2arsw7ab/clone had an update failure in subdataset\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_update_fetch_failure ___________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s2 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2 (dataset)\naction summary:\n  install (ok: 3)\nupdate(ok): . (dataset)\nupdate(error): s1 (dataset) [Fetch failed: CommandError(CommandError: 'git -c diff.ignoreSubmodules=none fetch --verbose --progress --no-recurse-submodules --prune origin' failed with exitcode 128 under /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1 [err: 'fatal: '/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a/s1' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.'])] [CommandError: 'git -c diff.ignoreSubmodules=none fetch --verbose --progress --no-recurse-submodules --prune origin' failed with exitcode 128 under /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1 [err: 'fatal: '/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a/s1' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.']]\nupdate(ok): s2 (dataset)\naction summary:\n  update (error: 1, ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a to /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b) to get /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a/s1 to /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a/s2 to /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a to /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b) to get /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a/s1 to /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds_a/s2 to /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s1)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failureh43jsz8q/ds-b/s2)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::test_update_git_smoke\nPASSED r2e_tests/test_1.py::test_update_volatile_subds\nPASSED r2e_tests/test_1.py::test_reobtain_data\nPASSED r2e_tests/test_1.py::test_multiway_merge\nPASSED r2e_tests/test_1.py::test_unrelated_history_merge\nPASSED r2e_tests/test_1.py::test_merge_conflict\nPASSED r2e_tests/test_1.py::test_merge_conflict_in_subdataset_only\nPASSED r2e_tests/test_1.py::test_merge_ff_only\nPASSED r2e_tests/test_1.py::test_update_follow_parentds_lazy\nPASSED r2e_tests/test_1.py::test_update_adjusted_incompatible_with_ff_only\nPASSED r2e_tests/test_1.py::test_update_how_subds_different[parentds-reset]\nPASSED r2e_tests/test_1.py::test_update_how_subds_different[sibling-checkout]\nPASSED r2e_tests/test_1.py::test_update_reset_dirty\nPASSED r2e_tests/test_1.py::test_process_how_args\nPASSED r2e_tests/test_1.py::test_update_fetch_failure\nSKIPPED [1] .venv/lib/python3.9/site-packages/_pytest/unittest.py:385: 'git annex init' with adjusted branch currently fails due to hard-coded 'origin'\nFAILED r2e_tests/test_1.py::test_update_simple - datalad.support.exceptions.R...\nFAILED r2e_tests/test_1.py::test_update_fetch_all - datalad.runner.exception....\nFAILED r2e_tests/test_1.py::test_newthings_coming_down - AssertionError: asse...\nFAILED r2e_tests/test_1.py::test_merge_no_merge_target - datalad.runner.excep...\nFAILED r2e_tests/test_1.py::test_merge_follow_parentds_subdataset_other_branch\nFAILED r2e_tests/test_1.py::test_merge_follow_parentds_subdataset_adjusted_warning\nFAILED r2e_tests/test_1.py::test_merge_follow_parentds_subdataset_detached[False]\nFAILED r2e_tests/test_1.py::test_update_unborn_master - datalad.runner.except...\nFAILED r2e_tests/test_1.py::test_update_follow_parentds_lazy_other_branch - d...\n============== 9 failed, 15 passed, 1 skipped in 64.65s (0:01:04) ==============\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/datalad_d506f925df22667910adb647463856cbcee325c5\nconfigfile: tox.ini\nplugins: fail-slow-0.6.0, xdist-3.6.1, cov-6.0.0\ncollected 25 items\n\nr2e_tests/test_1.py F.FF...FF...FFsFF.F......                            [100%]\n\n=================================== FAILURES ===================================\n______________________________ test_update_simple ______________________________\n\nself = AnnexRepo('/tmp/datalad_temp_test_update_simplet2jrde18')\nname = 'dl-test-remote'\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n    \n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n>           self.call_git(['remote', 'remove', name])\n\ndatalad/support/gitrepo.py:1863: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d17c15d30>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none remote remove dl-test-remote' failed with exitcode 2 [err: 'error: No such remote: 'dl-test-remote'']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n\nDuring handling of the above exception, another exception occurred:\n\norigin = '/tmp/datalad_temp_test_update_simplebtzs6b80'\nsrc_path = '/tmp/datalad_temp_test_update_simplet2jrde18'\ndst_path = '/tmp/datalad_temp_test_update_simplefzqmsskd'\n\n    @slow\n    @with_tempfile(mkdir=True)\n    @with_tempfile(mkdir=True)\n    @with_tempfile(mkdir=True)\n    def test_update_simple(origin=None, src_path=None, dst_path=None):\n        ca = dict(result_renderer='disabled')\n        # a remote dataset with a subdataset underneath\n        origds = Dataset(origin).create(**ca)\n        # naming is weird, but a legacy artifact\n        _ = origds.create('subm 1', **ca)\n        _ = origds.create('2', **ca)\n    \n        # prepare src\n        source = install(src_path, source=origin, recursive=True)\n        # forget we cloned it by removing remote, which should lead to\n        # setting tracking branch to target:\n>       source.repo.remove_remote(DEFAULT_REMOTE)\n\nr2e_tests/test_1.py:81: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = AnnexRepo('/tmp/datalad_temp_test_update_simplet2jrde18')\nname = 'dl-test-remote'\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n    \n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n>               raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\nE               datalad.support.exceptions.RemoteNotAvailableError: Remote 'dl-test-remote' is not available. Command failed:\nE               RemoteNotAvailableError: 'git remote remove' [No such remote] [err: 'error: No such remote: 'dl-test-remote'']\n\ndatalad/support/gitrepo.py:1866: RemoteNotAvailableError\n----------------------------- Captured stdout call -----------------------------\ninstall(ok): /tmp/datalad_temp_test_update_simplet2jrde18 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_simplet2jrde18/2 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_simplet2jrde18/subm 1 (dataset)\naction summary:\n  install (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_simplet2jrde18 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_simplebtzs6b80 to /tmp/datalad_temp_test_update_simplet2jrde18 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simplet2jrde18) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_simplet2jrde18) to get /tmp/datalad_temp_test_update_simplet2jrde18 \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_simplet2jrde18/2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_simplebtzs6b80/2 to /tmp/datalad_temp_test_update_simplet2jrde18/2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simplet2jrde18/2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_simplet2jrde18/subm 1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_simplebtzs6b80/subm 1 to /tmp/datalad_temp_test_update_simplet2jrde18/subm 1 \n[INFO] Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_simplet2jrde18/subm 1') \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_simplebtzs6b80\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_simplebtzs6b80\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_simplet2jrde18\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_simplebtzs6b80 to /tmp/datalad_temp_test_update_simplet2jrde18\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simplet2jrde18)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_simplet2jrde18) to get /tmp/datalad_temp_test_update_simplet2jrde18\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_simplet2jrde18/2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_simplebtzs6b80/2 to /tmp/datalad_temp_test_update_simplet2jrde18/2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_simplet2jrde18/2)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_simplet2jrde18/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_simplebtzs6b80/subm 1 to /tmp/datalad_temp_test_update_simplet2jrde18/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_simplet2jrde18/subm 1')\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_simplet2jrde18/2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_simplet2jrde18/subm 1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________________________ test_update_fetch_all _____________________________\n\npath = PosixPath('/tmp/datalad_temp_test_update_fetch_allp5ejed67')\n\n    @slow  # ~9s\n    @with_tempfile(mkdir=True)\n    def test_update_fetch_all(path=None):\n        path = Path(path)\n        remote_1 = str(path / \"remote_1\")\n        remote_2 = str(path / \"remote_2\")\n    \n        ds = Dataset(path / \"src\").create()\n        src = ds.repo.path\n    \n        ds_rmt1 = clone(source=src, path=remote_1)\n        ds_rmt2 = clone(source=src, path=remote_2)\n    \n        ds.siblings('add', name=\"sibling_1\", url=remote_1)\n        ds.siblings('add', name=\"sibling_2\", url=remote_2)\n    \n        # modify the remotes:\n        (ds_rmt1.pathobj / \"first.txt\").write_text(\"some file load\")\n        ds_rmt1.save()\n    \n        # TODO: Modify an already present file!\n    \n        (ds_rmt2.pathobj / \"second.txt\").write_text(\"different file load\")\n        ds_rmt2.save()\n    \n        # Let's init some special remote which we couldn't really update/fetch\n        if not dl_cfg.get('datalad.tests.dataladremote'):\n>           ds.repo.init_remote(\n                'datalad',\n                ['encryption=none', 'type=external', 'externaltype=datalad'])\n\nr2e_tests/test_1.py:246: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/annexrepo.py:2002: in init_remote\n    self.call_annex(['initremote'] + [name] + options)\ndatalad/support/annexrepo.py:1267: in call_annex\n    return self._call_annex(\ndatalad/support/annexrepo.py:944: in _call_annex\n    return runner.run(\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.gitrunner.GitWitlessRunner object at 0x7a8d1e3410c0>\ncmd = ['git', '-c', 'diff.ignoreSubmodules=none', 'annex', 'initremote', 'datalad', ...]\nprotocol = <class 'datalad.runner.coreprotocols.StdOutErrCapture'>, stdin = None\ncwd = None, env = None, timeout = None, exception_on_error = True, kwargs = {}\napplied_cwd = PosixPath('/tmp/datalad_temp_test_update_fetch_allp5ejed67/src')\napplied_env = {'ANTHROPIC_API_KEY': 'sk-ant-api03-kztdHBP1RruInQ2uzw8m4wJLbhPHBrb-qHX2mhAKmwoMFvM_dI2oWb5V6JtS45MScuW5CCjPusIrt9Zatx...3b4166bef66ef252935c/server/bin/helpers/browser.sh', 'COHERE_API_KEY': '66TDU3MmN5ELJT1NHWklGG2PJjK2zwVSQbVq42ol', ...}\nthreaded_runner = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d1706ac70>\nresults_or_iterator = {'code': 1, 'stderr': \"  Cannot run /home/gcpuser/r2e-edits-internal/.venv/bin/git-annex-remote-datalad -- Make sure i...ecial remote git-annex-remote-datalad\\ngit-annex: initremote: 1 failed\\n\", 'stdout': 'initremote datalad \\n\\nfailed\\n'}\nresults = {'code': 1, 'stderr': \"  Cannot run /home/gcpuser/r2e-edits-internal/.venv/bin/git-annex-remote-datalad -- Make sure i...ecial remote git-annex-remote-datalad\\ngit-annex: initremote: 1 failed\\n\", 'stdout': 'initremote datalad \\n\\nfailed\\n'}\nreturn_code = 1\n\n    def run(self,\n            cmd: list | str,\n            protocol: type[WitlessProtocol] | None = None,\n            stdin: bytes | IO | Queue | None = None,\n            cwd: PathLike | str | None = None,\n            env: dict | None = None,\n            timeout: float | None = None,\n            exception_on_error: bool = True,\n            **kwargs) -> dict | _ResultGenerator:\n        \"\"\"Execute a command and communicate with it.\n    \n        Parameters\n        ----------\n        cmd : list or str\n          Sequence of program arguments. Passing a single string causes\n          execution via the platform shell.\n        protocol : WitlessProtocol, optional\n          Protocol class handling interaction with the running process\n          (e.g. output capture). A number of pre-crafted classes are\n          provided (e.g `KillOutput`, `NoCapture`, `GitProgress`).\n          If the protocol has the GeneratorMixIn-mixin, the run-method\n          will return an iterator and can therefore be used in a for-clause.\n        stdin : file-like, bytes, Queue, or None\n          If stdin is a file-like, it will be directly used as stdin for the\n          subprocess. The caller is responsible for writing to it and closing it.\n          If stdin is a bytes, it will be fed to stdin of the subprocess.\n          If all data is written, stdin will be closed.\n          If stdin is a Queue, all elements (bytes) put into the Queue will\n          be passed to stdin until None is read from the queue. If None is read,\n          stdin of the subprocess is closed.\n        cwd : str or path-like, optional\n          If given, commands are executed with this path as PWD,\n          the PWD of the parent process is used otherwise. Overrides\n          any `cwd` given to the constructor.\n        env : dict, optional\n          Environment to be used for command execution. If given, it will\n          completely replace any environment provided to theconstructor. If\n          `cwd` is given, 'PWD' in the environment is set to its value.\n          This must be a complete environment definition, no values\n          from the current environment will be inherited. Overrides\n          any `env` given to the constructor.\n        timeout: float, optional\n          None or the seconds after which a timeout callback is\n          invoked, if no progress was made in communicating with\n          the sub-process, or if waiting for the subprocess exit\n          took more than the specified time. See the protocol and\n          `ThreadedRunner` descriptions for a more detailed discussion\n          on timeouts.\n        exception_on_error : bool, optional\n          This argument is first interpreted if the protocol is a subclass\n          of `GeneratorMixIn`. If it is `True` (default), a\n          `CommandErrorException` is raised by the generator if the\n          sub process exited with a return code not equal to zero. If the\n          parameter is `False`, no exception is raised. In both cases the\n          return code can be read from the attribute `return_code` of\n          the generator. Then this argument interpreted within this function\n          to not raise `CommandError` if value is False in case of non-0 exit.\n        kwargs :\n          Passed to the Protocol class constructor.\n    \n        Returns\n        -------\n          dict | _ResultGenerator\n    \n            If the protocol is not a subclass of `GeneratorMixIn`, the\n            result of protocol._prepare_result will be returned.\n    \n            If the protocol is a subclass of `GeneratorMixIn`, a Generator, i.e.\n            a `_ResultGenerator`, will be returned. This allows to use this\n            method in constructs like:\n    \n                for protocol_output in runner.run():\n                    ...\n    \n            Where the iterator yields whatever protocol.pipe_data_received\n            sends into the generator.\n            If all output was yielded and the process has terminated, the\n            generator will raise StopIteration(return_code), where\n            return_code is the return code of the process. The return code\n            of the process will also be stored in the \"return_code\"-attribute\n            of the runner. So you could write:\n    \n               gen = runner.run()\n               for file_descriptor, data in gen:\n                   ...\n    \n               # get the return code of the process\n               result = gen.return_code\n    \n        Raises\n        ------\n        CommandError\n          On execution failure (non-zero exit code) this exception is\n          raised which provides the command (cmd), stdout, stderr,\n          exit code (status), and a message identifying the failed\n          command, as properties.\n        FileNotFoundError\n          When a given executable does not exist.\n        \"\"\"\n        if protocol is None:\n            # by default let all subprocess stream pass through\n            protocol = NoCapture\n    \n        applied_cwd = cwd or self.cwd\n        applied_env = self._get_adjusted_env(\n            env=env or self.env,\n            cwd=applied_cwd,\n        )\n    \n        lgr.debug(\n            'Run %r (protocol_class=%s) (cwd=%s)',\n            cmd,\n            protocol.__name__,\n            applied_cwd\n        )\n    \n        threaded_runner = ThreadedRunner(\n            cmd=cmd,\n            protocol_class=protocol,\n            stdin=stdin,\n            protocol_kwargs=kwargs,\n            timeout=timeout,\n            exception_on_error=exception_on_error,\n            cwd=applied_cwd,\n            env=applied_env\n        )\n    \n        results_or_iterator = threaded_runner.run()\n        if issubclass(protocol, GeneratorMixIn):\n            return results_or_iterator\n    \n        results = cast(dict, results_or_iterator)\n        # log before any exception is raised\n        lgr.debug(\"Finished %r with status %s\", cmd, results['code'])\n    \n        # make it such that we always blow if a protocol did not report\n        # a return code at all or it was non-0 and we were not asked ignore\n        # errors\n        return_code = results.get('code', None)\n        if return_code is None or (return_code and exception_on_error):\n            # the runner has a better idea, doc string warns Protocol\n            # implementations not to return these\n            results.pop('cmd', None)\n            results.pop('cwd', None)\n>           raise CommandError(\n                # whatever the results were, we carry them forward\n                cmd=cmd,\n                cwd=applied_cwd,\n                **results,\n            )\nE           datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none annex initremote datalad encryption=none type=external externaltype=datalad -c annex.dotfiles=true' failed with exitcode 1 under /tmp/datalad_temp_test_update_fetch_allp5ejed67/src [out: 'initremote datalad \nE           \nE           failed'] [err: 'Cannot run /home/gcpuser/r2e-edits-internal/.venv/bin/git-annex-remote-datalad -- Make sure it's executable and that its dependencies are installed.\nE           git-annex: unable to use external special remote git-annex-remote-datalad\nE           git-annex: initremote: 1 failed']\n\ndatalad/runner/runner.py:223: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2 (dataset)\n.: sibling_1(+) [/tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1 (git)]\n.: sibling_2(+) [/tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2 (git)]\nadd(ok): first.txt (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): second.txt (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_allp5ejed67/src to /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_allp5ejed67/src to /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_allp5ejed67/src to /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_allp5ejed67/src to /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_allp5ejed67/remote_2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_newthings_coming_down __________________________\n\noriginpath = '/tmp/datalad_temp_test_newthings_coming_downbioryift'\ndestpath = '/tmp/datalad_temp_test_newthings_coming_downwk5jkp6z'\n\n    @with_tempfile(mkdir=True)\n    @with_tempfile(mkdir=True)\n    def test_newthings_coming_down(originpath=None, destpath=None):\n        origin = GitRepo(originpath, create=True)\n        create_tree(originpath, {'load.dat': 'heavy'})\n        Dataset(originpath).save('load.dat')\n        ds = install(\n            source=originpath, path=destpath,\n            result_xfm='datasets', return_type='item-or-list')\n        assert_is_instance(ds.repo, GitRepo)\n>       assert_in(DEFAULT_REMOTE, ds.repo.get_remotes())\n\nr2e_tests/test_1.py:294: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nfirst = 'dl-test-remote', second = ['origin'], msg = None\n\n    def assert_in(first, second, msg=None):\n        if msg is None:\n>           assert first in second\nE           AssertionError: assert 'dl-test-remote' in ['origin']\n\ndatalad/tests/utils_pytest.py:99: AssertionError\n----------------------------- Captured stdout call -----------------------------\nadd(ok): load.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_newthings_coming_downwk5jkp6z (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] Attempting a clone into /tmp/datalad_temp_test_newthings_coming_downwk5jkp6z \n[INFO] Attempting to clone from /tmp/datalad_temp_test_newthings_coming_downbioryift to /tmp/datalad_temp_test_newthings_coming_downwk5jkp6z \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_newthings_coming_downwk5jkp6z) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_newthings_coming_downbioryift\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_newthings_coming_downwk5jkp6z\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_newthings_coming_downbioryift to /tmp/datalad_temp_test_newthings_coming_downwk5jkp6z\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_newthings_coming_downwk5jkp6z)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_________________________ test_unrelated_history_merge _________________________\n\nlabel = ['error']\nresults = [{'action': 'merge', 'message': ('Merged %s', 'repo/master'), 'path': '/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds', 'refds': '/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds', ...}]\n\n    def assert_status(label, results):\n        \"\"\"Verify that each status dict in the results has a given status label\n    \n        `label` can be a sequence, in which case status must be one of the items\n        in this sequence.\n        \"\"\"\n        label = ensure_list(label)\n        results = ensure_result_list(results)\n        if len(results) == 0:\n            # If there are no results, an assertion about all results must fail.\n            raise AssertionError(\"No results retrieved\")\n        for i, r in enumerate(results):\n            try:\n                assert_in('status', r)\n>               assert_in(r['status'], label)\n\ndatalad/tests/utils_pytest.py:1310: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nfirst = 'ok', second = ['error'], msg = None\n\n    def assert_in(first, second, msg=None):\n        if msg is None:\n>           assert first in second\nE           AssertionError: assert 'ok' in ['error']\n\ndatalad/tests/utils_pytest.py:99: AssertionError\n\nDuring handling of the above exception, another exception occurred:\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0')\n\n    def test_unrelated_history_merge(tmp_path):\n        # prepare two independent datasets and try merging one into another\n        ds = Dataset(tmp_path / 'ds').create()\n        repo = AnnexRepo(tmp_path / 'repo')\n        f = (tmp_path / 'repo' / 'file.dat')\n        f.write_text(\"data\")\n        repo.add(str(f))\n        repo.commit()\n        # ATM we do not do any checks and allow such addition\n        assert_status('ok', ds.siblings(action='add', name='repo', url=repo.path))\n        res = ds.update(how='merge', on_failure='ignore')\n        # ATM we do not have any special handling, just that the first result record\n        # would have that error in \"message\" field\n>       assert_status('error', res[0])\n\nr2e_tests/test_1.py:506: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nlabel = ['error']\nresults = [{'action': 'merge', 'message': ('Merged %s', 'repo/master'), 'path': '/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds', 'refds': '/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds', ...}]\n\n    def assert_status(label, results):\n        \"\"\"Verify that each status dict in the results has a given status label\n    \n        `label` can be a sequence, in which case status must be one of the items\n        in this sequence.\n        \"\"\"\n        label = ensure_list(label)\n        results = ensure_result_list(results)\n        if len(results) == 0:\n            # If there are no results, an assertion about all results must fail.\n            raise AssertionError(\"No results retrieved\")\n        for i, r in enumerate(results):\n            try:\n                assert_in('status', r)\n                assert_in(r['status'], label)\n            except AssertionError:\n>               raise AssertionError('Test {}/{}: expected status {} not found in:\\n{}'.format(\n                    i + 1,\n                    len(results),\n                    label,\n                    dumps(results, indent=1, default=lambda x: str(x))))\nE               AssertionError: Test 1/1: expected status ['error'] not found in:\nE               [\nE                {\nE                 \"action\": \"merge\",\nE                 \"path\": \"/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds\",\nE                 \"type\": \"dataset\",\nE                 \"refds\": \"/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds\",\nE                 \"message\": [\nE                  \"Merged %s\",\nE                  \"repo/master\"\nE                 ],\nE                 \"status\": \"ok\"\nE                }\nE               ]\n\ndatalad/tests/utils_pytest.py:1312: AssertionError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\n.: repo(+) [/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/repo (git)]\nmerge(ok): . (dataset) [Merged repo/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Finished annex add\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/pytest-of-gcpuser/pytest-625/test_unrelated_history_merge0/ds)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   8% (1/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  16% (2/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (3/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (4/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  41% (5/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (6/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  58% (7/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (8/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (9/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  83% (10/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  91% (11/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (12/12)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (12/12), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  12% (1/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  25% (2/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  37% (3/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (4/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  62% (5/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  75% (6/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  87% (7/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (8/8)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (8/8), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_merge_no_merge_target __________________________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_no_merge_targetijmhpios')\n\n    @skip_if_adjusted_branch\n    @with_tempfile(mkdir=True)\n    def test_merge_no_merge_target(path=None):\n        path = Path(path)\n        ds_src = Dataset(path / \"source\").create()\n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        assert_repo_status(ds_src.path)\n>       ds_clone.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n\nr2e_tests/test_1.py:521: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d171edac0>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout -bnew dl-test-branch' failed with exitcode 128 [err: 'fatal: 'dl-test-branch' is not a commit and a branch 'new' cannot be created from it']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/source to /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone) to get /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/source to /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone) to get /tmp/datalad_temp_test_merge_no_merge_targetijmhpios/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.distribution.get:log.py:432 Installing: done\n______________ test_merge_follow_parentds_subdataset_other_branch ______________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip')\n\n    @slow  # 11sec on Yarik's laptop\n    @with_tempfile(mkdir=True)\n    def test_merge_follow_parentds_subdataset_other_branch(path=None):\n        path = Path(path)\n        ds_src = Dataset(path / \"source\").create()\n        on_adjusted = ds_src.repo.is_managed_branch()\n        ds_src_subds = ds_src.create(\"subds\")\n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n    \n        ds_src_subds.repo.call_git([\"checkout\", \"-b\", \"other\"])\n        (ds_src_subds.pathobj / \"foo\").write_text(\"foo content\")\n        ds_src.save(recursive=True)\n        assert_repo_status(ds_src.path)\n    \n        res = ds_clone.update(merge=True, follow=\"parentds\", recursive=True,\n                              on_failure=\"ignore\")\n        if on_adjusted:\n            # Our git-annex sync based on approach on adjusted branches is\n            # incompatible with follow='parentds'.\n            assert_in_results(res, action=\"update\", status=\"impossible\")\n            return\n        else:\n            assert_in_results(res, action=\"update\", status=\"ok\")\n        eq_(ds_clone.repo.get_hexsha(), ds_src.repo.get_hexsha())\n        ok_(ds_clone_subds.repo.is_under_annex(\"foo\"))\n    \n        (ds_src_subds.pathobj / \"bar\").write_text(\"bar content\")\n        ds_src.save(recursive=True)\n>       ds_clone_subds.repo.checkout(DEFAULT_BRANCH, options=[\"-bnew\"])\n\nr2e_tests/test_1.py:686: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d17c15a60>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout -bnew dl-test-branch' failed with exitcode 128 [err: 'fatal: 'dl-test-branch' is not a commit and a branch 'new' cannot be created from it']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subds (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds (dataset)\naction summary:\n  install (ok: 2)\nadd(ok): foo (file)\nsave(ok): subds (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): subds (dataset) [Merged 256695319328f6238ebef9a0ade75c897d76dedb]\nupdate.annex_merge(ok): subds (dataset) [Merged annex branch]\nupdate(ok): subds (dataset)\naction summary:\n  merge (ok: 2)\n  save (notneeded: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 2)\nadd(ok): bar (file)\nsave(ok): subds (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone/subds)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/clone\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_other_branch0riiblip/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________ test_merge_follow_parentds_subdataset_adjusted_warning ____________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c')\n\n    @skip_if_adjusted_branch\n    @with_tempfile(mkdir=True)\n    def test_merge_follow_parentds_subdataset_adjusted_warning(path=None):\n        path = Path(path)\n    \n        ds_src = Dataset(path / \"source\").create()\n        ds_src_subds = ds_src.create(\"subds\")\n    \n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        ds_clone_subds = Dataset(ds_clone.pathobj / \"subds\")\n        maybe_adjust_repo(ds_clone_subds.repo)\n        # Note: Were we to save ds_clone here, we would get a merge conflict in the\n        # top repo for the submodule (even if using 'git annex sync' rather than\n        # 'git merge').\n    \n>       ds_src_subds.repo.call_git([\"checkout\", DEFAULT_BRANCH + \"^0\"])\n\nr2e_tests/test_1.py:709: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d1707d3d0>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout 'dl-test-branch^0'' failed with exitcode 1 [err: 'error: pathspec 'dl-test-branch^0' did not match any file(s) known to git']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): subds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subds (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds (dataset)\naction summary:\n  install (ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/source/subds to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_adjusted_warningw8krny_c/clone/subds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________ test_merge_follow_parentds_subdataset_detached[False] _____________\n\npath = PosixPath('/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36')\n\n    @slow  # 12 + 21sec on Yarik's laptop\n    @pytest.mark.parametrize(\"on_adjusted\", [True, False])\n    # Skip non-adjusted case for systems that only support adjusted branches.\n    @skip_if_adjusted_branch\n    @with_tempfile(mkdir=True)\n    def test_merge_follow_parentds_subdataset_detached(path=None, *, on_adjusted):\n        if on_adjusted and DEFAULT_REMOTE != \"origin\" and \\\n           external_versions['cmd:annex'] <= \"8.20210330\":\n            raise SkipTest(\n                \"'git annex init' with adjusted branch currently fails \"\n                \"due to hard-coded 'origin'\")\n    \n        # Note: For the adjusted case, this is not much more than a smoke test that\n        # on an adjusted branch we fail sensibly. The resulting state is not easy\n        # to reason about nor desirable.\n        path = Path(path)\n        # $path/source/s0/s1\n        # The additional dataset level is to gain some confidence that this works\n        # for nested datasets.\n        ds_src = Dataset(path / \"source\").create()\n        ds_src_s0 = ds_src.create(\"s0\")\n        ds_src_s1 = ds_src_s0.create(\"s1\")\n        ds_src.save(recursive=True)\n        if on_adjusted:\n            # Note: We adjust after creating all the datasets above to avoid a bug\n            # fixed in git-annex 7.20191024, specifically bbdeb1a1a (sync: Fix\n            # crash when there are submodules and an adjusted branch is checked\n            # out, 2019-10-23).\n            for ds in [ds_src, ds_src_s0, ds_src_s1]:\n                maybe_adjust_repo(ds.repo)\n            ds_src.save(recursive=True)\n        assert_repo_status(ds_src.path)\n    \n        ds_clone = install(source=ds_src.path, path=path / \"clone\",\n                           recursive=True, result_xfm=\"datasets\")\n        ds_clone_s1 = Dataset(ds_clone.pathobj / \"s0\" / \"s1\")\n    \n>       ds_src_s1.repo.checkout(DEFAULT_BRANCH + \"^0\")\n\nr2e_tests/test_1.py:760: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d1706a910>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout 'dl-test-branch^0'' failed with exitcode 1 [err: 'error: pathspec 'dl-test-branch^0' did not match any file(s) known to git']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  save (notneeded: 2, ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1 (dataset)\naction summary:\n  install (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0/s1 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone) to get /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/source/s0/s1 to /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_follow_parentds_subdataset_detached13yk_i36/clone/s0/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_update_unborn_master ___________________________\n\npath = '/tmp/datalad_temp_test_update_unborn_masterxfv9sf38'\n\n    @with_tempfile(mkdir=True)\n    def test_update_unborn_master(path=None):\n        ds_a = Dataset(op.join(path, \"ds-a\")).create()\n>       ds_a.repo.call_git([\"branch\", \"-m\", DEFAULT_BRANCH, \"other\"])\n\nr2e_tests/test_1.py:832: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d17be9730>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none branch -m dl-test-branch other' failed with exitcode 128 [err: 'error: refname refs/heads/dl-test-branch not found\nE               fatal: Branch rename failed']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n________________ test_update_follow_parentds_lazy_other_branch _________________\n\npath = PosixPath('/tmp/datalad_temp_test_update_follow_parentds_lazy_other_branchc4b9j58d')\n\n    @slow  # ~10s\n    @with_tempfile(mkdir=True)\n    def test_update_follow_parentds_lazy_other_branch(path=None):\n        path = Path(path)\n        ds_src = Dataset(path / \"source\").create()\n        ds_src_sub = ds_src.create(\"sub\")\n>       ds_src_sub.repo.checkout(DEFAULT_BRANCH, options=[\"-bother\"])\n\nr2e_tests/test_1.py:917: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \ndatalad/support/gitrepo.py:2238: in checkout\n    self.call_git(cmd, expect_stderr=True)\ndatalad/dataset/gitrepo.py:453: in call_git\n    return \"\".join(\ndatalad/dataset/gitrepo.py:517: in call_git_items_\n    for file_no, line in self._generator_call_git(\ndatalad/dataset/gitrepo.py:355: in _generator_call_git\n    for file_no, content in generator:\n../../../../.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/_collections_abc.py:330: in __next__\n    return self.send(None)\ndatalad/runner/nonasyncrunner.py:97: in send\n    return self._locked_send(message)\ndatalad/runner/nonasyncrunner.py:122: in _locked_send\n    self._check_result()\ndatalad/runner/nonasyncrunner.py:93: in _check_result\n    self.runner._check_result()\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <datalad.runner.nonasyncrunner.ThreadedRunner object at 0x7a8d170be580>\n\n    def _check_result(self):\n        if self.exception_on_error is True:\n            if self.return_code not in (0, None):\n                protocol = self.protocol\n                decoded_output = {\n                    source: protocol.fd_infos[fileno][1].decode(protocol.encoding)\n                    for source, fileno in (\n                        (\"stdout\", protocol.stdout_fileno),\n                        (\"stderr\", protocol.stderr_fileno))\n                    if protocol.fd_infos[fileno][1] is not None\n                }\n>               raise CommandError(\n                    cmd=self.cmd,\n                    code=self.return_code,\n                    stdout=decoded_output.get(\"stdout\", None),\n                    stderr=decoded_output.get(\"stderr\", None)\n                )\nE               datalad.runner.exception.CommandError: CommandError: 'git -c diff.ignoreSubmodules=none checkout -bother dl-test-branch' failed with exitcode 128 [err: 'fatal: 'dl-test-branch' is not a commit and a branch 'other' cannot be created from it']\n\ndatalad/runner/nonasyncrunner.py:295: CommandError\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazy_other_branchc4b9j58d/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n==================================== PASSES ====================================\n____________________________ test_update_git_smoke _____________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_git_smokek09eg9pg (dataset)\nadd(ok): file.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_git_smokek09eg9pg \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_git_smokew8q25rwy to /tmp/datalad_temp_test_update_git_smokek09eg9pg \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_git_smokek09eg9pg) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_git_smokek09eg9pg) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_git_smokek09eg9pg\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_git_smokew8q25rwy to /tmp/datalad_temp_test_update_git_smokek09eg9pg\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_git_smokek09eg9pg)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_git_smokew8q25rwy\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_git_smokek09eg9pg)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_update_volatile_subds __________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_volatile_subds7h7gye87 (dataset)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subm 1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nupdate(ok): . (dataset)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nsubdataset(ok): subm 1 (dataset)\nuninstall(ok): subm 1 (dataset)\nremove(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  remove (ok: 1)\n  save (ok: 1)\n  uninstall (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): subm 1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): load.dat (file)\nsave(ok): subm 1 (dataset)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1 (dataset) [Installed subdataset in order to get /tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1/load.dat]\nget(ok): subm 1/load.dat (file) [from origin...]\naction summary:\n  get (ok: 1)\n  install (ok: 1)\nadd(ok): probe (file)\nsave(ok): subm 1 (dataset)\nadd(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nadd(ok): mike (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 4)\n  save (ok: 2)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): subm 1 (dataset) [Merged origin/master]\nupdate.annex_merge(ok): subm 1 (dataset) [Merged annex branch]\nupdate(ok): subm 1 (dataset)\naction summary:\n  merge (ok: 2)\n  save (notneeded: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 2)\nuninstall(ok): subm 1 (dataset)\nremove(ok): subm 1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  remove (ok: 1)\n  save (ok: 1)\n  uninstall (ok: 1)\nsubdataset(ok): subm 1 (dataset)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nuninstall(ok): . (dataset)\ncreate(ok): . (dataset)\ninstall(ok): other (dataset)\nadd(ok): other (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  install (ok: 1)\n  save (ok: 2)\nadd(ok): brand (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): other (dataset) [Merged origin/master]\nupdate.annex_merge(ok): other (dataset) [Merged annex branch]\nupdate(ok): other (dataset)\nadd(ok): other (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  merge (ok: 2)\n  save (ok: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_volatile_subds7h7gye87 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr to /tmp/datalad_temp_test_update_volatile_subds7h7gye87 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr/subm 1 to /tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1 \n[INFO] Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1') \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset('/tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1') \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_volatile_subds7h7gye87/other \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds2nnkloy4 to /tmp/datalad_temp_test_update_volatile_subds7h7gye87/other \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87/other) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87/other) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_volatile_subds7h7gye87\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr to /tmp/datalad_temp_test_update_volatile_subds7h7gye87\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (1/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (2/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (3/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (4/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr/subm 1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  20% (1/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  40% (2/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  60% (3/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  80% (4/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (5/5)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (5/5), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr/subm 1 to /tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset('/tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1')\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Start annex action: {'command': 'get', 'note': 'from origin...', 'input': ['load.dat'], 'key': 'MD5E-s5--7cfe64ea44dc3bbeb63b29ff3039a481.dat', 'file': 'load.dat'}\nINFO     datalad.annex:log.py:432 100%\nINFO     datalad.annex:log.py:432 Finished annex action: None\nINFO     datalad.annex:log.py:432 Finished annex get\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr/subm 1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset('/tmp/datalad_temp_test_update_volatile_subds7h7gye87/subm 1')\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds7h7gye87\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subdsie0gjcrr\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (1/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (2/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (3/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (4/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (5/5), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_volatile_subds7h7gye87/other\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_volatile_subds2nnkloy4 to /tmp/datalad_temp_test_update_volatile_subds7h7gye87/other\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87/other)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds7h7gye87\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds7h7gye87\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds2nnkloy4\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_volatile_subds7h7gye87/other)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_volatile_subds7h7gye87\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n______________________________ test_reobtain_data ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_reobtain_datacavnaca4 (dataset)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nadd(ok): load.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nget(ok): load.dat (file) [from origin...]\nadd(ok): novel (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  get (notneeded: 1)\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nadd(ok): load.dat (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nget(ok): load.dat (file) [from origin...]\nget(ok): load.dat (file) [from origin...]\nupdate(ok): . (dataset)\naction summary:\n  get (ok: 1)\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_reobtain_datacavnaca4 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_reobtain_datau3h3cvj7 to /tmp/datalad_temp_test_reobtain_datacavnaca4 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Ensuring content availability for 1 previously available files \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4) \n[INFO] Recording file content availability to re-obtain updated files later on \n[INFO] Ensuring content availability for 1 previously available files \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_reobtain_datacavnaca4\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_reobtain_datau3h3cvj7 to /tmp/datalad_temp_test_reobtain_datacavnaca4\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_reobtain_datau3h3cvj7\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Start annex action: {'command': 'get', 'note': 'from origin...', 'input': ['load.dat'], 'key': 'MD5E-s5--7cfe64ea44dc3bbeb63b29ff3039a481.dat', 'file': 'load.dat'}\nINFO     datalad.annex:log.py:432 100%\nINFO     datalad.annex:log.py:432 Finished annex action: None\nINFO     datalad.annex:log.py:432 Finished annex get\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_reobtain_datau3h3cvj7\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:607 Ensuring content availability for 1 previously available files\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_reobtain_datau3h3cvj7\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   9% (1/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  18% (2/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  27% (3/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  36% (4/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  45% (5/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  54% (6/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  63% (7/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  72% (8/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  81% (9/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (10/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (11/11)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (11/11), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.distribution.update:update.py:593 Applying updates to Dataset(/tmp/datalad_temp_test_reobtain_datacavnaca4)\nINFO     datalad.distribution.update:update.py:595 Recording file content availability to re-obtain updated files later on\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:607 Ensuring content availability for 1 previously available files\nINFO     datalad.annex:log.py:432 Start annex operation\nINFO     datalad.annex:log.py:432 Start annex action: {'command': 'get', 'note': 'from origin...', 'input': ['load.dat'], 'key': 'MD5E-s5--2ac43aa43bf473f9a9c09b4b608619d3.dat', 'file': 'load.dat'}\nINFO     datalad.annex:log.py:432 100%\nINFO     datalad.annex:log.py:432 Finished annex action: None\nINFO     datalad.annex:log.py:432 Finished annex get\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_____________________________ test_multiway_merge ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\n.: r1(+) [/tmp/datalad_temp_test_multiway_merget440wav5/ds_r1 (git)]\n.: r2(-) [/tmp/datalad_temp_test_multiway_merget440wav5/ds_r2 (git)]\npublish(ok): . (dataset) [refs/heads/git-annex->r1:refs/heads/git-annex 8ce9870..1b4e202]\npublish(ok): . (dataset) [refs/heads/master->r1:refs/heads/master [new branch]]\naction summary:\n  publish (ok: 2)\npublish(ok): . (dataset) [refs/heads/master->r2:refs/heads/master [new branch]]\npublish(ok): . (dataset) [refs/heads/git-annex->r2:refs/heads/git-annex [new branch]]\naction summary:\n  copy (notneeded: 1)\n  publish (ok: 2)\nupdate(ok): . (dataset)\nupdate(impossible): . (dataset) [Multiple siblings, please specify from which to update.]\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Determine push target \n[INFO] Push refspecs \n[INFO] Transfer data \n[INFO] Update availability information \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Start writing objects \n[INFO] Finished push of Dataset(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig) \n[INFO] Determine push target \n[INFO] Push refspecs \n[INFO] Transfer data \n[INFO] Update availability information \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Start writing objects \n[INFO] Finished push of Dataset(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig) \n[INFO] Start fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig) \n[INFO] Fetching remote r1 \n[INFO] Fetching remote r2 \n[INFO] Finished fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Determine push target\nINFO     datalad.core.distributed.push:log.py:432 Push refspecs\nINFO     datalad.core.distributed.push:log.py:432 Transfer data\nINFO     datalad.core.distributed.push:log.py:432 Update availability information\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 Counting objects:   6% (1/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  13% (2/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  20% (3/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  26% (4/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  33% (5/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  40% (6/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  46% (7/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  53% (8/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  60% (9/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  66% (10/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  73% (11/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  80% (12/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  86% (13/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  93% (14/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (15/15)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (15/15), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  11% (1/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  22% (2/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  33% (3/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  44% (4/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  55% (5/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  66% (6/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  77% (7/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  88% (8/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (9/9)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (9/9), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.gitrepo:log.py:432 Start writing objects\nINFO     datalad.gitrepo:log.py:432 Writing objects:   8% (1/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  16% (2/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  25% (3/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  33% (4/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  41% (5/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  50% (6/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  58% (7/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  66% (8/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  75% (9/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  83% (10/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  91% (11/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (12/12)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (12/12), 1.14 KiB | 1.14 MiB/s, done.\nINFO     datalad.gitrepo:log.py:432 Finished writing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Finished push of Dataset(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Determine push target\nINFO     datalad.core.distributed.push:log.py:432 Push refspecs\nINFO     datalad.core.distributed.push:log.py:432 Transfer data\nINFO     datalad.core.distributed.push:log.py:432 Update availability information\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 Counting objects:   5% (1/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  11% (2/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  17% (3/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  23% (4/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  29% (5/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  35% (6/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  41% (7/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  47% (8/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  52% (9/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  58% (10/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  64% (11/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  70% (12/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  76% (13/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  82% (14/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  88% (15/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects:  94% (16/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (17/17)\nINFO     datalad.gitrepo:log.py:432 Counting objects: 100% (17/17), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 Compressing objects:   8% (1/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  16% (2/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  25% (3/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  33% (4/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  41% (5/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  50% (6/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  58% (7/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  66% (8/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  75% (9/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  83% (10/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects:  91% (11/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (12/12)\nINFO     datalad.gitrepo:log.py:432 Compressing objects: 100% (12/12), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad.gitrepo:log.py:432 Start writing objects\nINFO     datalad.gitrepo:log.py:432 Writing objects:   5% (1/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  11% (2/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  17% (3/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  23% (4/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  29% (5/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  35% (6/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  41% (7/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  47% (8/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  52% (9/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  58% (10/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  64% (11/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  70% (12/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  76% (13/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  82% (14/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  88% (15/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects:  94% (16/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (17/17)\nINFO     datalad.gitrepo:log.py:432 Writing objects: 100% (17/17), 1.35 KiB | 1.35 MiB/s, done.\nINFO     datalad.gitrepo:log.py:432 Finished writing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.push:log.py:432 Finished push of Dataset(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig)\nINFO     datalad.gitrepo:log.py:432 Start fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig)\nINFO     datalad.gitrepo:log.py:432 Fetching remote r1\nINFO     datalad.gitrepo:log.py:432 Fetching remote r2\nINFO     datalad.gitrepo:log.py:432 Finished fetching remotes for AnnexRepo(/tmp/datalad_temp_test_multiway_merget440wav5/ds_orig)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_____________________________ test_merge_conflict ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1 (dataset)\naction summary:\n  install (ok: 3)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge --allow-unrelated-histories origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of origin/master failed]\naction summary:\n  merge (error: 1)\n  update (error: 1)\n  update.annex_merge (ok: 1)\nadd(ok): foo (file)\nsave(ok): s1 (dataset)\nadd(ok): foo (file)\nsave(ok): s0 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 5)\n  save (ok: 3)\nadd(ok): foo (file)\nsave(ok): s0 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (notneeded: 1, ok: 2)\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge --allow-unrelated-histories origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of origin/master failed]\nmerge(error): s0 (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge --allow-unrelated-histories origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): s0 (dataset) [Merged annex branch]\nupdate(error): s0 (dataset) [Update of origin/master failed]\nmerge(ok): s1 (dataset) [Merged origin/master]\nupdate.annex_merge(ok): s1 (dataset) [Merged annex branch]\nupdate(ok): s1 (dataset)\naction summary:\n  merge (error: 2, ok: 1)\n  update (error: 2, ok: 1)\n  update.annex_merge (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src to /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone) to get /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src/s0 to /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src/s1 to /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[WARNING] Not saving because top-level dataset /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone had an update failure in subdataset \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src to /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone) to get /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src/s0 to /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src/s1 to /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s0)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone/s1)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nWARNING  datalad.distribution.update:update.py:434 Not saving because top-level dataset /tmp/datalad_temp_test_merge_conflicts5nu8h6z/clone had an update failure in subdataset\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n____________________ test_merge_conflict_in_subdataset_only ____________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub_conflict (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub_conflict (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): sub_noconflict (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub_noconflict (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict (dataset)\naction summary:\n  install (ok: 3)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(error): sub_conflict (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge --allow-unrelated-histories origin/master' failed with exitcode 1)]\nupdate.annex_merge(ok): sub_conflict (dataset) [Merged annex branch]\nupdate(error): sub_conflict (dataset) [Update of origin/master failed]\nmerge(ok): sub_noconflict (dataset) [Merged origin/master]\nupdate.annex_merge(ok): sub_noconflict (dataset) [Merged annex branch]\nupdate(ok): sub_noconflict (dataset)\nadd(ok): sub_noconflict (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  merge (error: 1, ok: 2)\n  save (ok: 1)\n  update (error: 1, ok: 2)\n  update.annex_merge (ok: 3)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src to /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone) to get /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src/sub_conflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src/sub_noconflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src to /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone) to get /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src/sub_conflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src/sub_noconflict to /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src/sub_conflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/src/sub_noconflict\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_conflict)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  25% (1/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (2/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  75% (3/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (4/4), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone/sub_noconflict)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_conflict_in_subdataset_only4smwa1sv/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n______________________________ test_merge_ff_only ______________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff (dataset)\ninstall(ok): /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff (dataset)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): bar (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\nmerge(error): . (dataset) [CommandError(CommandError: 'git -c diff.ignoreSubmodules=none merge --ff-only --allow-unrelated-histories origin/master' failed with exitcode 128 [err: 'fatal: Not possible to fast-forward, aborting.'])]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(error): . (dataset) [Update of origin/master failed]\naction summary:\n  merge (error: 1)\n  update (error: 1)\n  update.annex_merge (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\naction summary:\n  merge (ok: 1)\n  update (ok: 1)\n  update.annex_merge (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/src to /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff \n[INFO] Attempting to clone from /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/src to /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/src to /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/src to /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_merge_ff_onlyshocz5d5/src\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_ff)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_merge_ff_onlyshocz5d5/clone_nonff)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_______________________ test_update_follow_parentds_lazy _______________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s0 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s2 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  save (notneeded: 5, ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1 (dataset)\naction summary:\n  install (ok: 6)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): foo (file)\nsave(ok): s1 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): s0 (dataset)\nadd(ok): s0 (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 6)\n  save (notneeded: 3, ok: 3)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nmerge(ok): s0 (dataset) [Merged cd3820981161aeb656eec7bc3299635983caa7d1]\nupdate.annex_merge(ok): s0 (dataset) [Merged annex branch]\nupdate(ok): s0 (dataset)\nmerge(ok): s0/s0 (dataset) [Merged 02ef5a3d1b56465f874cf11abe0fb1a9dfdb202b]\nupdate.annex_merge(ok): s0/s0 (dataset) [Merged annex branch]\nupdate(ok): s0/s0 (dataset)\nmerge(ok): s1 (dataset) [Merged 751ab3fdefa6eefd34925b6f68b6c5980c27e5f1]\nupdate.annex_merge(ok): s1 (dataset) [Merged annex branch]\nupdate(ok): s1 (dataset)\naction summary:\n  merge (ok: 4)\n  save (notneeded: 2)\n  update (notneeded: 2, ok: 4)\n  update.annex_merge (ok: 4)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone) to get /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s2 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s2\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s0\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone) to get /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s2 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s0 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s1 to /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s1\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source/s0\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0/s0)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s1)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone/s0\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_follow_parentds_lazyo6cub87a/clone\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n________________ test_update_adjusted_incompatible_with_ff_only ________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone (dataset)\nupdate(impossible): . (dataset) [Updating via 'ff-only' is incompatible with adjusted branches]\nupdate(ok): . (dataset)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/source to /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone) to get /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/source to /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone) to get /tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_adjusted_incompatible_with_ff_only2l062xcr/clone)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n_______________ test_update_how_subds_different[parentds-reset] ________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub (dataset)\naction summary:\n  install (ok: 2)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): bar (file)\nsave(ok): sub (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nadd(ok): baz (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nupdate.reset(ok): sub (dataset) [Reset to 2b8da1c4e5a352e0d379ff24ae4ac8dae79bb6ca]\nupdate(ok): sub (dataset)\naction summary:\n  merge (ok: 1)\n  save (notneeded: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 1)\n  update.reset (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source to /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone) to get /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source/sub to /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source to /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone) to get /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source/sub to /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone/sub)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   5% (1/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  11% (2/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  16% (3/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  22% (4/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  27% (5/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (6/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  38% (7/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  44% (8/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (9/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  55% (10/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  61% (11/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (12/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  72% (13/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  77% (14/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  83% (15/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  88% (16/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  94% (17/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:   7% (1/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (2/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  21% (3/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (4/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  35% (5/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (6/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (7/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (8/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  64% (9/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (10/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  78% (11/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (12/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  92% (13/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_different6stxqfaq/clone\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n______________ test_update_how_subds_different[sibling-checkout] _______________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): sub (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub (dataset)\naction summary:\n  install (ok: 2)\nadd(ok): foo (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nadd(ok): bar (file)\nsave(ok): sub (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 3)\n  save (ok: 2)\nadd(ok): baz (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 1)\n  save (ok: 1)\nmerge(ok): . (dataset) [Merged origin/master]\nupdate.annex_merge(ok): . (dataset) [Merged annex branch]\nupdate(ok): . (dataset)\nupdate.checkout(ok): sub (dataset) [Checkout origin/master]\nupdate(ok): sub (dataset)\nadd(ok): sub (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 2)\n  merge (ok: 1)\n  save (ok: 1)\n  update (ok: 2)\n  update.annex_merge (ok: 1)\n  update.checkout (ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source to /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone) to get /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source/sub to /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source to /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone) to get /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source/sub to /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/source/sub\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone/sub)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:   5% (1/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  11% (2/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  16% (3/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  22% (4/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  27% (5/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (6/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  38% (7/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  44% (8/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (9/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  55% (10/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  61% (11/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (12/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  72% (13/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  77% (14/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  83% (15/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  88% (16/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  94% (17/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (18/18), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:   7% (1/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (2/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  21% (3/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (4/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  35% (5/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (6/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (7/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (8/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  64% (9/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (10/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  78% (11/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (12/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  92% (13/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (14/14), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_how_subds_differentzt6ritev/clone\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n___________________________ test_update_reset_dirty ____________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s2 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2 (dataset)\naction summary:\n  install (ok: 3)\nadd(ok): bar (file)\nsave(ok): s2 (dataset)\nadd(ok): foo (file)\nsave(ok): s1 (dataset)\nadd(ok): s1 (dataset)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 5)\n  save (ok: 3)\nupdate.reset(error): . (dataset) [Refusing to reset dirty working tree]\nupdate(error): . (dataset) [Update of origin/master failed]\nupdate.reset(error): s1 (dataset) [Refusing to reset dirty working tree]\nupdate(error): s1 (dataset) [Update of origin/master failed]\nupdate.reset(ok): s2 (dataset) [Reset to origin/master]\nupdate(ok): s2 (dataset)\naction summary:\n  update (error: 2, ok: 1)\n  update.reset (error: 2, ok: 1)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source to /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone) to get /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source/s1 to /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source/s2 to /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2) \n[INFO] Start enumerating objects \n[INFO] Start counting objects \n[INFO] Start compressing objects \n[WARNING] Not saving because top-level dataset /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone had an update failure in subdataset \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source to /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone) to get /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source/s1 to /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source/s2 to /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/source\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  33% (1/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  66% (2/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (3/3), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  50% (1/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (2/2), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s1)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone/s2)\nINFO     datalad.gitrepo:log.py:432 Start enumerating objects\nINFO     datalad.gitrepo:log.py:432 Finished enumerating objects\nINFO     datalad.gitrepo:log.py:432 Start counting objects\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  10% (1/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  20% (2/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  30% (3/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  40% (4/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  50% (5/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  60% (6/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  70% (7/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  80% (8/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects:  90% (9/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10)\nINFO     datalad.gitrepo:log.py:432 remote: Counting objects: 100% (10/10), done.\nINFO     datalad.gitrepo:log.py:432 Finished counting objects\nINFO     datalad.gitrepo:log.py:432 Start compressing objects\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  14% (1/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  28% (2/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  42% (3/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  57% (4/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  71% (5/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects:  85% (6/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7)\nINFO     datalad.gitrepo:log.py:432 remote: Compressing objects: 100% (7/7), done.\nINFO     datalad.gitrepo:log.py:432 Finished compressing objects\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nWARNING  datalad.distribution.update:update.py:434 Not saving because top-level dataset /tmp/datalad_temp_test_update_reset_dirty0t0h90n5/clone had an update failure in subdataset\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n__________________________ test_update_fetch_failure ___________________________\n----------------------------- Captured stdout call -----------------------------\ncreate(ok): . (dataset)\nadd(ok): s1 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s1 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\nadd(ok): s2 (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\ncreate(ok): s2 (dataset)\naction summary:\n  add (ok: 2)\n  create (ok: 1)\n  save (ok: 1)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1 (dataset)\ninstall(ok): /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2 (dataset)\naction summary:\n  install (ok: 3)\nupdate(ok): . (dataset)\nupdate(error): s1 (dataset) [Fetch failed: CommandError(CommandError: 'git -c diff.ignoreSubmodules=none fetch --verbose --progress --no-recurse-submodules --prune origin' failed with exitcode 128 under /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1 [err: 'fatal: '/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a/s1' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.'])] [CommandError: 'git -c diff.ignoreSubmodules=none fetch --verbose --progress --no-recurse-submodules --prune origin' failed with exitcode 128 under /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1 [err: 'fatal: '/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a/s1' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.']]\nupdate(ok): s2 (dataset)\naction summary:\n  update (error: 1, ok: 2)\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a to /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Ensuring presence of Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b) to get /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a/s1 to /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Attempting a clone into /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2 \n[INFO] Attempting to clone from /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a/s2 to /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2 \n[INFO] Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2) \n[INFO] scanning for unlocked files (this may take some time) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1) \n[INFO] Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.local.save:log.py:432 Total: starting\nINFO     datalad.core.local.save:log.py:432 \nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a to /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:get.py:626 Ensuring presence of Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b) to get /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b\nINFO     datalad.distribution.get:log.py:432 Installing: starting\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a/s1 to /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1)\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.core.distributed.clone:log.py:432 Attempting a clone into /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2\nINFO     datalad.core.distributed.clone:log.py:432 Attempting to clone from /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds_a/s2 to /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2\nINFO     datalad.core.distributed.clone:log.py:432 Completed clone attempts for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2)\nINFO     datalad.distribution.get:log.py:432 \nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.annex:annexrepo.py:3912 scanning for unlocked files (this may take some time)\nINFO     datalad.distribution.get:log.py:432 Installing: processed result for /tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.get:log.py:432 Installing: done\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s1)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad.distribution.update:update.py:307 Fetching updates for Dataset(/tmp/datalad_temp_test_update_fetch_failure2wxsmw4w/ds-b/s2)\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad:log.py:432 Refresh progress bars\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::test_update_git_smoke\nPASSED r2e_tests/test_1.py::test_update_volatile_subds\nPASSED r2e_tests/test_1.py::test_reobtain_data\nPASSED r2e_tests/test_1.py::test_multiway_merge\nPASSED r2e_tests/test_1.py::test_merge_conflict\nPASSED r2e_tests/test_1.py::test_merge_conflict_in_subdataset_only\nPASSED r2e_tests/test_1.py::test_merge_ff_only\nPASSED r2e_tests/test_1.py::test_update_follow_parentds_lazy\nPASSED r2e_tests/test_1.py::test_update_adjusted_incompatible_with_ff_only\nPASSED r2e_tests/test_1.py::test_update_how_subds_different[parentds-reset]\nPASSED r2e_tests/test_1.py::test_update_how_subds_different[sibling-checkout]\nPASSED r2e_tests/test_1.py::test_update_reset_dirty\nPASSED r2e_tests/test_1.py::test_process_how_args\nPASSED r2e_tests/test_1.py::test_update_fetch_failure\nSKIPPED [1] .venv/lib/python3.9/site-packages/_pytest/unittest.py:385: 'git annex init' with adjusted branch currently fails due to hard-coded 'origin'\nFAILED r2e_tests/test_1.py::test_update_simple - datalad.support.exceptions.R...\nFAILED r2e_tests/test_1.py::test_update_fetch_all - datalad.runner.exception....\nFAILED r2e_tests/test_1.py::test_newthings_coming_down - AssertionError: asse...\nFAILED r2e_tests/test_1.py::test_unrelated_history_merge - AssertionError: Te...\nFAILED r2e_tests/test_1.py::test_merge_no_merge_target - datalad.runner.excep...\nFAILED r2e_tests/test_1.py::test_merge_follow_parentds_subdataset_other_branch\nFAILED r2e_tests/test_1.py::test_merge_follow_parentds_subdataset_adjusted_warning\nFAILED r2e_tests/test_1.py::test_merge_follow_parentds_subdataset_detached[False]\nFAILED r2e_tests/test_1.py::test_update_unborn_master - datalad.runner.except...\nFAILED r2e_tests/test_1.py::test_update_follow_parentds_lazy_other_branch - d...\n============= 10 failed, 14 passed, 1 skipped in 61.28s (0:01:01) ==============\n",
    "old_commit_res_stderr": ""
}
changelog.d/pr-7312.md datalad/distribution/tests/test_update.py datalad/support/gitrepo.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** GitRepo.merge Allows Merging Unrelated Histories Without Explicit Permission

**Description:**
When attempting to merge two independent repositories, the `GitRepo.merge` function proceeds without requiring explicit permission to allow unrelated histories. This behavior can lead to unintended merges, causing repository inconsistencies and potential data conflicts.

**Example Code:**
```python
from datalad import Dataset
from datalad.support.annexrepo import AnnexRepo

# Create two separate repositories
ds = Dataset('/tmp/repo1').create()
repo = AnnexRepo('/tmp/repo2')
file = '/tmp/repo2/file.dat'
with open(file, 'w') as f:
    f.write("data")
repo.add(file)
repo.commit()

# Attempt to add and merge the unrelated repository
ds.siblings(action='add', name='repo', url=repo.path)
result = ds.update(how='merge', on_failure='ignore')

# Expecting the merge to fail due to unrelated histories
```

**Expected Behavior:**
The merge operation should fail and return an error message indicating that merging unrelated histories is not allowed unless explicitly permitted. This prevents accidental merges that can complicate the repository's history.

**Actual Behavior:**
The merge operation completes successfully without any errors, even though the histories of the two repositories are unrelated. This can result in an inconsistent state of the repository and make it difficult to trace changes accurately.

[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### datalad/support/gitrepo.py
# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 et:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the datalad package for the
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Internal low-level interface to Git repositories

"""

import logging
import os
import os.path as op
import posixpath
import re
import warnings
from collections.abc import Mapping
from functools import wraps
from itertools import chain
from os import linesep
from os.path import (
    commonprefix,
    curdir,
    dirname,
    exists,
    isabs,
)
from os.path import join as opj
from os.path import (
    pardir,
    relpath,
    sep,
)

import datalad.utils as ut
from datalad import ssh_manager
from datalad.cmd import (
    BatchedCommand,
    GitWitlessRunner,
    NoCapture,
    StdOutErrCapture,
    WitlessProtocol,
)
from datalad.config import (
    parse_gitconfig_dump,
    write_config_section,
)
from datalad.consts import (
    ILLEGAL_CHARS_WIN,
    RESERVED_NAMES_WIN,
)
from datalad.core.local.repo import repo_from_path
from datalad.dataset.gitrepo import GitRepo as CoreGitRepo
from datalad.dataset.gitrepo import (
    _get_dot_git,
    path_based_str_repr,
)
from datalad.log import log_progress
from datalad.support.due import (
    Doi,
    due,
)
from datalad.utils import (
    Path,
    PurePosixPath,
    ensure_dir,
    ensure_list,
    ensure_unicode,
    generate_file_chunks,
    getpwd,
    is_interactive,
    on_windows,
    optional_args,
    path_is_subpath,
    posix_relpath,
)

from .exceptions import (
    CapturedException,
    CommandError,
    FileNotInRepositoryError,
    InvalidGitReferenceError,
    InvalidGitRepositoryError,
    NoSuchPathError,
)
# imports from same module:
from .external_versions import external_versions
from .network import (
    RI,
    PathRI,
    is_ssh,
)
from .path import get_parent_paths, get_filtered_paths_

# shortcuts
_curdirsep = curdir + sep
_pardirsep = pardir + sep


lgr = logging.getLogger('datalad.gitrepo')


# outside the repo base classes only used in ConfigManager
def to_options(split_single_char_options=True, **kwargs):
    """Transform keyword arguments into a list of cmdline options

    Imported from GitPython.

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License

    Parameters
    ----------
    split_single_char_options: bool

    kwargs:

    Returns
    -------
    list
    """
    def dashify(string):
        return string.replace('_', '-')

    def transform_kwarg(name, value, split_single_char_options):
        if len(name) == 1:
            if value is True:
                return ["-%s" % name]
            elif value not in (False, None):
                if split_single_char_options:
                    return ["-%s" % name, "%s" % value]
                else:
                    return ["-%s%s" % (name, value)]
        else:
            if value is True:
                return ["--%s" % dashify(name)]
            elif value is not False and value is not None:
                return ["--%s=%s" % (dashify(name), value)]
        return []

    args = []
    kwargs = dict(sorted(kwargs.items(), key=lambda x: x[0]))
    for k, v in kwargs.items():
        if isinstance(v, (list, tuple)):
            for value in v:
                args += transform_kwarg(k, value, split_single_char_options)
        else:
            args += transform_kwarg(k, v, split_single_char_options)
    return args


def _normalize_path(base_dir, path):
    """Helper to check paths passed to methods of this class.

    Checks whether `path` is beneath `base_dir` and normalizes it.
    Additionally paths are converted into relative paths with respect to
    `base_dir`, considering PWD in case of relative paths. This
    is intended to be used in repository classes, which means that
    `base_dir` usually will be the repository's base directory.

    Parameters
    ----------
    base_dir: str
        directory to serve as base to normalized, relative paths
    path: str
        path to be normalized

    Returns
    -------
    str:
        path, that is a relative path with respect to `base_dir`
    """
    if not path:
        return path
    pathobj = Path(path)

    # do absolute() in addition to always get an absolute path
    # even with non-existing base_dirs on windows
    base_dir = str(Path(base_dir).resolve().absolute())  # realpath OK

    # path = normpath(path)
    # Note: disabled normpath, because it may break paths containing symlinks;
    # But we don't want to realpath relative paths, in case cwd isn't the
    # correct base.

    if pathobj.is_absolute():
        # path might already be a symlink pointing to annex etc,
        # so realpath only its directory, to get "inline" with
        # realpath(base_dir) above
        path = str(pathobj.parent.resolve() / pathobj.name)  # realpath OK
    # Executive decision was made to not do this kind of magic!
    #
    # elif commonprefix([realpath(getpwd()), base_dir]) == base_dir:
    #     # If we are inside repository, rebuilt relative paths.
    #     path = opj(realpath(getpwd()), path)
    #
    # BUT with relative curdir/pardir start it would assume relative to curdir
    #
    elif path.startswith(_curdirsep) or path.startswith(_pardirsep):
        path = str(Path(getpwd()).resolve() / pathobj)  # realpath OK
    else:
        # We were called from outside the repo. Therefore relative paths
        # are interpreted as being relative to self.path already.
        return path

    if commonprefix([path, base_dir]) != base_dir:
        raise FileNotInRepositoryError(msg="Path outside repository: %s"
                                           % base_dir, filename=path)

    return relpath(path, start=base_dir)


@optional_args
def normalize_path(func):
    """Decorator to provide unified path conversion for a single file

    Unlike normalize_paths, intended to be used for functions dealing with a
    single filename at a time

    Note
    ----
    This is intended to be used within the repository classes and therefore
    returns a class method!

    The decorated function is expected to take a path at
    first positional argument (after 'self'). Additionally the class `func`
    is a member of, is expected to have an attribute 'path'.
    """

    @wraps(func)
    def  _wrap_normalize_path(self, file_, *args, **kwargs):
        file_new = _normalize_path(self.path, file_)
        return func(self, file_new, *args, **kwargs)

    return  _wrap_normalize_path


@optional_args
def normalize_paths(func, match_return_type=True, map_filenames_back=False,
                    serialize=False):
    """Decorator to provide unified path conversions.

    Note
    ----
    This is intended to be used within the repository classes and therefore
    returns a class method!

    The decorated function is expected to take a path or a list of paths at
    first positional argument (after 'self'). Additionally the class `func`
    is a member of, is expected to have an attribute 'path'.

    Accepts either a list of paths or a single path in a str. Passes a list
    to decorated function either way, but would return based on the value of
    match_return_type and possibly input argument.

    If a call to the wrapped function includes normalize_path and it is False
    no normalization happens for that function call (used for calls to wrapped
    functions within wrapped functions, while possible CWD is within a
    repository)

    Parameters
    ----------
    match_return_type : bool, optional
      If True, and a single string was passed in, it would return the first
      element of the output (after verifying that it is a list of length 1).
      It makes easier to work with single files input.
    map_filenames_back : bool, optional
      If True and returned value is a dictionary, it assumes to carry entries
      one per file, and then filenames are mapped back to as provided from the
      normalized (from the root of the repo) paths
    serialize : bool, optional
      Loop through files giving only a single one to the function one at a time.
      This allows to simplify implementation and interface to annex commands
      which do not take multiple args in the same call (e.g. checkpresentkey)
    """

    @wraps(func)
    def  _wrap_normalize_paths(self, files, *args, **kwargs):

        normalize = _normalize_path if kwargs.pop('normalize_paths', True) \
            else lambda rpath, filepath: filepath

        if files:
            if isinstance(files, str) or not files:
                files_new = [normalize(self.path, files)]
                single_file = True
            elif isinstance(files, list):
                files_new = [normalize(self.path, path) for path in files]
                single_file = False
            else:
                raise ValueError("_files_decorator: Don't know how to handle "
                                 "instance of %s." % type(files))
        else:
            single_file = None
            files_new = []

        if map_filenames_back:
            def remap_filenames(out):
                """Helper to map files back to non-normalized paths"""
                if isinstance(out, dict):
                    assert(len(out) == len(files_new))
                    files_ = [files] if single_file else files
                    mapped = out.__class__()
                    for fin, fout in zip(files_, files_new):
                        mapped[fin] = out[fout]
                    return mapped
                else:
                    return out
        else:
            remap_filenames = lambda x: x

        if serialize:  # and not single_file:
            result = [
                func(self, f, *args, **kwargs)
                for f in files_new
            ]
        else:
            result = func(self, files_new, *args, **kwargs)

        if single_file is None:
            # no files were provided, nothing we can do really
            return result
        elif (result is None) or not match_return_type or not single_file:
            # If function doesn't return anything or no denormalization
            # was requested or it was not a single file
            return remap_filenames(result)
        elif single_file:
            if len(result) != 1:
                # Magic doesn't apply
                return remap_filenames(result)
            elif isinstance(result, (list, tuple)):
                return result[0]
            elif isinstance(result, dict) and tuple(result)[0] == files_new[0]:
                # assume that returned dictionary has files as keys.
                return tuple(result.values())[0]
            else:
                # no magic can apply
                return remap_filenames(result)
        else:
            return RuntimeError("should have not got here... check logic")

    return  _wrap_normalize_paths


if "2.24.0" <= external_versions["cmd:git"] < "2.25.0":
    # An unintentional change in Git 2.24.0 led to `ls-files -o` traversing
    # into untracked submodules when multiple pathspecs are given, returning
    # repositories that are deeper than the first level. This helper filters
    # these deeper levels out so that save_() doesn't fail trying to add them.
    #
    # This regression fixed with upstream's 072a231016 (2019-12-10).
    def _prune_deeper_repos(repos):
        firstlevel_repos = []
        prev = None
        for repo in sorted(repos):
            if not (prev and str(repo).startswith(prev)):
                prev = str(repo)
                firstlevel_repos.append(repo)
        return firstlevel_repos
else:
    def _prune_deeper_repos(repos):
        return repos


class GitProgress(WitlessProtocol):
    """Reduced variant of GitPython's RemoteProgress class

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License
    """
    # inform super-class to capture stderr
    proc_err = True

    _num_op_codes = 10
    BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT, ENUMERATING = \
        [1 << x for x in range(_num_op_codes)]
    STAGE_MASK = BEGIN | END
    OP_MASK = ~STAGE_MASK

    DONE_TOKEN = 'done.'
    TOKEN_SEPARATOR = ', '

    _known_ops = {
        COUNTING: ("Counting", "Objects"),
        ENUMERATING: ("Enumerating", "Objects"),
        COMPRESSING: ("Compressing", "Objects"),
        WRITING: ("Writing", "Objects"),
        RECEIVING: ("Receiving", "Objects"),
        RESOLVING: ("Resolving", "Deltas"),
        FINDING_SOURCES: ("Finding", "Sources"),
        CHECKING_OUT: ("Check out", "Things"),
    }

    __slots__ = ('_seen_ops', '_pbars', '_encoding')

    re_op_absolute = re.compile(r"(remote: )?([\w\s]+):\s+()(\d+)()(.*)")
    re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")

    def __init__(self, *args):
        super().__init__(*args)
        self._unprocessed = None

    def connection_made(self, transport):
        super().connection_made(transport)
        self._seen_ops = []
        self._pbars = set()

    def process_exited(self):
        # take down any progress bars that were not closed orderly
        for pbar_id in self._pbars:
            log_progress(
                lgr.info,
                pbar_id,
                'Finished',
            )
        super().process_exited()

    def pipe_data_received(self, fd, byts):
        # progress reports only come from stderr
        if fd != 2:
            # let the base class decide what to do with it
            super().pipe_data_received(fd, byts)
            return
        for line in byts.splitlines(keepends=True):
            # put any unprocessed content back in front
            line = self._unprocessed + line if self._unprocessed else line
            self._unprocessed = None
            if not self._parse_progress_line(line):
                # anything that doesn't look like a progress report
                # is retained and returned
                # in case of partial progress lines, this can lead to
                # leakage of progress info into the output, but
                # it is better to enable better (maybe more expensive)
                # subsequent filtering than hiding lines with
                # unknown, potentially important info
                lgr.debug('Non-progress stderr: %s', line)
                if line.endswith((b'\r', b'\n')):
                    # complete non-progress line, pass on
                    super().pipe_data_received(fd, line)
                else:
                    # an incomplete line, maybe the next batch completes
                    # it to become a recognizable progress report
                    self._unprocessed = line

    def _parse_progress_line(self, line):
        """Process a single line

        Parameters
        ----------
        line : bytes

        Returns
        -------
        bool
          Flag whether the line was recognized as a Git progress report.
        """
        # handle
        # Counting objects: 4, done.
        # Compressing objects:  50% (1/2)
        # Compressing objects: 100% (2/2)
        # Compressing objects: 100% (2/2), done.
        line = line.decode(self.encoding) if isinstance(line, bytes) else line
        if line.startswith(('warning:', 'error:', 'fatal:')):
            return False

        # find escape characters and cut them away - regex will not work with
        # them as they are non-ascii. As git might expect a tty, it will send them
        last_valid_index = None
        for i, c in enumerate(reversed(line)):
            if ord(c) < 32:
                # its a slice index
                last_valid_index = -i - 1
            # END character was non-ascii
        # END for each character in line
        if last_valid_index is not None:
            line = line[:last_valid_index]
        # END cut away invalid part
        line = line.rstrip()

        cur_count, max_count = None, None
        match = self.re_op_relative.match(line)
        if match is None:
            match = self.re_op_absolute.match(line)

        if not match:
            return False
        # END could not get match

        op_code = 0
        _remote, op_name, _percent, cur_count, max_count, message = match.groups()

        # get operation id
        if op_name == "Counting objects":
            op_code |= self.COUNTING
        elif op_name == "Compressing objects":
            op_code |= self.COMPRESSING
        elif op_name == "Writing objects":
            op_code |= self.WRITING
        elif op_name == 'Receiving objects':
            op_code |= self.RECEIVING
        elif op_name == 'Resolving deltas':
            op_code |= self.RESOLVING
        elif op_name == 'Finding sources':
            op_code |= self.FINDING_SOURCES
        elif op_name == 'Checking out files':
            op_code |= self.CHECKING_OUT
        elif op_name == 'Enumerating objects':
            op_code |= self.ENUMERATING
        else:
            # Note: On windows it can happen that partial lines are sent
            # Hence we get something like "CompreReceiving objects", which is
            # a blend of "Compressing objects" and "Receiving objects".
            # This can't really be prevented.
            lgr.debug(
                'Output line matched a progress report of an unknown type: %s',
                line)
            # TODO investigate if there is any chance that we might swallow
            # important info -- until them do not flag this line
            # as progress
            return False
        # END handle op code

        pbar_id = 'gitprogress-{}-{}'.format(id(self), op_code)

        op_props = self._known_ops[op_code]

        # figure out stage
        if op_code not in self._seen_ops:
            self._seen_ops.append(op_code)
            op_code |= self.BEGIN
            log_progress(
                lgr.info,
                pbar_id,
                'Start {} {}'.format(
                    op_props[0].lower(),
                    op_props[1].lower(),
                ),
                label=op_props[0],
                unit=' {}'.format(op_props[1]),
                total=float(max_count) if max_count else None,
            )
            self._pbars.add(pbar_id)
        # END begin opcode

        if message is None:
            message = ''
        # END message handling

        done_progress = False
        message = message.strip()
        if message.endswith(self.DONE_TOKEN):
            op_code |= self.END
            message = message[:-len(self.DONE_TOKEN)]
            done_progress = True
        # END end message handling
        message = message.strip(self.TOKEN_SEPARATOR)

        if cur_count and max_count:
            log_progress(
                lgr.info,
                pbar_id,
                line,
                update=float(cur_count),
                noninteractive_level=logging.DEBUG,
            )

        if done_progress:
            log_progress(
                lgr.info,
                pbar_id,
                'Finished {} {}'.format(
                    op_props[0].lower(),
                    op_props[1].lower(),
                ),
                noninteractive_level=logging.DEBUG,
            )
            self._pbars.discard(pbar_id)
        return True


class StdOutCaptureWithGitProgress(GitProgress):
    proc_out = True


class FetchInfo(dict):
    """
    dict that carries results of a fetch operation of a single head

    Reduced variant of GitPython's RemoteProgress class

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License
    """

    NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \
        FAST_FORWARD, ERROR = [1 << x for x in range(8)]

    _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) [-> ]+ ([^\s]+)(    \(.*\)?$)?')

    _flag_map = {
        '!': ERROR,
        '+': FORCED_UPDATE,
        '*': 0,
        '=': HEAD_UPTODATE,
        ' ': FAST_FORWARD,
        '-': TAG_UPDATE,
    }
    _operation_map = {
        NEW_TAG: 'new-tag',
        NEW_HEAD: 'new-branch',
        HEAD_UPTODATE: 'uptodate',
        TAG_UPDATE: 'tag-update',
        REJECTED: 'rejected',
        FORCED_UPDATE: 'forced-update',
        FAST_FORWARD: 'fast-forward',
        ERROR: 'error',
    }

    @classmethod
    def _from_line(cls, line):
        """Parse information from the given line as returned by git-fetch -v
        and return a new FetchInfo object representing this information.
        """
        match = cls._re_fetch_result.match(line)
        if match is None:
            raise ValueError("Failed to parse line: %r" % line)

        # parse lines
        control_character, operation, local_remote_ref, remote_local_ref, note = \
            match.groups()

        # parse flags from control_character
        flags = 0
        try:
            flags |= cls._flag_map[control_character]
        except KeyError:
            raise ValueError(
                "Control character %r unknown as parsed from line %r"
                % (control_character, line))
        # END control char exception handling

        # parse operation string for more info - makes no sense for symbolic refs,
        # but we parse it anyway
        old_commit = None
        if 'rejected' in operation:
            flags |= cls.REJECTED
        if 'new tag' in operation:
            flags |= cls.NEW_TAG
        if 'tag update' in operation:
            flags |= cls.TAG_UPDATE
        if 'new branch' in operation:
            flags |= cls.NEW_HEAD
        if '...' in operation or '..' in operation:
            split_token = '...'
            if control_character == ' ':
                split_token = split_token[:-1]
            old_commit = operation.split(split_token)[0]
        # END handle refspec

        return cls(
            ref=remote_local_ref.strip(),
            local_ref=local_remote_ref.strip(),
            # convert flag int into a list of operation labels
            operations=[
                cls._operation_map[o]
                for o in cls._operation_map.keys()
                if flags & o
            ],
            note=note,
            old_commit=old_commit,
        )


class PushInfo(dict):
    """dict that carries results of a push operation of a single head

    Reduced variant of GitPython's RemoteProgress class

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License
    """
    NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \
        FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)]

    _flag_map = {'X': NO_MATCH,
                 '-': DELETED,
                 '*': 0,
                 '+': FORCED_UPDATE,
                 ' ': FAST_FORWARD,
                 '=': UP_TO_DATE,
                 '!': ERROR}

    _operation_map = {
        NEW_TAG: 'new-tag',
        NEW_HEAD: 'new-branch',
        NO_MATCH: 'no-match',
        REJECTED: 'rejected',
        REMOTE_REJECTED: 'remote-rejected',
        REMOTE_FAILURE: 'remote-failure',
        DELETED: 'deleted',
        FORCED_UPDATE: 'forced-update',
        FAST_FORWARD: 'fast-forward',
        UP_TO_DATE: 'uptodate',
        ERROR: 'error',
    }

    @classmethod
    def _from_line(cls, line):
        """Create a new PushInfo instance as parsed from line which is expected to be like
            refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes"""
        control_character, from_to, summary = line.split('\t', 3)
        flags = 0

        # control character handling
        try:
            flags |= cls._flag_map[control_character]
        except KeyError:
            raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line))
        # END handle control character

        # from_to handling
        from_ref_string, to_ref_string = from_to.split(':')

        # commit handling, could be message or commit info
        old_commit = None
        if summary.startswith('['):
            if "[rejected]" in summary:
                flags |= cls.REJECTED
            elif "[remote rejected]" in summary:
                flags |= cls.REMOTE_REJECTED
            elif "[remote failure]" in summary:
                flags |= cls.REMOTE_FAILURE
            elif "[no match]" in summary:
                flags |= cls.ERROR
            elif "[new tag]" in summary:
                flags |= cls.NEW_TAG
            elif "[new branch]" in summary:
                flags |= cls.NEW_HEAD
            # up-to-date encoded in control character
        else:
            # fast-forward or forced update - was encoded in control character,
            # but we parse the old and new commit
            split_token = "..."
            if control_character == " ":
                split_token = ".."
            old_sha, _new_sha = summary.split(' ')[0].split(split_token)
            # have to use constructor here as the sha usually is abbreviated
            old_commit = old_sha
        # END message handling

        return cls(
            from_ref=from_ref_string.strip(),
            to_ref=to_ref_string.strip(),
            # convert flag int into a list of operation labels
            operations=[
                cls._operation_map[o]
                for o in cls._operation_map.keys()
                if flags & o
            ],
            note=summary.strip(),
            old_commit=old_commit,
        )


@path_based_str_repr
class GitRepo(CoreGitRepo):
    """Representation of a git repository

    """
    # We must check git config to have name and email set, but
    # should do it once
    _config_checked = False

    GIT_MIN_VERSION = "2.19.1"
    git_version = None

    @classmethod
    def _check_git_version(cls):
        external_versions.check("cmd:git", min_version=cls.GIT_MIN_VERSION)
        cls.git_version = external_versions['cmd:git']

    # This is the least common denominator to claim that a user
    # used DataLad.
    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262
    @due.dcite(Doi("10.21105/joss.03262"),
               # override path since there is no need ATM for such details
               path="datalad",
               description="DataLad - Data management and distribution platform")
    def __init__(self, path, runner=None, create=True,
                 git_opts=None, repo=None, fake_dates=False,
                 create_sanity_checks=True,
                 **kwargs):
        """Creates representation of git repository at `path`.

        Can also be used to create a git repository at `path`.

        Parameters
        ----------
        path: str
          path to the git repository; In case it's not an absolute path,
          it's relative to PWD
        create: bool, optional
          if true, creates a git repository at `path` if there is none. Also
          creates `path`, if it doesn't exist.
          If set to false, an exception is raised in case `path` doesn't exist
          or doesn't contain a git repository.
        repo: git.Repo, optional
          This argument is ignored.
        create_sanity_checks: bool, optional
          Whether to perform sanity checks during initialization (when
          `create=True` and target path is not a valid repo already), such as
          that new repository is not created in the directory where git already
          tracks some files.
        kwargs:
          keyword arguments serving as additional options to the git-init
          command. Therefore, it makes sense only if called with `create`.

          Generally, this way of passing options to the git executable is
          (or will be) used a lot in this class. It's a transformation of
          python-style keyword arguments (or a `dict`) to command line arguments,
          provided by GitPython.

          A single character keyword will be prefixed by '-', multiple characters
          by '--'. An underscore in the keyword becomes a dash. The value of the
          keyword argument is used as the value for the corresponding command
          line argument. Assigning a boolean creates a flag.

          Examples:
          no_commit=True => --no-commit
          C='/my/path'   => -C /my/path

        """
        # this will set up .pathobj and .dot_git
        super().__init__(path)

        if self.git_version is None:
            self._check_git_version()

        # BEGIN Repo validity test
        # We want to fail early for tests, that would be performed a lot. In
        # particular this is about GitRepo.is_valid_repo. We would use the
        # latter to decide whether or not to call GitRepo() only for __init__ to
        # then test the same things again. If we fail early we can save the
        # additional test from outer scope.
        self.path = path

        # Note, that the following three path objects are used often and
        # therefore are stored for performance. Path object creation comes with
        # a cost. Most notably, this is used for validity checking of the
        # repository.
        _valid_repo = self.is_valid_git()

        do_create = False
        if create and not _valid_repo:
            if repo is not None:
                # `repo` passed with `create`, which doesn't make sense
                raise TypeError("argument 'repo' must not be used with 'create'")
            do_create = True
        else:
            # Note: We used to call gitpy.Repo(path) here, which potentially
            # raised NoSuchPathError or InvalidGitRepositoryError. This is
            # used by callers of GitRepo.__init__() to detect whether we have a
            # valid repo at `path`. Now, with switching to lazy loading property
            # `repo`, we detect those cases without instantiating a
            # gitpy.Repo().

            if not exists(path):
                raise NoSuchPathError(path)
            if not _valid_repo:
                raise InvalidGitRepositoryError(path)
        # END Repo validity test

        # So that we "share" control paths with git/git-annex
        if ssh_manager:
            ssh_manager.ensure_initialized()

        # note: we may also want to distinguish between a path to the worktree
        # and the actual repository

        if git_opts is None:
            git_opts = {}
        if kwargs:
            git_opts.update(kwargs)

        self._cfg = None

        if do_create:  # we figured it out earlier
            from_cmdline = git_opts.pop('_from_cmdline_', [])
            self.init(
                sanity_checks=create_sanity_checks,
                init_options=from_cmdline + to_options(**git_opts),
            )

        # with DryRunProtocol path might still not exist
        if exists(self.path):
            self.inode = os.stat(self.path).st_ino
        else:
            self.inode = None

        if fake_dates:
            self.configure_fake_dates()

    @property
    def bare(self):
        """Returns a bool indicating whether the repository is bare

        Importantly, this is not reporting the configuration value
        of 'core.bare', in order to be usable at a stage where a
        Repo instance is not yet equipped with a ConfigManager.
        Instead, it is testing whether the repository path and its
        "dot_git" are identical. The value of 'core.bare' can be query
        from the ConfigManager in a fully initialized instance.
        """
        return self.pathobj == self.dot_git

    @classmethod
    def clone(cls, url, path, *args, clone_options=None, **kwargs):
        """Clone url into path

        Provides workarounds for known issues (e.g.
        https://github.com/datalad/datalad/issues/785)

        Parameters
        ----------
        url : str
        path : str
        clone_options : dict or list
          Arbitrary options that will be passed on to the underlying call to
          `git-clone`. This may be a list of plain options or key-value pairs
          that will be converted to a list of plain options with `to_options`.
        expect_fail : bool
          Whether expect that command might fail, so error should be logged then
          at DEBUG level instead of ERROR
        kwargs:
          Passed to the Repo class constructor.
        """

        if 'repo' in kwargs:
            raise TypeError("argument 'repo' conflicts with cloning")
            # TODO: what about 'create'?

        expect_fail = kwargs.pop('expect_fail', False)
        # fail early on non-empty target:
        from os import listdir
        if exists(path) and listdir(path):
            raise ValueError(
                "destination path '%s' already exists and is not an "
                "empty directory." % path)
        else:
            # protect against cloning into existing and obviously dangling
            # instance for that location
            try:
                del cls._unique_instances[path]
            except KeyError:
                # didn't exist - all fine
                pass

        # Massage URL
        url_ri = RI(url) if not isinstance(url, RI) else url
        if on_windows:
            # When we're cloning from a local path on Windows, the URL at
            # this point is platform-specific (e.g., "..\\origin"). According 
            # to Git clone's manpage, clone urls can't have backslashes.
            # While Git does manage to clone a URL with backslashes, 
            # in the case of subdatasets cloned from relative paths it nevertheless
            # messed up the resulting remote url, resulting in a mix of
            # front and backslashes (see also gh-7180): 
            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\origin' 
            # Therefore, we're turning it to Posix now.
            if isinstance(url_ri, PathRI):
                url = Path(url).as_posix()
                url_ri = PathRI(url)

        else:
            # if we are on windows, the local path of a URL
            # would not end up being a proper local path and cloning
            # would fail. Don't try to be smart and just pass the
            # URL along unmodified

            # try to get a local path from `url`:
            try:
                url = url_ri.localpath
                url_ri = RI(url)
            except ValueError:
                pass

        if is_ssh(url_ri):
            ssh_manager.get_connection(url).open()
        else:
            if isinstance(url_ri, PathRI):
                # expand user, because execution not going through a shell
                # doesn't work well otherwise
                new_url = os.path.expanduser(url)
                if url != new_url:
                    lgr.info("Expanded source path to %s from %s", new_url, url)
                    url = new_url

        cmd = cls._git_cmd_prefix + ['clone', '--progress']
        if clone_options:
            if isinstance(clone_options, Mapping):
                clone_options = to_options(**clone_options)
            cmd.extend(clone_options)
        cmd.extend([url, path])

        fix_annex = None
        ntries = 5  # 3 is not enough for robust workaround
        for trial in range(ntries):
            try:
                lgr.debug("Git clone from %s to %s", url, path)

                res = GitWitlessRunner().run(cmd, protocol=GitProgress)
                # fish out non-critical warnings by git-clone
                # (empty repo clone, etc.), all other content is logged
                # by the progress helper to 'debug'
                for errline in res['stderr'].splitlines():
                    if errline.startswith('warning:'):
                        lgr.warning(errline[8:].strip())
                lgr.debug("Git clone completed")
                break
            except CommandError as e:
                # log here but let caller decide what to do
                ce = CapturedException(e)
                str_e = str(e)
                # see https://github.com/datalad/datalad/issues/785
                if re.search("Request for .*aborted.*Unable to find", str_e,
                             re.DOTALL) \
                        and trial < ntries - 1:
                    lgr.info(
                        "Hit a known issue with Git (see GH#785). Trial #%d, "
                        "retrying",
                        trial)
                    continue
                    #(lgr.debug if expect_fail else lgr.error)(e_str)

                if "Clone succeeded, but checkout failed." in str_e:
                    fix_annex = ce
                    break

                raise

        # get ourselves a repository instance
        gr = cls(path, *args, **kwargs)
        if fix_annex:
            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then
            # would need to import our own subclass
            if hasattr(gr, 'is_valid_annex'):
                lgr.warning("Experienced issues while cloning. "
                            "Trying to fix it, using git-annex-fsck.")
                if not gr.is_initialized():
                    gr._init()
                gr.fsck()
            else:
                lgr.warning("Experienced issues while cloning: %s", fix_annex)
        # ensure that Git doesn't mangle relative paths into obscure absolute
        # paths: https://github.com/datalad/datalad/issues/3538
        if isinstance(url_ri, PathRI):
            url_path = Path(url)
            if not url_path.is_absolute():
                # get git-created path
                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'
                git_url = gr.config.get(remote_url)
                if Path(git_url).is_absolute():
                    # Git created an absolute path from a relative URL.
                    git_url = op.relpath(git_url, gr.path)
                path = Path(git_url)
                # always in POSIX even on Windows
                path = path.as_posix()
                gr.config.set(remote_url, path,
                              scope='local', force=True)
        return gr

    # Note: __del__ shouldn't be needed anymore as we switched to
    #       `weakref.finalize`.
    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods
    #
    #       Keeping both methods and this comment around as a reminder to not
    #       use __del__, if we figure there's a need for cleanup in the future.
    #
    # def __del__(self):
    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird
    #     # stalls etc
    #     self._cfg = None

    def is_valid_git(self):
        """Returns whether the underlying repository appears to be still valid

        Note, that this almost identical to the classmethod is_valid_repo().
        However, if we are testing an existing instance, we can save Path object
        creations. Since this testing is done a lot, this is relevant. Creation
        of the Path objects in is_valid_repo() takes nearly half the time of the
        entire function.

        Also note, that this method is bound to an instance but still
        class-dependent, meaning that a subclass cannot simply overwrite it.
        This is particularly important for the call from within __init__(),
        which in turn is called by the subclasses' __init__. Using an overwrite
        would lead to the wrong thing being called.
        """
        return self.is_valid()

    @classmethod
    def is_valid_repo(cls, path):
        """Returns if a given path points to a git repository"""
        return cls.is_valid(path)

    @staticmethod
    def get_git_dir(repo):
        """figure out a repo's gitdir

        '.git' might be a  directory, a symlink or a file

        Note
        ----
        This method is likely to get deprecated, please use GitRepo.dot_git instead!
        That one's not static, but it's cheaper and you should avoid
        not having an instance of a repo you're working on anyway.
        Note, that the property in opposition to this method returns an absolute path.


        Parameters
        ----------
        repo: path or Repo instance
          currently expected to be the repos base dir

        Returns
        -------
        str
          relative path to the repo's git dir; So, default would be ".git"
        """
        if isinstance(repo, GitRepo):
            return str(repo.dot_git)
        pathobj = Path(repo)
        dot_git = _get_dot_git(pathobj, ok_missing=False)
        try:
            dot_git = dot_git.relative_to(pathobj)
        except ValueError:
            # is not a subpath, return as is
            lgr.debug("Path %r is not subpath of %r", dot_git, pathobj)
        return str(dot_git)

    @property
    def config(self):
        # just proxy the core repo APIs property for backward-compatibility
        return self.cfg

    def is_with_annex(self):
        """Report if GitRepo (assumed) has (remotes with) a git-annex branch
        """
        return any(
            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')
            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])
        )

    @classmethod
    def get_toppath(cls, path, follow_up=True, git_options=None):
        """Return top-level of a repository given the path.

        Parameters
        -----------
        follow_up : bool
          If path has symlinks -- they get resolved by git.  If follow_up is
          True, we will follow original path up until we hit the same resolved
          path.  If no such path found, resolved one would be returned.
        git_options: list of str
          options to be passed to the git rev-parse call

        Return None if no parent directory contains a git repository.
        """
        cmd = ['git']
        if git_options:
            cmd.extend(git_options)
        cmd += ["rev-parse", "--show-toplevel"]
        try:
            out = GitWitlessRunner(cwd=path).run(
                cmd, protocol=StdOutErrCapture)
            toppath = out['stdout'].rstrip('\n\r')
        except CommandError:
            return None
        except OSError:
            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,
                                          git_options=git_options)

        # normalize the report, because, e.g. on windows it can come out
        # with improper directory separators (C:/Users/datalad)
        toppath = str(Path(toppath))

        if follow_up:
            path_ = path
            path_prev = ""
            while path_ and path_ != path_prev:  # on top /.. = /
                if str(Path(path_).resolve()) == toppath:
                    toppath = path_
                    break
                path_prev = path_
                path_ = dirname(path_)

        return toppath

    @normalize_paths
    def add(self, files, git=True, git_options=None, update=False):
        """Adds file(s) to the repository.

        Parameters
        ----------
        files: list
          list of paths to add
        git: bool
          somewhat ugly construction to be compatible with AnnexRepo.add();
          has to be always true.
        update: bool
          --update option for git-add. From git's manpage:
           Update the index just where it already has an entry matching
           <pathspec>. This removes as well as modifies index entries to match
           the working tree, but adds no new files.

           If no <pathspec> is given when --update option is used, all tracked
           files in the entire working tree are updated (old versions of Git
           used to limit the update to the current directory and its
           subdirectories).

        Returns
        -------
        list
          Of status dicts.
        """
        # under all circumstances call this class' add_ (otherwise
        # AnnexRepo.add would go into a loop
        return list(GitRepo.add_(self, files, git=git, git_options=git_options,
                    update=update))

    def add_(self, files, git=True, git_options=None, update=False):
        """Like `add`, but returns a generator"""
        # TODO: git_options is used as options for the git-add here,
        # instead of options to the git executable => rename for consistency

        if not git:
            lgr.warning(
                'GitRepo.add() called with git=%s, this should not happen',
                git)
            git = True

        # there is no other way then to collect all files into a list
        # at this point, because we need to pass them at once to a single
        # `git add` call
        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]

        if not (files or git_options or update):
            # wondering why just a warning? in cmdline this is also not an error
            lgr.warning("add was called with empty file list and no options.")
            return

        try:
            # without --verbose git 2.9.3  add does not return anything
            add_out = self._call_git(
                # Set annex.gitaddtoannex to prevent storing files in
                # annex with a v6+ annex repo.
                ['-c', 'annex.gitaddtoannex=false', 'add'] +
                ensure_list(git_options) +
                to_options(update=update) + ['--verbose'],
                files=files,
                pathspec_from_file=True,
                read_only=False,
            )
            # get all the entries
            for o in self._process_git_get_output(*add_out):
                yield o
            # Note: as opposed to git cmdline, force is True by default in
            #       gitpython, which would lead to add things, that are
            #       ignored or excluded otherwise
            # 2. Note: There is an issue with globbing (like adding '.'),
            #       which apparently doesn't care for 'force' and therefore
            #       adds '.git/...'. May be it's expanded at the wrong
            #       point in time or sth. like that.
            # For now, use direct call to git add.
            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,
            #                      force=False)
            # TODO: May be make use of 'fprogress'-option to indicate
            # progress
            # But then, we don't have it for git-annex add, anyway.
            #
            # TODO: Is write=True a reasonable way to do it?
            # May be should not write until success of operation is
            # confirmed?
            # What's best in case of a list of files?
        except OSError as e:
            lgr.error("add: %s", e)
            raise

        # Make sure return value from GitRepo is consistent with AnnexRepo
        # currently simulating similar return value, assuming success
        # for all files:
        # TODO: Make return values consistent across both *Repo classes!
        return

    @staticmethod
    def _process_git_get_output(stdout, stderr=None):
        """Given both outputs (stderr is ignored atm) of git add - process it

        Primarily to centralize handling in both indirect annex and direct
        modes when ran through proxy
        """
        return [{u'file': f, u'success': True}
                for f in re.findall("'(.*)'[\n$]", ensure_unicode(stdout))]

    @normalize_paths(match_return_type=False)
    def remove(self, files, recursive=False, **kwargs):
        """Remove files.

        Calls git-rm.

        Parameters
        ----------
        files: list of str
          list of paths to remove
        recursive: False
          whether to allow recursive removal from subdirectories
        kwargs:
          see `__init__`

        Returns
        -------
        [str]
          list of successfully removed files.
        """
        if recursive:
            kwargs['r'] = True

        # the name is chosen badly, but the purpose is to make sure that
        # any pending operations actually manifest themselves in the Git repo
        # on disk (in case of an AnnexRepo, it could be pending batch
        # processes that need closing)
        self.precommit()

        # output per removed file is expected to be "rm 'PATH'":
        return [
            line.strip()[4:-1]
            for line in self.call_git_items_(
                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)
        ]

    def precommit(self):
        """Perform pre-commit maintenance tasks
        """
        # we used to clean up GitPython here
        pass

    @staticmethod
    def _get_prefixed_commit_msg(msg):
        DATALAD_PREFIX = "[DATALAD]"
        return DATALAD_PREFIX if not msg else "%s %s" % (DATALAD_PREFIX, msg)

    def configure_fake_dates(self):
        """Configure repository to use fake dates.
        """
        lgr.debug("Enabling fake dates")
        self.config.set("datalad.fake-dates", "true")

    @property
    def fake_dates_enabled(self):
        """Is the repository configured to use fake dates?
        """
        # this turned into a private property of the CoreGitRepo
        return self._fake_dates_enabled

    def add_fake_dates(self, env):
        # was renamed in CoreGitRepo
        return self.add_fake_dates_to_env(env)

    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,
               files=None, date=None, index_file=None):
        """Commit changes to git.

        Parameters
        ----------
        msg: str, optional
          commit-message
        options: list of str, optional
          cmdline options for git-commit
        _datalad_msg: bool, optional
          To signal that commit is automated commit by datalad, so
          it would carry the [DATALAD] prefix
        careless: bool, optional
          if False, raise when there's nothing actually committed;
          if True, don't care
        files: list of str, optional
          path(s) to commit
        date: str, optional
          Date in one of the formats git understands
        index_file: str, optional
          An alternative index to use
        """

        self.precommit()

        # assemble commandline
        cmd = ['commit']
        options = ensure_list(options)

        if date:
            options += ["--date", date]

        orig_msg = msg
        if not msg:
            if '--amend' in options:
                if '--no-edit' not in options:
                    # don't overwrite old commit message with our default
                    # message by default, but re-use old one. In other words:
                    # Make --no-edit the default:
                    options += ["--no-edit"]
            else:
                msg = 'Recorded changes'
                _datalad_msg = True

        if _datalad_msg:
            msg = self._get_prefixed_commit_msg(msg)

        if msg:
            options += ["-m", msg]
        cmd.extend(options)

        # set up env for commit
        env = self.add_fake_dates(None) \
            if self.fake_dates_enabled else os.environ.copy()
        if index_file:
            env['GIT_INDEX_FILE'] = index_file

        lgr.debug("Committing via direct call of git: %s", cmd)

        prev_sha = self.get_hexsha()

        # Old code was doing clever --amend'ing of chunked series of commits manually
        # here, but with pathspec_from_file it is no longer needed.
        # store pre-commit state to be able to check if anything was committed
        try:
            # Note: call_git operates via joining call_git_items_ and that one wipes out
            # .stdout from exception and collects/repopulates stderr only. Let's use
            # _call_git which returns both outputs and collects/re-populates both stdout
            # **and** stderr
            _ = self._call_git(
                    cmd,
                    files=files,
                    env=env,
                    pathspec_from_file=True,
            )
        except CommandError as e:
            # real errors first
            if "did not match any file(s) known to git" in e.stderr:
                raise FileNotInRepositoryError(
                    cmd=e.cmd,
                    msg="File(s) unknown to git",
                    code=e.code,
                    filename=linesep.join([
                        l for l in e.stderr.splitlines()
                        if l.startswith("error: pathspec")
                    ])
                )
            # behavior choices now
            elif not careless:
                # not willing to compromise at all
                raise
            elif 'nothing to commit' in e.stdout:
                lgr.debug("nothing to commit in %s. Ignored.", self)
            elif 'no changes added to commit' in e.stdout or \
                    'nothing added to commit' in e.stdout:
                lgr.debug("no changes added to commit in %s. Ignored.", self)
            else:
                raise
        if orig_msg \
                or '--dry-run' in cmd \
                or prev_sha == self.get_hexsha() \
                or ('--amend' in cmd and '--no-edit' in cmd) \
                or (not is_interactive()) \
                or self.config.obtain('datalad.save.no-message') != 'interactive':
            # we had a message given, or nothing was committed, or prev. commit
            # was amended, or we are not connected to a terminal, or no
            # interactive message input is desired:
            # we can go home
            return

        # handle interactive message entry by running another `git-commit`
        self._git_runner.run(
            cmd + ['--amend', '--edit'],
            protocol=NoCapture,
            stdin=None,
            env=env,
        )

    # TODO usage is only in the tests, consider making a test helper and
    # remove from GitRepo API
    def get_indexed_files(self):
        """Get a list of files in git's index

        Returns
        -------
        list
            list of paths rooting in git's base dir
        """

        return [
            str(r.relative_to(self.pathobj))
            for r in self.get_content_info(
                paths=None, ref=None, untracked='no')
        ]

    def format_commit(self, fmt, commitish=None):
        """Return `git show` output for `commitish`.

        Parameters
        ----------
        fmt : str
            A format string accepted by `git show`.
        commitish: str, optional
          Any commit identifier (defaults to "HEAD").

        Returns
        -------
        str or, if there are not commits yet, None.
        """
        # use git-log and not git-show due to faster performance with
        # complex commits (e.g. octopus merges)
        # https://github.com/datalad/datalad/issues/4801
        cmd = ['log', '-1', '-z', '--format=' + fmt]
        if commitish is not None:
            cmd.append(commitish + "^{commit}")
        # make sure Git takes our argument as a revision
        cmd.append('--')
        try:
            stdout = self.call_git(
                cmd, expect_stderr=True, expect_fail=True,
                read_only=True)
        except CommandError as e:
            if 'bad revision' in e.stderr:
                raise ValueError("Unknown commit identifier: %s" % commitish)
            elif 'does not have any commits yet' in e.stderr:
                return None
            else:
                raise e
        # This trailing null is coming from the -z above, which avoids the
        # newline that Git would append to the output. We could drop -z and
        # strip the newline directly, but then we'd have to worry about
        # compatibility across platforms.
        return stdout.rsplit("\0", 1)[0]

    def get_hexsha(self, commitish=None, short=False):
        """Return a hexsha for a given commitish.

        Parameters
        ----------
        commitish : str, optional
          Any identifier that refers to a commit (defaults to "HEAD").
        short : bool, optional
          Return the abbreviated form of the hexsha.

        Returns
        -------
        str or, if no commitish was given and there are no commits yet, None.

        Raises
        ------
        ValueError
          If a commitish was given, but no corresponding commit could be
          determined.
        """
        # use --quiet because the 'Needed a single revision' error message
        # that is the result of running this in a repo with no commits
        # isn't useful to report
        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(
            commitish if commitish else 'HEAD')
        ]
        if short:
            cmd.append('--short')
        try:
            return self.call_git_oneline(cmd, read_only=True)
        except CommandError as e:
            if commitish is None:
                return None
            raise ValueError("Unknown commit identifier: %s" % commitish)

    @normalize_paths(match_return_type=False)
    def get_last_commit_hexsha(self, files):
        """Return the hash of the last commit the modified any of the given
        paths"""
        try:
            commit = self.call_git(
                ['rev-list', '-n1', 'HEAD'],
                files=files,
                expect_fail=True,
                read_only=True,
            )
            commit = commit.strip()
            return commit if commit else None
        except CommandError:
            if self.get_hexsha() is None:
                # unborn branch, don't freak out
                return None
            raise

    def get_revisions(self, revrange=None, fmt="%H", options=None):
        """Return list of revisions in `revrange`.

        Parameters
        ----------
        revrange : str or list of str or None, optional
            Revisions or revision ranges to walk. If None, revision defaults to
            HEAD unless a revision-modifying option like `--all` or
            `--branches` is included in `options`.
        fmt : string, optional
            Format accepted by `--format` option of `git log`. This should not
            contain new lines because the output is split on new lines.
        options : list of str, optional
            Options to pass to `git log`.  This should not include `--format`.

        Returns
        -------
        List of revisions (str), formatted according to `fmt`.
        """
        if revrange is None:
            revrange = []
        elif isinstance(revrange, str):
            revrange = [revrange]

        cmd = ["log", "--format={}".format(fmt)]
        cmd.extend((options or []) + revrange + ["--"])
        try:
            stdout = self.call_git(cmd, expect_fail=True, read_only=True)
        except CommandError as e:
            if "does not have any commits" in e.stderr:
                return []
            raise
        return stdout.splitlines()

    def commit_exists(self, commitish):
        """Does `commitish` exist in the repo?

        Parameters
        ----------
        commitish : str
            A commit or an object that can be dereferenced to one.

        Returns
        -------
        bool
        """
        # Note: The peeling operator "^{commit}" is required so that rev-parse
        # doesn't succeed if passed a full hexsha that is valid but doesn't
        # exist.
        return self.call_git_success(
            ["rev-parse", "--verify", commitish + "^{commit}"],
            read_only=True,
        )

    def get_merge_base(self, commitishes):
        """Get a merge base hexsha

        Parameters
        ----------
        commitishes: str or list of str
          List of commitishes (branches, hexshas, etc) to determine the merge
          base of. If a single value provided, returns merge_base with the
          current branch.

        Returns
        -------
        str or None
          If no merge-base for given commits, or specified treeish doesn't
          exist, None returned
        """
        if isinstance(commitishes, str):
            commitishes = [commitishes]
        if not commitishes:
            raise ValueError("Provide at least a single value")
        elif len(commitishes) == 1:
            commitishes = commitishes + [self.get_active_branch()]

        try:
            base = self.call_git_oneline(['merge-base'] + commitishes,
                                         read_only=True)
        except CommandError as exc:
            if exc.code == 1 and not (exc.stdout or exc.stderr):
                # No merge base was found (unrelated commits).
                return None
            if "fatal: Not a valid object name" in exc.stderr:
                return None
            raise

        return base

    def is_ancestor(self, reva, revb):
        """Is `reva` an ancestor of `revb`?

        Parameters
        ----------
        reva, revb : str
            Revisions.

        Returns
        -------
        bool
        """
        return self.call_git_success(
            ["merge-base", "--is-ancestor", reva, revb],
            read_only=True)

    def get_commit_date(self, branch=None, date='authored'):
        """Get the date stamp of the last commit (in a branch or head otherwise)

        Parameters
        ----------
        date: {'authored', 'committed'}
          Which date to return.  "authored" will be the date shown by "git show"
          and the one possibly specified via --date to `git commit`

        Returns
        -------
        int or None
          None if no commit
        """
        if date == 'committed':
            format = '%ct'
        elif date == 'authored':
            format = '%at'
        else:
            raise ValueError('unknow date type: {}'.format(date))
        d = self.format_commit(format, commitish=branch)
        return int(d) if d else None

    def get_active_branch(self):
        """Get the name of the active branch

        Returns
        -------
        str or None
          Returns None if there is no active branch, i.e. detached HEAD,
          and the branch name otherwise.
        """
        try:
            out = self.call_git(["symbolic-ref", "HEAD"], expect_fail=True,
                                read_only=True)
        except CommandError as e:
            if 'HEAD is not a symbolic ref' in e.stderr:
                lgr.debug("detached HEAD in %s", self)
                return None
            else:
                raise e
        return out.strip()[11:]  # strip refs/heads/

    def get_corresponding_branch(self, branch=None):
        """Always returns None, a plain GitRepo has no managed branches"""
        return None

    def get_branches(self):
        """Get all branches of the repo.

        Returns
        -------
        [str]
            Names of all branches of this repository.
        """

        return [
            b['refname:strip=2']
            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')
        ]

    def get_remote_branches(self):
        """Get all branches of all remotes of the repo.

        Returns
        -----------
        [str]
            Names of all remote branches.
        """
        # TODO: Reconsider melting with get_branches()

        # TODO: treat entries like this: origin/HEAD -> origin/master'
        # currently this is done in collection

        return [
            b['refname:strip=2']
            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')
        ]

    def get_remotes(self, with_urls_only=False):
        """Get known remotes of the repository

        Parameters
        ----------
        with_urls_only : bool, optional
          return only remotes which have urls

        Returns
        -------
        remotes : list of str
          List of names of the remotes
        """
        from datalad.utils import unique

        self.config.reload()
        remotes = unique([x[7:] for x in self.config.sections()
                          if x.startswith("remote.")])

        if with_urls_only:
            remotes = [
                r for r in remotes
                if self.config.get('remote.%s.url' % r)
            ]
        return remotes

    # TODO this is practically unused outside the tests, consider turning
    # into a test helper and trim from the API
    def get_files(self, branch=None):
        """Get a list of files in git.

        Lists the files in the (remote) branch.

        Parameters
        ----------
        branch: str
          Name of the branch to query. Default: active branch.

        Returns
        -------
        [str]
          list of files.
        """
        return [
            str(p.relative_to(self.pathobj))
            for p in self.get_content_info(
                paths=None, ref=branch, untracked='no')
            ]

    def add_remote(self, name, url, options=None):
        """Register remote pointing to a url
        """
        cmd = ['remote', 'add']
        if options:
            cmd += options
        cmd += [name, url]

        # for historical reasons this method returns stdout and
        # stderr, keeping that for now
        result = self._call_git(cmd)
        self.config.reload()
        return result

    def remove_remote(self, name):
        """Remove existing remote
        """

        # TODO: testing and error handling!
        from .exceptions import RemoteNotAvailableError
        try:
            self.call_git(['remote', 'remove', name])
        except CommandError as e:
            if 'No such remote' in e.stderr:
                raise RemoteNotAvailableError(name,
                                              cmd="git remote remove",
                                              msg="No such remote",
                                              stdout=e.stdout,
                                              stderr=e.stderr)
            else:
                raise e

        # config.reload necessary, because the associated remote config
        # will vanish
        self.config.reload()
        return

    def _maybe_open_ssh_connection(self, remote, prefer_push=True):
        """Open connection if `remote` has an SSH URL.

        Doing so enables SSH caching, preventing datalad-sshrun subprocesses
        from opening (and then closing) their own.

        Parameters
        ----------
        remote : str
        prefer_push : bool, optional
            Use `remote.<remote>.pushurl` if there is one, falling back to
            `remote.<remote>.url`.
        """
        if remote:
            url = None
            if prefer_push:
                url = self.get_remote_url(remote, push=True)
            url = url or self.get_remote_url(remote)
            if url and is_ssh(url):
                ssh_manager.get_connection(url).open()

    def update_remote(self, name=None, verbose=False):
        """
        """
        options = ["-v"] if verbose else []
        self._maybe_open_ssh_connection(name)
        name = [name] if name else []
        self.call_git(
            ['remote'] + name + ['update'] + options,
            expect_stderr=True
        )

    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,
              **kwargs):
        """Fetches changes from a remote (or all remotes).

        Parameters
        ----------
        remote : str, optional
          name of the remote to fetch from. If no remote is given and
          `all_` is not set, the tracking branch is fetched.
        refspec : str or list, optional
          refspec(s) to fetch.
        all_ : bool, optional
          fetch all remotes (and all of their branches).
          Fails if `remote` was given.
        git_options : list, optional
          Additional command line options for git-fetch.
        kwargs :
          Deprecated. GitPython-style keyword argument for git-fetch.
          Will be appended to any git_options.
        """
        git_options = ensure_list(git_options)
        if kwargs:
            git_options.extend(to_options(**kwargs))
        return list(
            self.fetch_(
                remote=remote,
                refspec=refspec,
                all_=all_,
                git_options=git_options,
            )
        )

    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):
        """Like `fetch`, but returns a generator"""
        yield from self._fetch_push_helper(
            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],
            action='fetch',
            urlvars=('remote.{}.url', 'remote.{}.url'),
            protocol=GitProgress,
            info_cls=FetchInfo,
            info_from='stderr',
            add_remote=False,
            remote=remote,
            refspec=refspec,
            all_=all_,
            git_options=git_options)

    def push(self, remote=None, refspec=None, all_remotes=False,
             all_=False, git_options=None, **kwargs):
        """Push changes to a remote (or all remotes).

        If remote and refspec are specified, and remote has
        `remote.{remote}.datalad-push-default-first` configuration variable
        set (e.g. by `create-sibling-github`), we will first push the first
        refspec separately to possibly ensure that the first refspec is chosen
        by remote as the "default branch".
        See https://github.com/datalad/datalad/issues/4997
        Upon successful push if this variable was set in the local git config,
        we unset it, so subsequent pushes would proceed normally.

        Parameters
        ----------
        remote : str, optional
          name of the remote to push to. If no remote is given and
          `all_` is not set, the tracking branch is pushed.
        refspec : str or list, optional
          refspec(s) to push.
        all_ : bool, optional
          push to all remotes. Fails if `remote` was given.
        git_options : list, optional
          Additional command line options for git-push.
        kwargs :
          Deprecated. GitPython-style keyword argument for git-push.
          Will be appended to any git_options.
        """
        git_options = ensure_list(git_options)
        if kwargs:
            git_options.extend(to_options(**kwargs))
        if all_remotes:
            # be nice to the elderly
            all_ = True

        push_refspecs = [refspec]
        cfg = self.config  # shortcut
        cfg_push_var = "remote.{}.datalad-push-default-first".format(remote)
        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):
            refspec = ensure_list(refspec)
            lgr.debug("As indicated by %s pushing first refspec %s separately first",
                      cfg_push_var, refspec[0])
            push_refspecs = [[refspec[0]], refspec[1:]]

        push_res = []
        for refspecs in push_refspecs:
            push_res.extend(
                self.push_(
                    remote=remote,
                    refspec=refspecs,
                    all_=all_,
                    git_options=git_options,
                )
            )
        # note: above push_ should raise exception if errors out
        if '--dry-run' not in git_options \
            and cfg.get_from_source('local', cfg_push_var) is not None:
            lgr.debug("Removing %s variable from local git config after successful push", cfg_push_var)
            cfg.unset(cfg_push_var, 'local')
        return push_res

    def push_(self, remote=None, refspec=None, all_=False, git_options=None):
        """Like `push`, but returns a generator"""
        yield from self._fetch_push_helper(
            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],
            action='push',
            urlvars=('remote.{}.pushurl', 'remote.{}.url'),
            protocol=StdOutCaptureWithGitProgress,
            info_cls=PushInfo,
            info_from='stdout',
            add_remote=True,
            remote=remote,
            refspec=refspec,
            all_=all_,
            git_options=git_options)

    def _fetch_push_helper(
            self,
            base_cmd,     # arg list
            action,       # label fetch|push
            urlvars,      # variables to query for URLs
            protocol,     # processor for output
            info_cls,     # Push|FetchInfo
            info_from,    # stdout, stderr
            add_remote,   # whether to add a 'remote' field to the info dict
            remote=None, refspec=None, all_=False, git_options=None):

        git_options = ensure_list(git_options)

        cmd = base_cmd + git_options

        if remote is None:
            if refspec:
                # conflicts with using tracking branch or push all remotes
                # For now: Just fail.
                # TODO: May be check whether it fits to tracking branch
                raise ValueError(
                    "refspec specified without a remote. ({})".format(refspec))
            if all_:
                remotes_to_process = self.get_remotes(with_urls_only=True)
            else:
                # No explicit remote to fetch.
                # => get tracking branch:
                tb_remote, refspec = self.get_tracking_branch()
                if tb_remote is not None:
                    remotes_to_process = [tb_remote]
                else:
                    # No remote, no tracking branch
                    # => fail
                    raise ValueError(
                        "Neither a remote is specified to {} "
                        "from nor a tracking branch is set up.".format(action))
        else:
            if all_:
                raise ValueError(
                    "Option 'all_' conflicts with specified remote "
                    "'{}'.".format(remote))
            remotes_to_process = [remote]

        if refspec:
            # prep for appending to cmd
            refspec = ensure_list(refspec)

        # no need for progress report, when there is just one remote
        log_remote_progress = len(remotes_to_process) > 1
        if log_remote_progress:
            pbar_id = '{}remotes-{}'.format(action, id(self))
            log_progress(
                lgr.info,
                pbar_id,
                'Start %sing remotes for %s', action, self,
                total=len(remotes_to_process),
                label=action.capitalize(),
                unit=' Remotes',
            )
        try:
            for remote in remotes_to_process:
                r_cmd = cmd + [remote]
                if refspec:
                    r_cmd += refspec

                if log_remote_progress:
                    log_progress(
                        lgr.info,
                        pbar_id,
                        '{}ing remote %s'.format(action.capitalize()),
                        remote,
                        update=1,
                        increment=True,
                    )
                # best effort to enable SSH connection caching
                url = self.config.get(
                    # make two attempts to get a URL
                    urlvars[0].format(remote),
                    self.config.get(
                        urlvars[1].format(remote),
                        None)
                )
                if url and is_ssh(url):
                    ssh_manager.get_connection(url).open()
                try:
                    out = self._git_runner.run(
                        r_cmd,
                        protocol=protocol,
                    )
                    output = out[info_from] or ''
                except CommandError as e:
                    output = None
                    # intercept some errors that we express as an error report
                    # in the info dicts
                    if re.match(
                            '.*^error: failed to (push|fetch) some refs',
                            e.stderr,
                            re.DOTALL | re.MULTILINE):
                        output = getattr(e, info_from)
                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()
                                          if l.startswith('hint: ')])
                        if output is None:
                            output = ''
                    if not output:
                        raise

                for line in output.splitlines():
                    try:
                        # push info doesn't identify a remote, add it here
                        pi = info_cls._from_line(line)
                        if add_remote:
                            pi['remote'] = remote
                        # There were errors, but Git provided hints
                        if 'error' in pi['operations']:
                            pi['hints'] = hints or None
                        yield pi
                    except Exception:
                        # it is not progress and no push info
                        # don't hide it completely
                        lgr.debug('git-%s reported: %s', action, line)
        finally:
            if log_remote_progress:
                log_progress(
                    lgr.info,
                    pbar_id,
                    'Finished %sing remotes for %s', action, self,
                )

    def get_remote_url(self, name, push=False):
        """Get the url of a remote.

        Reads the configuration of remote `name` and returns its url or None,
        if there is no url configured.

        Parameters
        ----------
        name: str
          name of the remote
        push: bool
          if True, get the pushurl instead of the fetch url.
        """

        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')
        return self.config.get(var, None)

    def set_remote_url(self, name, url, push=False):
        """Set the URL a remote is pointing to

        Sets the URL of the remote `name`. Requires the remote to already exist.

        Parameters
        ----------
        name: str
          name of the remote
        url: str
        push: bool
          if True, set the push URL, otherwise the fetch URL
        """

        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')
        self.config.set(var, url, scope='local', reload=True)

    def get_branch_commits_(self, branch=None, limit=None, stop=None):
        """Return commit hexshas for a branch

        Parameters
        ----------
        branch: str, optional
          If not provided, assumes current branch
        limit: None | 'left-only', optional
          Limit which commits to report.  If None -- all commits (merged or not),
          if 'left-only' -- only the commits from the left side of the tree upon
          merges
        stop: str, optional
          hexsha of the commit at which stop reporting (matched one is not
          reported either)

        Yields
        ------
        str
        """
        cmd = ['rev-list']
        if limit == 'left-only':
            cmd.append('--left-only')
        if not branch:
            branch = self.get_active_branch()
        cmd.append(branch)
        # and trailing -- marker to make sure that Git never confused the branch
        # with a potentially existing directory of the same name
        cmd.append('--')
        for r in self.call_git_items_(cmd):
            if stop and stop == r:
                return
            yield r

    def checkout(self, name, options=None):
        """
        """
        # TODO: May be check for the need of -b options herein?
        cmd = ['checkout']
        if options:
            cmd += options
        cmd += [str(name)]

        self.call_git(cmd, expect_stderr=True)
        # checkout can change committed config, or create branch config
        self.config.reload()

    # TODO: Before implementing annex merge, find usages and check for a needed
    # change to call super().merge
    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):
        if options is None:
            options = []
        if msg:
            options = options + ["-m", msg]
        options += ['--allow-unrelated-histories']
        self.call_git(
            ['merge'] + options + [name],
            **kwargs
        )

    def remove_branch(self, branch):
        self.call_git(['branch', '-D', branch])

    def cherry_pick(self, commit):
        """Cherry pick `commit` to the current branch.

        Parameters
        ----------
        commit : str
            A single commit.
        """
        self.call_git(["cherry-pick", commit])

    @property
    def dirty(self):
        """Is the repository dirty?

        Note: This provides a quick answer when you simply want to know if
        there are any untracked changes or modifications in this repository or
        its submodules. For finer-grained control and more detailed reporting,
        use status() instead.
        """
        stdout = self.call_git(
            ["status", "--porcelain",
             # Ensure the result isn't influenced by status.showUntrackedFiles.
             "--untracked-files=normal",
             # Ensure the result isn't influenced by diff.ignoreSubmodules.
             "--ignore-submodules=none"])
        if bool(stdout.strip()):
            # The quick `git status`-based check can give a different answer
            # than `datalad status` for submodules on an adjusted branch.
            st = self.diffstatus(fr="HEAD" if self.get_hexsha() else None,
                                 to=None, untracked="normal")
            return any(r.get("state") != "clean" for r in st.values())
        return False

    @property
    def untracked_files(self):
        """Legacy interface, do not use! Use the status() method instead.

        Despite its name, it also reports on untracked datasets, and
        yields their names with trailing path separators.
        """
        return [
            '{}{}'.format(
                str(p.relative_to(self.pathobj)),
                os.sep if props['type'] != 'file' else ''
            )
            for p, props in self.status(
                    untracked='all', eval_submodule_state='no').items()
            if props.get('state', None) == 'untracked'
        ]

    def gc(self, allow_background=False, auto=False):
        """Perform house keeping (garbage collection, repacking)"""
        cmd_options = []
        if not allow_background:
            cmd_options += ['-c', 'gc.autodetach=0']
        cmd_options += ['gc', '--aggressive']
        if auto:
            cmd_options += ['--auto']
        self.call_git(cmd_options)

    def _parse_gitmodules(self):
        # TODO read .gitconfig from Git blob?
        gitmodules = self.pathobj / '.gitmodules'
        if not gitmodules.exists():
            return {}
        # pull out file content
        out = self.call_git(
            ['config', '-z', '-l', '--file', '.gitmodules'],
            read_only=True)
        # abuse our config parser
        # disable multi-value report, because we could not deal with them
        # anyways, and they should not appear in a normal .gitmodules file
        # but could easily appear when duplicates are included. In this case,
        # we better not crash
        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)
        mods = {}
        for k, v in db.items():
            if not k.startswith('submodule.'):
                # we don't know what this is
                lgr.warning("Skip unrecognized .gitmodule specification: %s=%s", k, v)
                continue
            k_l = k.split('.')
            # module name is everything after 'submodule.' that is not the variable
            # name
            mod_name = '.'.join(k_l[1:-1])
            mod = mods.get(mod_name, {})
            # variable name is the last 'dot-free' segment in the key
            mod[k_l[-1]] = v
            mods[mod_name] = mod

        out = {}
        # bring into traditional shape
        for name, props in mods.items():
            if 'path' not in props:
                lgr.warning("Failed to get '%s.path', skipping this submodule", name)
                continue
            modprops = {'gitmodule_{}'.format(k): v
                        for k, v in props.items()
                        if not k.startswith('__')}
            # Keep as PurePosixPath for possible normalization of / in the path etc
            modpath = PurePosixPath(props['path'])
            modprops['gitmodule_name'] = name
            out[modpath] = modprops
        return out

    def get_submodules_(self, paths=None):
        """Yield submodules in this repository.

        Parameters
        ----------
        paths : list(pathlib.PurePath), optional
            Restrict submodules to those under `paths`. Paths must be relative
            to the resolved repository root, and must be normed to match the
            reporting done by Git, i.e. no parent dir components
            (ala "some/../this").

        Returns
        -------
        A generator that yields a dictionary with information for each
        submodule.
        """
        if not (self.pathobj / ".gitmodules").exists():
            return

        modinfo = self._parse_gitmodules()
        if not modinfo:
            # we exit early, if there is nothing on record (even though
            # a .gitmodules file exists).
            # without this conditional exit, we would be able to discover
            # subprojects even when they are not recorded in .gitmodules,
            # but at the cost of running an unconstrained ls-files call
            # below
            return

        posix_mod_paths = [m.as_posix() for m in modinfo]
        if paths:
            # harmonize them into relative to the repository
            posix_paths = []
            for path in paths:
                path = ut.PurePath(path)
                if path.is_absolute():
                    try:
                        path = path.relative_to(self.pathobj)
                    except ValueError as exc:
                        lgr.debug(
                            "Path %s it not underneath %s, skipping since nothing should match it: %s",
                            path, self.pathobj, CapturedException(exc)
                        )
                        continue
                posix_paths.append(path.as_posix())

            # constrain the report by the given paths, make sure all paths are POSIX
            posix_mod_paths = list(get_filtered_paths_(
                posix_mod_paths,
                posix_paths,
                include_within_path=True,
            ))

        for r in self.call_git_items_(
            ['ls-files', '--stage', '-z'],
            sep='\0',
            files=posix_mod_paths,
            read_only=True,
            keep_ends=True,
        ):
            if not r.startswith('160000'):
                # make sure this method never talks about non-dataset
                # content
                continue
            props, rpath = r.split('\t')
            mode, gitsha, stage = props.split(' ')
            if stage not in ('0', '2'):
                # we either have non-merge situation, or a simple merge
                # situation (i.e. stage=0). the reported gitsha always
                # matches what we have locally.
                # or we are in a three-way merge, in which case stage=2
                # is what we want to report, because it matches the
                # current HEAD (see git-read-tree manpage).
                # there is either a stage 2 or stage 0, never both
                continue
            # remove the expected line separator from the path
            rpath = rpath[:-1]
            path = PurePosixPath(rpath)
            yield dict(
                path=self.pathobj / rpath, # full path returned here
                type='dataset',
                gitshasum=gitsha,
                **modinfo.get(path, {})
            )

    def get_submodules(self, sorted_=True, paths=None):
        """Return list of submodules.

        Parameters
        ----------
        sorted_ : bool, optional
            Sort submodules by path name.
        paths : list(pathlib.PurePath), optional
            Restrict submodules to those under `paths`.

        Returns
        -------
        List of submodule namedtuples if `compat` is true or otherwise a list
        of dictionaries as returned by `get_submodules_`.
        """
        xs = self.get_submodules_(paths=paths)

        if sorted_:
            xs = sorted(xs, key=lambda x: x["path"])
        return list(xs)

    def update_ref(self, ref, value, oldvalue=None, symbolic=False):
        """Update the object name stored in a ref "safely".

        Just a shim for `git update-ref` call if not symbolic, and
        `git symbolic-ref` if symbolic

        Parameters
        ----------
        ref : str
          Reference, such as `ref/heads/BRANCHNAME` or HEAD.
        value : str
          Value to update to, e.g. hexsha of a commit when updating for a
          branch ref, or branch ref if updating HEAD
        oldvalue: str
          Value to update from. Safeguard to be verified by git. This is only
          valid if `symbolic` is not True.
        symbolic : None
          To instruct if ref is symbolic, e.g. should be used in case of
          ref=HEAD
        """
        if symbolic:
            if oldvalue:
                raise ValueError("oldvalue and symbolic must not be given both")
            cmd = ['symbolic-ref', ref, value]
        else:
            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])
        self.call_git(cmd)

    def tag(self, tag, message=None, commit=None, options=None):
        """Tag a commit

        Parameters
        ----------
        tag : str
          Custom tag label. Must be a valid tag name.
        message : str, optional
          If provided, adds ['-m', <message>] to the list of `git tag`
          arguments.
        commit : str, optional
          If provided, will be appended as last argument to the `git tag` call,
          and can be used to identify the commit that shall be tagged, if
          not HEAD.
        options : list, optional
          Additional command options, inserted prior a potential `commit`
          argument.
        """
        # TODO: call in save.py complains about extensive logging. When does it
        # happen in what way? Figure out, whether to just silence it or raise or
        # whatever else.
        args = ['tag']
        if message:
            args += ['-m', message]
        if options is not None:
            args.extend(options)
        args.append(tag)
        if commit:
            args.append(commit)
        self.call_git(args)

    def get_tags(self, output=None):
        """Get list of tags

        Parameters
        ----------
        output : str, optional
          If given, limit the return value to a list of values matching that
          particular key of the tag properties.

        Returns
        -------
        list
          Each item is a dictionary with information on a tag. At present
          this includes 'hexsha', and 'name', where the latter is the string
          label of the tag, and the former the hexsha of the object the tag
          is attached to. The list is sorted by the creator date (committer
          date for lightweight tags and tagger date for annotated tags), with
          the most recent commit being the last element.
        """
        tags = [
            dict(
                name=t['refname:strip=2'],
                hexsha=t['object'] if t['object'] else t['objectname'],
            )
            for t in self.for_each_ref_(
                fields=['refname:strip=2', 'objectname', 'object'],
                pattern='refs/tags',
                sort='creatordate')
        ]
        if output:
            return [t[output] for t in tags]
        else:
            return tags

    def describe(self, commitish=None, **kwargs):
        """ Quick and dirty implementation to call git-describe

        Parameters
        ----------
        kwargs:
            transformed to cmdline options for git-describe;
            see __init__ for description of the transformation
        """
        # TODO: be more precise what failure to expect when and raise actual
        # errors
        cmd = ['describe'] + to_options(**kwargs)
        if commitish is not None:
            cmd.append(commitish)
        try:
            describe = self.call_git(cmd, expect_fail=True)
            return describe.strip()
        # TODO: WTF "catch everything"?
        except:
            return None

    def get_tracking_branch(self, branch=None, remote_only=False):
        """Get the tracking branch for `branch` if there is any.

        Parameters
        ----------
        branch: str
            local branch to look up. If none is given, active branch is used.
        remote_only : bool
            Don't return a value if the upstream remote is set to "." (meaning
            this repository).

        Returns
        -------
        tuple
            (remote or None, refspec or None) of the tracking branch
        """
        if branch is None:
            branch = self.get_corresponding_branch() or self.get_active_branch()
            if branch is None:
                return None, None

        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)
        if remote_only and track_remote == ".":
            return None, None
        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)
        return track_remote, track_branch

    @property
    def count_objects(self):
        """return dictionary with count, size(in KiB) information of git objects
        """

        count_cmd = ['count-objects', '-v']
        count_str = self.call_git(count_cmd)
        count = {key: int(value)
                 for key, value in [item.split(': ')
                                    for item in count_str.split('\n')
                                    if len(item.split(': ')) == 2]}
        return count

    def get_git_attributes(self):
        """Query gitattributes which apply to top level directory

        It is a thin compatibility/shortcut wrapper around more versatile
        get_gitattributes which operates on a list of paths and returns
        a dictionary per each path

        Returns
        -------
        dict:
          a dictionary with attribute name and value items relevant for the
          top ('.') directory of the repository, and thus most likely the
          default ones (if not overwritten with more rules) for all files within
          repo.
        """
        return self.get_gitattributes('.')['.']

    def get_gitattributes(self, path, index_only=False):
        """Query gitattributes for one or more paths

        Parameters
        ----------
        path: path or list
          Path(s) to query. Paths may be relative or absolute.
        index_only: bool
          Flag whether to consider only gitattribute setting that are reflected
          in the repository index, not just in the work tree content.

        Returns
        -------
        dict:
          Each key is a queried path (always relative to the repository root),
          each value is a dictionary with attribute
          name and value items. Attribute values are either True or False,
          for set and unset attributes, or are the literal attribute value.
        """
        path = ensure_list(path)
        cmd = ["check-attr", "-z", "--all"]
        if index_only:
            cmd.append('--cached')
        # make sure we have one entry for each query path to
        # simplify work with the result
        attributes = {p: {} for p in path}
        attr = []
        for item in self.call_git_items_(cmd, files=path, sep='\0',
                                         read_only=True):
            attr.append(item)
            if len(attr) < 3:
                continue
            # we have a full record
            p, name, value = attr
            attrs = attributes[p]
            attrs[name] = \
                True if value == 'set' else False if value == 'unset' else value
            # done, reset item
            attr = []
        return {relpath(k, self.path) if isabs(k) else k: v
                for k, v in attributes.items()}

    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):
        """Set gitattributes

        By default appends additional lines to `attrfile`. Note, that later
        lines in `attrfile` overrule earlier ones, which may or may not be
        what you want. Set `mode` to 'w' to replace the entire file by
        what you provided in `attrs`.

        Parameters
        ----------
        attrs : list
          Each item is a 2-tuple, where the first element is a path pattern,
          and the second element is a dictionary with attribute key/value
          pairs. The attribute dictionary must use the same semantics as those
          returned by `get_gitattributes()`. Path patterns can use absolute paths,
          in which case they will be normalized relative to the directory
          that contains the target .gitattributes file (see `attrfile`).
        attrfile: path
          Path relative to the repository root of the .gitattributes file the
          attributes shall be set in.
        mode: str
          'a' to append .gitattributes, 'w' to replace it
        """

        git_attributes_file = op.join(self.path, attrfile)
        attrdir = op.dirname(git_attributes_file)
        if not op.exists(attrdir):
            os.makedirs(attrdir)

        with open(git_attributes_file, mode + '+') as f:
            # for append, fix existing files that do not end with \n
            if mode == 'a' and f.tell():
                f.seek(max(0, f.tell() - len(os.linesep)))
                if not f.read().endswith('\n'):
                    f.write('\n')

            for pattern, attr in sorted(attrs, key=lambda x: x[0]):
                # normalize the pattern relative to the target .gitattributes file
                npath = _normalize_path(
                    op.join(self.path, op.dirname(attrfile)), pattern)
                # paths in gitattributes always have to be POSIX
                npath = Path(npath).as_posix()
                attrline = u''
                if npath.count(' '):
                    # quote patterns with spaces
                    attrline += u'"{}"'.format(npath.replace('"', '\\"'))
                else:
                    attrline += npath
                for a in sorted(attr):
                    val = attr[a]
                    if val is True:
                        attrline += ' {}'.format(a)
                    elif val is False:
                        attrline += ' -{}'.format(a)
                    else:
                        attrline += ' {}={}'.format(a, val)
                f.write('{}\n'.format(attrline))

    def get_content_info(self, paths=None, ref=None, untracked='all'):
        """Get identifier and type information from repository content.

        This is simplified front-end for `git ls-files/tree`.

        Both commands differ in their behavior when queried about subdataset
        paths. ls-files will not report anything, ls-tree will report on the
        subdataset record. This function uniformly follows the behavior of
        ls-tree (report on the respective subdataset mount).

        Parameters
        ----------
        paths : list(pathlib.PurePath) or None
          Specific paths, relative to the resolved repository root, to query
          info for. Paths must be normed to match the reporting done by Git,
          i.e. no parent dir components (ala "some/../this").
          If `None`, info is reported for all content.
        ref : gitref or None
          If given, content information is retrieved for this Git reference
          (via ls-tree), otherwise content information is produced for the
          present work tree (via ls-files). With a given reference, the
          reported content properties also contain a 'bytesize' record,
          stating the size of a file in bytes.
        untracked : {'no', 'normal', 'all'}
          If and how untracked content is reported when no `ref` was given:
          'no': no untracked files are reported; 'normal': untracked files
          and entire untracked directories are reported as such; 'all': report
          individual files even in fully untracked directories.

        Returns
        -------
        dict
          Each content item has an entry under a pathlib `Path` object instance
          pointing to its absolute path inside the repository (this path is
          guaranteed to be underneath `Repo.path`).
          Each value is a dictionary with properties:

          `type`
            Can be 'file', 'symlink', 'dataset', 'directory'

          `gitshasum`
            SHASUM of the item as tracked by Git, or None, if not
            tracked. This could be different from the SHASUM of the file
            in the worktree, if it was modified.

        Raises
        ------
        ValueError
          In case of an invalid Git reference (e.g. 'HEAD' in an empty
          repository)
        """
        lgr.debug('%s.get_content_info(...)', self)
        # TODO limit by file type to replace code in subdatasets command
        info = dict()

        if paths:  # is not None separate after
            # path matching will happen against what Git reports
            # and Git always reports POSIX paths
            # any incoming path has to be relative already, so we can simply
            # convert unconditionally
            # note: will be list-ified below
            posix_paths = [ut.PurePath(p).as_posix() for p in paths]
        elif paths is not None:
            return info
        else:
            posix_paths = None

        if posix_paths and (not ref or external_versions["cmd:git"] >= "2.29.0"):
            # If a path points within a submodule, we need to map it to the
            # containing submodule before feeding it to ls-files or ls-tree.
            #
            # Before Git 2.29.0, ls-tree and ls-files differed in how they
            # reported paths within submodules: ls-files provided no output,
            # and ls-tree listed the submodule. Now they both return no output.
            submodules = [s["path"].relative_to(self.pathobj).as_posix()
                          for s in self.get_submodules_()]
            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well
            posix_paths = get_parent_paths(posix_paths, submodules)

        # this will not work in direct mode, but everything else should be
        # just fine
        if not ref:
            # make sure no operations are pending before we figure things
            # out in the worktree
            self.precommit()

            # --exclude-standard will make sure to honor and standard way
            # git can be instructed to ignore content, and will prevent
            # crap from contaminating untracked file reports
            cmd = ['ls-files', '--stage', '-z']
            # untracked report mode, using labels from `git diff` option style
            if untracked == 'all':
                cmd += ['--exclude-standard', '-o']
            elif untracked == 'normal':
                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']
            elif untracked == 'no':
                pass
            else:
                raise ValueError(
                    'unknown value for `untracked`: {}'.format(untracked))
            props_re = re.compile(
                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\t(?P<fname>.*)$')
        else:
            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']
            props_re = re.compile(
                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\s]*(?P<size>[0-9-]+)\t(?P<fname>.*)$')

        lgr.debug('Query repo: %s', cmd)
        try:
            stdout = self.call_git(
                cmd,
                files=posix_paths,
                expect_fail=True,
                read_only=True)
        except CommandError as exc:
            if "fatal: Not a valid object name" in exc.stderr:
                raise InvalidGitReferenceError(ref)
            raise
        lgr.debug('Done query repo: %s', cmd)

        self._get_content_info_line_helper(
            ref,
            info,
            stdout.split('\0'),
            props_re)

        lgr.debug('Done %s.get_content_info(...)', self)
        return info

    def _get_content_info_line_helper(self, ref, info, lines, props_re):
        """Internal helper of get_content_info() to parse Git output"""
        mode_type_map = {
            '100644': 'file',
            '100755': 'file',
            '120000': 'symlink',
            '160000': 'dataset',
        }
        for line in lines:
            if not line:
                continue
            inf = {}
            props = props_re.match(line)
            if not props:
                # Kludge: Filter out paths starting with .git/ to work around
                # an `ls-files -o` bug that was fixed in Git 2.25.
                #
                # TODO: Drop this condition when GIT_MIN_VERSION is at least
                # 2.25.
                if line.startswith(".git/"):
                    lgr.debug("Filtering out .git/ file: %s", line)
                    continue
                # not known to Git, but Git always reports POSIX
                path = ut.PurePosixPath(line)
                inf['gitshasum'] = None
            else:
                # again Git reports always in POSIX
                path = ut.PurePosixPath(props.group('fname'))

            # revisit the file props after this path has not been rejected
            if props:
                inf['gitshasum'] = props.group('sha')
                inf['type'] = mode_type_map.get(
                    props.group('type'), props.group('type'))

                if ref and inf['type'] == 'file':
                    inf['bytesize'] = int(props.group('size'))

            # join item path with repo path to get a universally useful
            # path representation with auto-conversion and tons of other
            # stuff
            path = self.pathobj.joinpath(path)
            if 'type' not in inf:
                # be nice and assign types for untracked content
                inf['type'] = 'symlink' if path.is_symlink() \
                    else 'directory' if path.is_dir() else 'file'
            info[path] = inf

    def status(self, paths=None, untracked='all', eval_submodule_state='full'):
        """Simplified `git status` equivalent.

        Parameters
        ----------
        paths : list or None
          If given, limits the query to the specified paths. To query all
          paths specify `None`, not an empty list. If a query path points
          into a subdataset, a report is made on the subdataset record
          within the queried dataset only (no recursion).
        untracked : {'no', 'normal', 'all'}
          If and how untracked content is reported:
          'no': no untracked files are reported; 'normal': untracked files
          and entire untracked directories are reported as such; 'all': report
          individual files even in fully untracked directories.
        eval_submodule_state : {'full', 'commit', 'no'}
          If 'full' (the default), the state of a submodule is evaluated by
          considering all modifications, with the treatment of untracked files
          determined by `untracked`. If 'commit', the modification check is
          restricted to comparing the submodule's HEAD commit to the one
          recorded in the superdataset. If 'no', the state of the subdataset is
          not evaluated.

        Returns
        -------
        dict
          Each content item has an entry under a pathlib `Path` object instance
          pointing to its absolute path inside the repository (this path is
          guaranteed to be underneath `Repo.path`).
          Each value is a dictionary with properties:

          `type`
            Can be 'file', 'symlink', 'dataset', 'directory'
          `state`
            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.
        """
        lgr.debug('Query status of %r for %s paths',
                  self, len(paths) if paths is not None else 'all')
        return self.diffstatus(
            fr='HEAD' if self.get_hexsha() else None,
            to=None,
            paths=paths,
            untracked=untracked,
            eval_submodule_state=eval_submodule_state)

    def diff(self, fr, to, paths=None, untracked='all',
             eval_submodule_state='full'):
        """Like status(), but reports changes between to arbitrary revisions

        Parameters
        ----------
        fr : str or None
          Revision specification (anything that Git understands). Passing
          `None` considers anything in the target state as new.
        to : str or None
          Revision specification (anything that Git understands), or None
          to compare to the state of the work tree.
        paths : list or None
          If given, limits the query to the specified paths. To query all
          paths specify `None`, not an empty list.
        untracked : {'no', 'normal', 'all'}
          If and how untracked content is reported when `to` is None:
          'no': no untracked files are reported; 'normal': untracked files
          and entire untracked directories are reported as such; 'all': report
          individual files even in fully untracked directories.
        eval_submodule_state : {'full', 'commit', 'no'}
          If 'full' (the default), the state of a submodule is evaluated by
          considering all modifications, with the treatment of untracked files
          determined by `untracked`. If 'commit', the modification check is
          restricted to comparing the submodule's HEAD commit to the one
          recorded in the superdataset. If 'no', the state of the subdataset is
          not evaluated.

        Returns
        -------
        dict
          Each content item has an entry under a pathlib `Path` object instance
          pointing to its absolute path inside the repository (this path is
          guaranteed to be underneath `Repo.path`).
          Each value is a dictionary with properties:

          `type`
            Can be 'file', 'symlink', 'dataset', 'directory'
          `state`
            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.
        """
        return {k: v for k, v in self.diffstatus(
            fr=fr, to=to, paths=paths,
            untracked=untracked,
            eval_submodule_state=eval_submodule_state).items()
            if v.get('state', None) != 'clean'}

    def diffstatus(self, fr, to, paths=None, untracked='all',
                   eval_submodule_state='full', _cache=None):
        """Like diff(), but reports the status of 'clean' content too.

        It supports an additional submodule evaluation state 'global'.
        If given, it will return a single 'modified'
        (vs. 'clean') state label for the entire repository, as soon as
        it can.
        """
        def _get_cache_key(label, paths, ref, untracked=None):
            return self.path, label, tuple(paths) if paths else None, \
                ref, untracked

        if _cache is None:
            _cache = {}

        if paths is not None:
            # at this point we must normalize paths to the form that
            # Git would report them, to easy matching later on
            paths = map(ut.Path, paths)
            paths = [
                p.relative_to(self.pathobj) if p.is_absolute() else p
                for p in paths
            ]

        # TODO report more info from get_content_info() calls in return
        # value, those are cheap and possibly useful to a consumer
        # we need (at most) three calls to git
        if to is None:
            # everything we know about the worktree, including os.stat
            # for each file
            key = _get_cache_key('ci', paths, None, untracked)
            if key in _cache:
                to_state = _cache[key]
            else:
                to_state = self.get_content_info(
                    paths=paths, ref=None, untracked=untracked)
                _cache[key] = to_state
            # we want Git to tell us what it considers modified and avoid
            # reimplementing logic ourselves
            key = _get_cache_key('mod', paths, None)
            if key in _cache:
                modified = _cache[key]
            else:
                # from Git 2.31.0 onwards ls-files has --deduplicate
                # by for backward compatibility keep doing deduplication here
                modified = set(
                    self.pathobj.joinpath(ut.PurePosixPath(p))
                    for p in self.call_git_items_(
                        # we must also look for deleted files, for the logic
                        # below to work. Only from Git 2.31.0 would they be
                        # included with `-m` alone
                        ['ls-files', '-z', '-m', '-d'],
                        # low-level code cannot handle pathobjs
                        files=[str(p) for p in paths] if paths is not None else None,
                        sep='\0',
                        read_only=True)
                    if p)
                _cache[key] = modified
        else:
            key = _get_cache_key('ci', paths, to)
            if key in _cache:
                to_state = _cache[key]
            else:
                to_state = self.get_content_info(paths=paths, ref=to)
                _cache[key] = to_state
            # we do not need worktree modification detection in this case
            modified = None
        # origin state
        key = _get_cache_key('ci', paths, fr)
        if key in _cache:
            from_state = _cache[key]
        else:
            if fr:
                from_state = self.get_content_info(paths=paths, ref=fr)
            else:
                # no ref means from nothing
                from_state = {}
            _cache[key] = from_state

        status = dict()
        for f, to_state_r in to_state.items():
            props = self._diffstatus_get_state_props(
                f,
                from_state.get(f, None),
                to_state_r,
                # are we comparing against a recorded commit or the worktree
                to is not None,
                # if we have worktree modification info, report if
                # path is reported as modified in it
                modified and f in modified,
                eval_submodule_state)
            # potential early exit in "global" eval mode
            if eval_submodule_state == 'global' and \
                    props.get('state', None) not in ('clean', None):
                # any modification means globally 'modified'
                return 'modified'
            status[f] = props

        for f, from_state_r in from_state.items():
            if f not in to_state:
                # we new this, but now it is gone and Git is not complaining
                # about it being missing -> properly deleted and deletion
                # stages
                status[f] = dict(
                    state='deleted',
                    type=from_state_r['type'],
                    # report the shasum to distinguish from a plainly vanished
                    # file
                    gitshasum=from_state_r['gitshasum'],
                )
                if eval_submodule_state == 'global':
                    return 'modified'

        if to is not None or eval_submodule_state == 'no':
            # if we have `to` we are specifically comparing against
            # a recorded state, and this function only attempts
            # to label the state of a subdataset, not investigate
            # specifically what the changes in subdatasets are
            # this is done by a high-level command like rev-diff
            # so the comparison within this repo and the present
            # `state` label are all we need, and they are done already
            if eval_submodule_state == 'global':
                return 'clean'
            else:
                return status

        # loop over all subdatasets and look for additional modifications
        for f, st in status.items():
            f = str(f)
            if 'state' in st or not st['type'] == 'dataset':
                # no business here
                continue
            if not GitRepo.is_valid_repo(f):
                # submodule is not present, no chance for a conflict
                st['state'] = 'clean'
                continue
            # we have to recurse into the dataset and get its status
            subrepo = repo_from_path(f)
            # get the HEAD commit, or the one of the corresponding branch
            # only that one counts re super-sub relationship
            # save() syncs the corresponding branch each time
            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())
            st['gitshasum'] = subrepo_commit
            # subdataset records must be labeled clean up to this point
            # test if current commit in subdataset deviates from what is
            # recorded in the dataset
            st['state'] = 'modified' \
                if st['prev_gitshasum'] != subrepo_commit \
                else 'clean'
            if eval_submodule_state == 'global' and st['state'] == 'modified':
                return 'modified'
            if eval_submodule_state == 'commit':
                continue
            # the recorded commit did not change, so we need to make
            # a more expensive traversal
            st['state'] = subrepo.diffstatus(
                # we can use 'HEAD' because we know that the commit
                # did not change. using 'HEAD' will facilitate
                # caching the result
                fr='HEAD',
                to=None,
                paths=None,
                untracked=untracked,
                eval_submodule_state='global',
                _cache=_cache) if st['state'] == 'clean' else 'modified'
            if eval_submodule_state == 'global' and st['state'] == 'modified':
                return 'modified'

        if eval_submodule_state == 'global':
            return 'clean'
        else:
            return status

    def _diffstatus_get_state_props(self, f, from_state, to_state,
                                    against_commit,
                                    modified_in_worktree,
                                    eval_submodule_state):
        """Helper to determine diff properties for a single path

        Parameters
        ----------
        f : Path
        from_state : dict
        to_state : dict
        against_commit : bool
          Flag whether `to_state` reflects a commit or the worktree.
        modified_in_worktree : bool
          Flag whether a worktree modification is reported. This is ignored
          when `against_commit` is True.
        eval_submodule_state : {'commit', 'no', ...}
        """
        if against_commit:
            # we can ignore any worktree modification reported when
            # comparing against a commit
            modified_in_worktree = False

        props = {}
        if 'type' in to_state:
            props['type'] = to_state['type']

        to_sha = to_state['gitshasum']
        from_sha = from_state['gitshasum'] if from_state else None

        # determine the state of `f` from from_state and to_state records, if
        # it can be determined conclusively from it. If not, it will
        # stay None for now
        state = None
        if not from_state:
            # this is new, or rather not known to the previous state
            state = 'added' if to_sha else 'untracked'
        elif to_sha == from_sha and not modified_in_worktree:
            # something that is seemingly unmodified, based on the info
            # gathered so far
            if to_state['type'] == 'dataset':
                if against_commit or eval_submodule_state == 'commit':
                    # we compare against a recorded state, just based on
                    # the shas we can be confident, otherwise the state
                    # of a subdataset isn't fully known yet, because
                    # `modified_in_worktree` will only reflect changes
                    # in the commit of a subdataset without looking into
                    # it for uncommitted changes. Such tests are done
                    # later and based on further conditionals for
                    # performance reasons
                    state = 'clean'
            else:
                # no change in git record, and no change on disk
                # at this point we know that the reported object ids
                # for this file are identical in the to and from
                # records.  If to is None, we're comparing to the
                # working tree and a deleted file will still have an
                # identical id, so we need to check whether the file is
                # gone before declaring it clean. This working tree
                # check is irrelevant and wrong if to is a ref.
                state = 'clean' \
                    if against_commit or (f.exists() or f.is_symlink()) \
                    else 'deleted'
        else:
            # change in git record, or on disk
            # for subdatasets leave the 'modified' judgement to the caller
            # for supporting corner cases, such as adjusted branch
            # which require inspection of a subdataset
            # TODO we could have a new file that is already staged
            # but had subsequent modifications done to it that are
            # unstaged. Such file would presently show up as 'added'
            # ATM I think this is OK, but worth stating...
            state = ('modified'
                     if against_commit or to_state['type'] != 'dataset'
                     else None
                    ) if f.exists() or f.is_symlink() else 'deleted'
            # TODO record before and after state for diff-like use
            # cases

        if state in ('clean', 'added', 'modified', None):
            # assign present gitsha to any record
            # state==None can only happen for subdatasets that
            # already existed, so also assign a sha for them
            props['gitshasum'] = to_sha
            if 'bytesize' in to_state:
                # if we got this cheap, report it
                props['bytesize'] = to_state['bytesize']
            elif state == 'clean' and 'bytesize' in from_state:
                # no change, we can take this old size info
                props['bytesize'] = from_state['bytesize']
        if state in ('clean', 'modified', 'deleted', None):
            # assign previous gitsha to any record
            # state==None can only happen for subdatasets that
            # already existed, so also assign a sha for them
            props['prev_gitshasum'] = from_sha
        if state:
            # only report a state if we could determine any
            # outside code tests for existence of the property
            # and not (always) for the value
            props['state'] = state
        return props

    def _save_pre(self, paths, _status, **kwargs):
        # helper to get an actionable status report
        if paths is not None and not paths and not _status:
            return
        if _status is None:
            if 'untracked' not in kwargs:
                kwargs['untracked'] = 'normal'
            status = self.status(
                paths=paths,
                **{k: kwargs[k] for k in kwargs
                   if k in ('untracked', 'eval_submodule_state')})
        else:
            # we want to be able to add items down the line
            # make sure to detach from prev. owner
            status = _status.copy()
        return status

    def get_staged_paths(self):
        """Returns a list of any stage repository path(s)

        This is a rather fast call, as it will not depend on what is going on
        in the worktree.
        """
        try:
            return list(self.call_git_items_(
                ['diff', '--name-only', '--staged'],
                expect_stderr=True))
        except CommandError as e:
            lgr.debug(CapturedException(e))
            return []

    def _save_post(self, message, files, partial_commit, amend=False,
                   allow_empty=False):
        # helper to commit changes reported in status

        # TODO remove pathobj stringification when commit() can
        # handle it
        to_commit = [str(f.relative_to(self.pathobj))
                     for f in files] \
                    if partial_commit else None
        if not partial_commit or to_commit or allow_empty or \
                (amend and message):
            # we directly call GitRepo.commit() to avoid a whole slew
            # if direct-mode safeguards and workarounds in the AnnexRepo
            # implementation (which also run an additional dry-run commit
            GitRepo.commit(
                self,
                files=to_commit,
                msg=message,
                options=to_options(amend=amend, allow_empty=allow_empty),
                # do not raise on empty commit
                # it could be that the `add` in this save-cycle has already
                # brought back a 'modified' file into a clean state
                careless=True,
            )

    def save(self, message=None, paths=None, _status=None, **kwargs):
        """Save dataset content.

        Parameters
        ----------
        message : str or None
          A message to accompany the changeset in the log. If None,
          a default message is used.
        paths : list or None
          Any content with path matching any of the paths given in this
          list will be saved. Matching will be performed against the
          dataset status (GitRepo.status()), or a custom status provided
          via `_status`. If no paths are provided, ALL non-clean paths
          present in the repo status or `_status` will be saved.
        _status : dict or None
          If None, Repo.status() will be queried for the given `ds`. If
          a dict is given, its content will be used as a constraint.
          For example, to save only modified content, but no untracked
          content, set `paths` to None and provide a `_status` that has
          no entries for untracked content.
        **kwargs :
          Additional arguments that are passed to underlying Repo methods.
          Supported:

          - git : bool (passed to Repo.add()
          - eval_submodule_state : {'full', 'commit', 'no'}
            passed to Repo.status()
          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()
          - amend : bool (passed to GitRepo.commit)
        """
        return list(
            self.save_(
                message=message,
                paths=paths,
                _status=_status,
                **kwargs
            )
        )

    def save_(self, message=None, paths=None, _status=None, **kwargs):
        """Like `save()` but working as a generator."""
        from datalad.interface.results import get_status_dict

        status_state = _get_save_status_state(
            self._save_pre(paths, _status, **kwargs) or {}
        )
        amend = kwargs.get('amend', False)

        # TODO: check on those None's -- may be those are also "nothing to worry about"
        # and we could just return?
        if not any(status_state.values()) and not (message and amend):
            # all clean, nothing todo
            lgr.debug('Nothing to save in %r, exiting early', self)
            return

        # three things are to be done:
        # - remove (deleted if not already staged)
        # - add (modified/untracked)
        # - commit (with all paths that have been touched, to bypass
        #   potential pre-staged bits)

        staged_paths = self.get_staged_paths()
        need_partial_commit = bool(staged_paths)
        if need_partial_commit and hasattr(self, "call_annex"):
            # so we have some staged content. let's check which ones
            # are symlinks -- those could be annex key links that
            # are broken after a `git-mv` operation
            # https://github.com/datalad/datalad/issues/4967
            # call `git-annex pre-commit` on them to rectify this before
            # saving the wrong symlinks
            added = status_state['added']
            tofix = [
                sp for sp in staged_paths
                if added.get(self.pathobj / sp, {}).get("type") == "symlink"
            ]
            if tofix:
                self.call_annex(['pre-commit'], files=tofix)

        submodule_change = False

        if status_state['deleted']:
            vanished_subds = [
                str(f.relative_to(self.pathobj))
                for f, props in status_state['deleted'].items()
                if props.get('type') == 'dataset'
            ]
            if vanished_subds:
                # we submodule removal we use `git-rm`, because the clean-up
                # is more complex than just an index update -- make no
                # sense to have a duplicate implementation.
                # we do not yield here, but only altogether below -- we are just
                # processing gone components, should always be quick.
                self._call_git(['rm', '-q'], files=vanished_subds)
                submodule_change = True
            # remove anything from the index that was found to be gone
            self._call_git(
                ['update-index', '--remove'],
                files=[
                    str(f.relative_to(self.pathobj))
                    for f, props in status_state['deleted'].items()
                    # do not update the index, if there is already
                    # something staged for this path (e.g.,
                    # a directory was removed and a file staged
                    # in its place)
                    if not props.get('gitshasum')
                    # we already did the submodules
                    and props.get('type') != 'dataset'
                ]
            )
            # now yield all deletions
            for p, props in status_state['deleted'].items():
                yield get_status_dict(
                    action='delete',
                    refds=self.pathobj,
                    type=props.get('type'),
                    path=p,
                    status='ok',
                    logger=lgr)

        # TODO this additional query should not be, based on status as given
        # if anyhow possible, however, when paths are given, status may
        # not contain all required information. In case of path=None AND
        # _status=None, we should be able to avoid this, because
        # status should have the full info already
        # looks for contained repositories
        untracked_dirs = [
            f.relative_to(self.pathobj)
            for f, props in status_state['untracked'].items()
            if props.get('type', None) == 'directory']
        to_add_submodules = []
        if untracked_dirs:
            to_add_submodules = [
                sm for sm, sm_props in
                self.get_content_info(
                    untracked_dirs,
                    ref=None,
                    # request exhaustive list, so that everything that is
                    # still reported as a directory must be its own repository
                    untracked='all').items()
                if sm_props.get('type', None) == 'directory']
            to_add_submodules = _prune_deeper_repos(to_add_submodules)

        to_stage_submodules = {
            f: props
            for f, props in status_state['modified_or_untracked'].items()
            if props.get('type', None) == 'dataset'}
        if to_stage_submodules:
            lgr.debug(
                '%i submodule path(s) to stage in %r %s',
                len(to_stage_submodules), self,
                to_stage_submodules
                if len(to_stage_submodules) < 10 else '')
            to_add_submodules += list(to_stage_submodules)

        if to_add_submodules:
            for r in self._save_add_submodules(to_add_submodules):
                if r.get('status', None) == 'ok':
                    submodule_change = True
                yield r

        if submodule_change:
            # this will alter the config, reload
            self.config.reload()
            # need to include .gitmodules in what needs committing
            f = self.pathobj.joinpath('.gitmodules')
            status_state['modified_or_untracked'][f] = \
                status_state['modified'][f] = \
                dict(type='file', state='modified')
            # now stage .gitmodules
            self._call_git(['update-index', '--add'], files=['.gitmodules'])
            # and report on it
            yield get_status_dict(
                action='add',
                refds=self.pathobj,
                type='file',
                path=f,
                status='ok',
                logger=lgr)

        to_add = {
            # TODO remove pathobj stringification when add() can
            # handle it
            str(f.relative_to(self.pathobj)): props
            for f, props in status_state['modified_or_untracked'].items()
            if not (f in to_add_submodules or f in to_stage_submodules)}
        if to_add:
            compat_config = \
                self.config.obtain("datalad.save.windows-compat-warning")
            to_add, problems = self._check_for_win_compat(to_add, compat_config)
            lgr.debug(
                '%i path(s) to add to %s %s',
                len(to_add), self, to_add if len(to_add) < 10 else '')

            if to_add:
                yield from self._save_add(
                        to_add,
                        git_opts=None,
                        **{k: kwargs[k] for k in kwargs
                           if k in (('git',) if hasattr(self, 'uuid')
                                    else tuple())})
            if problems:
                from datalad.interface.results import get_status_dict
                msg = \
                    'Incompatible name for Windows systems; disable with ' \
                    'datalad.save.windows-compat-warning.',
                for path in problems:
                    yield get_status_dict(
                        action='save',
                        refds=self.pathobj,
                        type='file',
                        path=(self.pathobj / ut.PurePosixPath(path)),
                        status='impossible',
                        message=msg,
                        logger=lgr)


        # https://github.com/datalad/datalad/issues/6558
        # file could have become a directory. Unfortunately git
        # would then mistakenly refuse to commit if that old path is also
        # given to commit, so we better filter it out
        if status_state['deleted'] and status_state['added']:
            # check if any "deleted" is a directory now. Then for those
            # there should be some other path under that directory in 'added'
            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:
                # this could potentially be expensive if lots of files become
                # directories, but it is unlikely to happen often
                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish
                # path_is_subpath faster, also if comparing to "in f.parents"
                f_str = str(f)
                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):
                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post

        # Note, that allow_empty is always ok when we amend. Required when we
        # amend an empty commit while the amendment is empty, too (though
        # possibly different message). If an empty commit was okay before, it's
        # okay now.
        status_state.pop('modified_or_untracked')  # pop the hybrid state
        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,
                        allow_empty=amend)
        # TODO yield result for commit, prev helper checked hexsha pre
        # and post...

    def _check_for_win_compat(self, files, config):
        """Check file names for illegal characters or reserved names on Windows

        In the case that a non-Windows-compatible file is detected, warn users
        about potential interoperability issues.

        Parameters
        ----------
        files
          list of files to add
        config
          value of self.config.obtain("datalad.save.windows-compat-warning"),
          used to choose appropriate behavior. "none" performs no check,
          "warning" warns in case of incompatibilities, and "error" results in
          an error result in case of incompatibilities
        """
        # don't perform any check when the configuration is set to 'none'
        if config == 'none':
            return files, None

        from collections import defaultdict
        problems = defaultdict(list)
        for file in files:
            for part in Path(file).parts:
                # check every component of the path for incompatibilities
                if Path(part).stem.upper() in RESERVED_NAMES_WIN:
                    problems['Elements using a reserved filename:'].append(part)
                    problems['paths'].append(file)
                if re.search(ILLEGAL_CHARS_WIN, part):
                    problems['Elements with illegal characters:'].append(part)
                    problems['paths'].append(file)
                if part.endswith('.'):
                    problems['Elements ending with a dot:'].append(part)
                    problems['paths'].append(file)
                if part.endswith(' '):
                    problems['Elements ending with a space:'].append(part)
                    problems['paths'].append(file)
        if not problems:
            return files, None
        msg = \
            "Some elements of your dataset are not compatible with " \
            "Windows systems. Disable this check by changing " \
            "datalad.save.windows-compat-warning or consider renaming " \
            "the following elements: "
        for k, v in problems.items():
            # use the key as an explanation, and report filenames only once
            msg += f"\n{k} {[*{*v}]}" if k != 'paths' else ''
        if config == 'warning':
            lgr.warning(msg)
            return files, None

        elif config == 'error':
            # take the problematic files out of to_add
            for path in [*{*problems['paths']}]:
                files.pop(path)
            return files, [*{*problems['paths']}]

    def _save_add(self, files, git_opts=None):
        """Simple helper to add files in save()"""
        from datalad.interface.results import get_status_dict
        try:
            # without --verbose git 2.9.3  add does not return anything
            add_out = self._call_git(
                # Set annex.largefiles to prevent storing files in
                # annex with a v6+ annex repo.
                ['-c', 'annex.largefiles=nothing', 'add'] +
                ensure_list(git_opts) + ['--verbose'],
                files=list(files.keys()),
                pathspec_from_file=True,
            )
            # get all the entries
            for r in self._process_git_get_output(*add_out):
                yield get_status_dict(
                    action=r.get('command', 'add'),
                    refds=self.pathobj,
                    type='file',
                    path=(self.pathobj / ut.PurePosixPath(r['file']))
                    if 'file' in r else None,
                    status='ok' if r.get('success', None) else 'error',
                    key=r.get('key', None),
                    # while there is no git-annex underneath here, we
                    # tend to fake its behavior, so we can also support
                    # this type of messaging
                    message='\n'.join(r['error-messages'])
                    if 'error-messages' in r else None,
                    logger=lgr)
        except OSError as e:
            lgr.error("add: %s", e)
            raise

    def _save_add_submodules(self, paths):
        """Add new submodules, or updates records of existing ones

        This method does not use `git submodule add`, but aims to be more
        efficient by limiting the scope to mere in-place registration of
        multiple already present repositories.

        Parameters
        ----------
        paths : list(Path)

        Yields
        ------
        dict
          Result records
        """
        from datalad.interface.results import get_status_dict

        # first gather info from all datasets in read-only fashion, and then
        # update index, .gitmodules and .git/config at once
        info = []
        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843
        # We must not add already known submodules explicitly since "untracked"
        # can be assigned even for known ones (TODO: add issue, might have been closed)?
        # Not sure if operating on relative paths would provide any speed up so use full
        known_sub_paths = {s['path'] for s in self.get_submodules_()}
        for path in paths:
            already_known = path in known_sub_paths
            rpath = str(path.relative_to(self.pathobj).as_posix())
            subm = repo_from_path(path)
            # if there is a corresponding branch, we want to record it's state.
            # we rely on the corresponding branch being synced already.
            # `save` should do that each time it runs.
            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())
            if not subm_commit:
                yield get_status_dict(
                    action='add_submodule',
                    ds=self,
                    path=path,
                    status='error',
                    message=('cannot add subdataset %s with no commits', subm),
                    logger=lgr)
                continue
            # make an attempt to configure a submodule source URL based on the
            # discovered remote configuration
            remote, branch = subm.get_tracking_branch()
            url = subm.get_remote_url(remote) if remote else None
            if url is None:
                url = './{}'.format(rpath)
            subm_id = subm.config.get('datalad.dataset.id', None)
            info.append(
                dict(
                     # if we have additional information on this path, pass it on.
                     # if not, treat it as an untracked directory
                     paths[path] if isinstance(paths, dict)
                     else dict(type='directory', state='untracked'),
                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,
                     url=url, known=already_known))

        # bypass any convenience or safe-manipulator for speed reasons
        # use case: saving many new subdatasets in a single run
        with (self.pathobj / '.gitmodules').open('a') as gmf, \
             (self.pathobj / '.git' / 'config').open('a') as gcf:
            for i in info:
                # we update the subproject commit unconditionally
                self.call_git([
                    'update-index', '--add', '--replace', '--cacheinfo', '160000',
                    i['commit'], i['rpath']
                ])
                # only write the .gitmodules/.config changes when this is not yet
                # a subdataset and not yet already known
                # TODO: we could update the URL, and branch info at this point,
                # even for previously registered subdatasets
                if not i['known'] and (i['type'] != 'dataset' or (
                        i['type'] == 'dataset' and i['state'] == 'untracked')):
                    gmprops = dict(path=i['rpath'], url=i['url'])
                    if i['id']:
                        gmprops['datalad-id'] = i['id']
                    write_config_section(
                        gmf, 'submodule', i['rpath'], gmprops)
                    write_config_section(
                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))

                # This mirrors the result structure yielded for
                # to_stage_submodules below.
                yield get_status_dict(
                    action='add',
                    refds=self.pathobj,
                    type='dataset',
                    key=None,
                    path=i['path'],
                    status='ok',
                    logger=lgr)


def _get_save_status_state(status):
    """
    Returns
    -------
    dict
      By status category by file path, mapped to status properties.
    """
    # Sort status into status by state with explicit list of states
    # (excluding clean we do not care about) we expect to be present
    # and which we know of (unless None), and modified_or_untracked hybrid
    # since it is used below
    status_state = {
        k: {}
        for k in (None,  # not cared of explicitly here
                  'added',  # not cared of explicitly here
                  # 'clean'  # not even wanted since nothing to do about those
                  'deleted',
                  'modified',
                  'untracked',
                  'modified_or_untracked',  # hybrid group created here
                  )}
    for f, props in status.items():
        state = props.get('state', None)
        if state == 'clean':
            # we don't care about clean
            continue
        if state == 'modified' and props.get('gitshasum') \
                and props.get('gitshasum') == props.get('prev_gitshasum'):
            # reported as modified, but with identical shasums -> typechange
            # a subdataset maybe? do increasingly expensive tests for
            # speed reasons
            if props.get('type') != 'dataset' and f.is_dir() \
                    and GitRepo.is_valid_repo(f):
                # it was not a dataset, but now there is one.
                # we declare it untracked to engage the discovery tooling.
                state = 'untracked'
                props = dict(type='dataset', state='untracked')
        status_state[state][f] = props
        # The hybrid one to retain the same order as in original status
        if state in ('modified', 'untracked'):
            status_state['modified_or_untracked'][f] = props
    return status_state


# used in in the get command and GitRepo.add_submodule(), the
# latter is not used outside the tests
def _fixup_submodule_dotgit_setup(ds, relativepath):
    """Implementation of our current of .git in a subdataset

    Each subdataset/module has its own .git directory where a standalone
    repository would have it. No gitdir files, no symlinks.
    """
    # move .git to superrepo's .git/modules, remove .git, create
    # .git-file
    path = opj(ds.path, relativepath)
    subds_dotgit = opj(path, ".git")

    repo = GitRepo(path, create=False)
    if repo.dot_git.parent == repo.pathobj:
        # this is what we want
        return

    # first we want to remove any conflicting worktree setup
    # done by git to find the checkout at the mountpoint of the
    # submodule, if we keep that, any git command will fail
    # after we move .git
    # Ben: Shouldn't we re-setup a possible worktree afterwards?
    repo.config.unset('core.worktree', scope='local')
    # what we have here is some kind of reference, remove and
    # replace by the target
    os.remove(subds_dotgit)
    # make absolute
    src_dotgit = str(repo.dot_git)
    # move .git
    from os import (
        listdir,
        rename,
        rmdir,
    )
    ensure_dir(subds_dotgit)
    for dot_git_entry in listdir(src_dotgit):
        rename(opj(src_dotgit, dot_git_entry),
               opj(subds_dotgit, dot_git_entry))
    assert not listdir(src_dotgit)
    rmdir(src_dotgit)


# try retro-fitting GitRepo with deprecated functionality
# must be done last in this file
try:
    from datalad_deprecated.gitrepo import DeprecatedGitRepoMethods
    for symbol in dir(DeprecatedGitRepoMethods):
        if symbol.startswith('__'):
            # ignore Python internals
            continue
        if hasattr(GitRepo, symbol):
            lgr.debug(
                'Not retro-fitted GitRepo with deprecated %s, '
                'name-space conflict', symbol)
            # do not override existing symbols
            continue
        # assign deprecated symbol to GitRepo
        setattr(GitRepo, symbol, getattr(DeprecatedGitRepoMethods, symbol))
        lgr.debug('Retro-fitted GitRepo with deprecated %s', symbol)
except ImportError as e:
    ce = CapturedException(e)
    lgr.debug(
        'Not retro-fitting GitRepo with deprecated symbols, '
        'datalad-deprecated package not found')

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** GitRepo.merge Allows Merging Unrelated Histories Without Explicit Permission

**Description:**
When attempting to merge two independent repositories, the `GitRepo.merge` function proceeds without requiring explicit permission to allow unrelated histories. This behavior can lead to unintended merges, causing repository inconsistencies and potential data conflicts.

**Example Code:**
```python
from datalad import Dataset
from datalad.support.annexrepo import AnnexRepo

# Create two separate repositories
ds = Dataset('/tmp/repo1').create()
repo = AnnexRepo('/tmp/repo2')
file = '/tmp/repo2/file.dat'
with open(file, 'w') as f:
    f.write("data")
repo.add(file)
repo.commit()

# Attempt to add and merge the unrelated repository
ds.siblings(action='add', name='repo', url=repo.path)
result = ds.update(how='merge', on_failure='ignore')

# Expecting the merge to fail due to unrelated histories
```

**Expected Behavior:**
The merge operation should fail and return an error message indicating that merging unrelated histories is not allowed unless explicitly permitted. This prevents accidental merges that can complicate the repository's history.

**Actual Behavior:**
The merge operation completes successfully without any errors, even though the histories of the two repositories are unrelated. This can result in an inconsistent state of the repository and make it difficult to trace changes accurately.

[/ISSUE]
{
    "test_update_git_smoke": "PASSED",
    "test_update_volatile_subds": "PASSED",
    "test_reobtain_data": "PASSED",
    "test_multiway_merge": "PASSED",
    "test_unrelated_history_merge": "PASSED",
    "test_merge_conflict": "PASSED",
    "test_merge_conflict_in_subdataset_only": "PASSED",
    "test_merge_ff_only": "PASSED",
    "test_update_follow_parentds_lazy": "PASSED",
    "test_update_adjusted_incompatible_with_ff_only": "PASSED",
    "test_update_how_subds_different[parentds-reset]": "PASSED",
    "test_update_how_subds_different[sibling-checkout]": "PASSED",
    "test_update_reset_dirty": "PASSED",
    "test_process_how_args": "PASSED",
    "test_update_fetch_failure": "PASSED",
    "test_update_simple": "FAILED",
    "test_update_fetch_all": "FAILED",
    "test_newthings_coming_down": "FAILED",
    "test_merge_no_merge_target": "FAILED",
    "test_merge_follow_parentds_subdataset_other_branch": "FAILED",
    "test_merge_follow_parentds_subdataset_adjusted_warning": "FAILED",
    "test_merge_follow_parentds_subdataset_detached[False]": "FAILED",
    "test_update_unborn_master": "FAILED",
    "test_update_follow_parentds_lazy_other_branch": "FAILED"
}
diff --git a/datalad/support/gitrepo.py b/datalad/support/gitrepo.py
index 26fa9f076..bd3b04fe1 100644
--- a/datalad/support/gitrepo.py
+++ b/datalad/support/gitrepo.py
@@ -2246,7 +2246,8 @@ class GitRepo(CoreGitRepo):
             options = []
         if msg:
             options = options + ["-m", msg]
-        options += ['--allow-unrelated-histories']
+        if allow_unrelated:
+            options += ['--allow-unrelated-histories']
         self.call_git(
             ['merge'] + options + [name],
             **kwargs
 

{"datalad/support/gitrepo.py": "# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-\n# ex: set sts=4 ts=4 sw=4 et:\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n#\n#   See COPYING file distributed along with the datalad package for the\n#   copyright and license terms.\n#\n# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n\"\"\"Internal low-level interface to Git repositories\n\n\"\"\"\n\nimport logging\nimport os\nimport os.path as op\nimport posixpath\nimport re\nimport warnings\nfrom collections.abc import Mapping\nfrom functools import wraps\nfrom itertools import chain\nfrom os import linesep\nfrom os.path import (\n    commonprefix,\n    curdir,\n    dirname,\n    exists,\n    isabs,\n)\nfrom os.path import join as opj\nfrom os.path import (\n    pardir,\n    relpath,\n    sep,\n)\n\nimport datalad.utils as ut\nfrom datalad import ssh_manager\nfrom datalad.cmd import (\n    BatchedCommand,\n    GitWitlessRunner,\n    NoCapture,\n    StdOutErrCapture,\n    WitlessProtocol,\n)\nfrom datalad.config import (\n    parse_gitconfig_dump,\n    write_config_section,\n)\nfrom datalad.consts import (\n    ILLEGAL_CHARS_WIN,\n    RESERVED_NAMES_WIN,\n)\nfrom datalad.core.local.repo import repo_from_path\nfrom datalad.dataset.gitrepo import GitRepo as CoreGitRepo\nfrom datalad.dataset.gitrepo import (\n    _get_dot_git,\n    path_based_str_repr,\n)\nfrom datalad.log import log_progress\nfrom datalad.support.due import (\n    Doi,\n    due,\n)\nfrom datalad.utils import (\n    Path,\n    PurePosixPath,\n    ensure_dir,\n    ensure_list,\n    ensure_unicode,\n    generate_file_chunks,\n    getpwd,\n    is_interactive,\n    on_windows,\n    optional_args,\n    path_is_subpath,\n    posix_relpath,\n)\n\nfrom .exceptions import (\n    CapturedException,\n    CommandError,\n    FileNotInRepositoryError,\n    InvalidGitReferenceError,\n    InvalidGitRepositoryError,\n    NoSuchPathError,\n)\n# imports from same module:\nfrom .external_versions import external_versions\nfrom .network import (\n    RI,\n    PathRI,\n    is_ssh,\n)\nfrom .path import get_parent_paths, get_filtered_paths_\n\n# shortcuts\n_curdirsep = curdir + sep\n_pardirsep = pardir + sep\n\n\nlgr = logging.getLogger('datalad.gitrepo')\n\n\n# outside the repo base classes only used in ConfigManager\ndef to_options(split_single_char_options=True, **kwargs):\n    \"\"\"Transform keyword arguments into a list of cmdline options\n\n    Imported from GitPython.\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n\n    Parameters\n    ----------\n    split_single_char_options: bool\n\n    kwargs:\n\n    Returns\n    -------\n    list\n    \"\"\"\n    def dashify(string):\n        return string.replace('_', '-')\n\n    def transform_kwarg(name, value, split_single_char_options):\n        if len(name) == 1:\n            if value is True:\n                return [\"-%s\" % name]\n            elif value not in (False, None):\n                if split_single_char_options:\n                    return [\"-%s\" % name, \"%s\" % value]\n                else:\n                    return [\"-%s%s\" % (name, value)]\n        else:\n            if value is True:\n                return [\"--%s\" % dashify(name)]\n            elif value is not False and value is not None:\n                return [\"--%s=%s\" % (dashify(name), value)]\n        return []\n\n    args = []\n    kwargs = dict(sorted(kwargs.items(), key=lambda x: x[0]))\n    for k, v in kwargs.items():\n        if isinstance(v, (list, tuple)):\n            for value in v:\n                args += transform_kwarg(k, value, split_single_char_options)\n        else:\n            args += transform_kwarg(k, v, split_single_char_options)\n    return args\n\n\ndef _normalize_path(base_dir, path):\n    \"\"\"Helper to check paths passed to methods of this class.\n\n    Checks whether `path` is beneath `base_dir` and normalizes it.\n    Additionally paths are converted into relative paths with respect to\n    `base_dir`, considering PWD in case of relative paths. This\n    is intended to be used in repository classes, which means that\n    `base_dir` usually will be the repository's base directory.\n\n    Parameters\n    ----------\n    base_dir: str\n        directory to serve as base to normalized, relative paths\n    path: str\n        path to be normalized\n\n    Returns\n    -------\n    str:\n        path, that is a relative path with respect to `base_dir`\n    \"\"\"\n    if not path:\n        return path\n    pathobj = Path(path)\n\n    # do absolute() in addition to always get an absolute path\n    # even with non-existing base_dirs on windows\n    base_dir = str(Path(base_dir).resolve().absolute())  # realpath OK\n\n    # path = normpath(path)\n    # Note: disabled normpath, because it may break paths containing symlinks;\n    # But we don't want to realpath relative paths, in case cwd isn't the\n    # correct base.\n\n    if pathobj.is_absolute():\n        # path might already be a symlink pointing to annex etc,\n        # so realpath only its directory, to get \"inline\" with\n        # realpath(base_dir) above\n        path = str(pathobj.parent.resolve() / pathobj.name)  # realpath OK\n    # Executive decision was made to not do this kind of magic!\n    #\n    # elif commonprefix([realpath(getpwd()), base_dir]) == base_dir:\n    #     # If we are inside repository, rebuilt relative paths.\n    #     path = opj(realpath(getpwd()), path)\n    #\n    # BUT with relative curdir/pardir start it would assume relative to curdir\n    #\n    elif path.startswith(_curdirsep) or path.startswith(_pardirsep):\n        path = str(Path(getpwd()).resolve() / pathobj)  # realpath OK\n    else:\n        # We were called from outside the repo. Therefore relative paths\n        # are interpreted as being relative to self.path already.\n        return path\n\n    if commonprefix([path, base_dir]) != base_dir:\n        raise FileNotInRepositoryError(msg=\"Path outside repository: %s\"\n                                           % base_dir, filename=path)\n\n    return relpath(path, start=base_dir)\n\n\n@optional_args\ndef normalize_path(func):\n    \"\"\"Decorator to provide unified path conversion for a single file\n\n    Unlike normalize_paths, intended to be used for functions dealing with a\n    single filename at a time\n\n    Note\n    ----\n    This is intended to be used within the repository classes and therefore\n    returns a class method!\n\n    The decorated function is expected to take a path at\n    first positional argument (after 'self'). Additionally the class `func`\n    is a member of, is expected to have an attribute 'path'.\n    \"\"\"\n\n    @wraps(func)\n    def  _wrap_normalize_path(self, file_, *args, **kwargs):\n        file_new = _normalize_path(self.path, file_)\n        return func(self, file_new, *args, **kwargs)\n\n    return  _wrap_normalize_path\n\n\n@optional_args\ndef normalize_paths(func, match_return_type=True, map_filenames_back=False,\n                    serialize=False):\n    \"\"\"Decorator to provide unified path conversions.\n\n    Note\n    ----\n    This is intended to be used within the repository classes and therefore\n    returns a class method!\n\n    The decorated function is expected to take a path or a list of paths at\n    first positional argument (after 'self'). Additionally the class `func`\n    is a member of, is expected to have an attribute 'path'.\n\n    Accepts either a list of paths or a single path in a str. Passes a list\n    to decorated function either way, but would return based on the value of\n    match_return_type and possibly input argument.\n\n    If a call to the wrapped function includes normalize_path and it is False\n    no normalization happens for that function call (used for calls to wrapped\n    functions within wrapped functions, while possible CWD is within a\n    repository)\n\n    Parameters\n    ----------\n    match_return_type : bool, optional\n      If True, and a single string was passed in, it would return the first\n      element of the output (after verifying that it is a list of length 1).\n      It makes easier to work with single files input.\n    map_filenames_back : bool, optional\n      If True and returned value is a dictionary, it assumes to carry entries\n      one per file, and then filenames are mapped back to as provided from the\n      normalized (from the root of the repo) paths\n    serialize : bool, optional\n      Loop through files giving only a single one to the function one at a time.\n      This allows to simplify implementation and interface to annex commands\n      which do not take multiple args in the same call (e.g. checkpresentkey)\n    \"\"\"\n\n    @wraps(func)\n    def  _wrap_normalize_paths(self, files, *args, **kwargs):\n\n        normalize = _normalize_path if kwargs.pop('normalize_paths', True) \\\n            else lambda rpath, filepath: filepath\n\n        if files:\n            if isinstance(files, str) or not files:\n                files_new = [normalize(self.path, files)]\n                single_file = True\n            elif isinstance(files, list):\n                files_new = [normalize(self.path, path) for path in files]\n                single_file = False\n            else:\n                raise ValueError(\"_files_decorator: Don't know how to handle \"\n                                 \"instance of %s.\" % type(files))\n        else:\n            single_file = None\n            files_new = []\n\n        if map_filenames_back:\n            def remap_filenames(out):\n                \"\"\"Helper to map files back to non-normalized paths\"\"\"\n                if isinstance(out, dict):\n                    assert(len(out) == len(files_new))\n                    files_ = [files] if single_file else files\n                    mapped = out.__class__()\n                    for fin, fout in zip(files_, files_new):\n                        mapped[fin] = out[fout]\n                    return mapped\n                else:\n                    return out\n        else:\n            remap_filenames = lambda x: x\n\n        if serialize:  # and not single_file:\n            result = [\n                func(self, f, *args, **kwargs)\n                for f in files_new\n            ]\n        else:\n            result = func(self, files_new, *args, **kwargs)\n\n        if single_file is None:\n            # no files were provided, nothing we can do really\n            return result\n        elif (result is None) or not match_return_type or not single_file:\n            # If function doesn't return anything or no denormalization\n            # was requested or it was not a single file\n            return remap_filenames(result)\n        elif single_file:\n            if len(result) != 1:\n                # Magic doesn't apply\n                return remap_filenames(result)\n            elif isinstance(result, (list, tuple)):\n                return result[0]\n            elif isinstance(result, dict) and tuple(result)[0] == files_new[0]:\n                # assume that returned dictionary has files as keys.\n                return tuple(result.values())[0]\n            else:\n                # no magic can apply\n                return remap_filenames(result)\n        else:\n            return RuntimeError(\"should have not got here... check logic\")\n\n    return  _wrap_normalize_paths\n\n\nif \"2.24.0\" <= external_versions[\"cmd:git\"] < \"2.25.0\":\n    # An unintentional change in Git 2.24.0 led to `ls-files -o` traversing\n    # into untracked submodules when multiple pathspecs are given, returning\n    # repositories that are deeper than the first level. This helper filters\n    # these deeper levels out so that save_() doesn't fail trying to add them.\n    #\n    # This regression fixed with upstream's 072a231016 (2019-12-10).\n    def _prune_deeper_repos(repos):\n        firstlevel_repos = []\n        prev = None\n        for repo in sorted(repos):\n            if not (prev and str(repo).startswith(prev)):\n                prev = str(repo)\n                firstlevel_repos.append(repo)\n        return firstlevel_repos\nelse:\n    def _prune_deeper_repos(repos):\n        return repos\n\n\nclass GitProgress(WitlessProtocol):\n    \"\"\"Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n    # inform super-class to capture stderr\n    proc_err = True\n\n    _num_op_codes = 10\n    BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT, ENUMERATING = \\\n        [1 << x for x in range(_num_op_codes)]\n    STAGE_MASK = BEGIN | END\n    OP_MASK = ~STAGE_MASK\n\n    DONE_TOKEN = 'done.'\n    TOKEN_SEPARATOR = ', '\n\n    _known_ops = {\n        COUNTING: (\"Counting\", \"Objects\"),\n        ENUMERATING: (\"Enumerating\", \"Objects\"),\n        COMPRESSING: (\"Compressing\", \"Objects\"),\n        WRITING: (\"Writing\", \"Objects\"),\n        RECEIVING: (\"Receiving\", \"Objects\"),\n        RESOLVING: (\"Resolving\", \"Deltas\"),\n        FINDING_SOURCES: (\"Finding\", \"Sources\"),\n        CHECKING_OUT: (\"Check out\", \"Things\"),\n    }\n\n    __slots__ = ('_seen_ops', '_pbars', '_encoding')\n\n    re_op_absolute = re.compile(r\"(remote: )?([\\w\\s]+):\\s+()(\\d+)()(.*)\")\n    re_op_relative = re.compile(r\"(remote: )?([\\w\\s]+):\\s+(\\d+)% \\((\\d+)/(\\d+)\\)(.*)\")\n\n    def __init__(self, *args):\n        super().__init__(*args)\n        self._unprocessed = None\n\n    def connection_made(self, transport):\n        super().connection_made(transport)\n        self._seen_ops = []\n        self._pbars = set()\n\n    def process_exited(self):\n        # take down any progress bars that were not closed orderly\n        for pbar_id in self._pbars:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Finished',\n            )\n        super().process_exited()\n\n    def pipe_data_received(self, fd, byts):\n        # progress reports only come from stderr\n        if fd != 2:\n            # let the base class decide what to do with it\n            super().pipe_data_received(fd, byts)\n            return\n        for line in byts.splitlines(keepends=True):\n            # put any unprocessed content back in front\n            line = self._unprocessed + line if self._unprocessed else line\n            self._unprocessed = None\n            if not self._parse_progress_line(line):\n                # anything that doesn't look like a progress report\n                # is retained and returned\n                # in case of partial progress lines, this can lead to\n                # leakage of progress info into the output, but\n                # it is better to enable better (maybe more expensive)\n                # subsequent filtering than hiding lines with\n                # unknown, potentially important info\n                lgr.debug('Non-progress stderr: %s', line)\n                if line.endswith((b'\\r', b'\\n')):\n                    # complete non-progress line, pass on\n                    super().pipe_data_received(fd, line)\n                else:\n                    # an incomplete line, maybe the next batch completes\n                    # it to become a recognizable progress report\n                    self._unprocessed = line\n\n    def _parse_progress_line(self, line):\n        \"\"\"Process a single line\n\n        Parameters\n        ----------\n        line : bytes\n\n        Returns\n        -------\n        bool\n          Flag whether the line was recognized as a Git progress report.\n        \"\"\"\n        # handle\n        # Counting objects: 4, done.\n        # Compressing objects:  50% (1/2)\n        # Compressing objects: 100% (2/2)\n        # Compressing objects: 100% (2/2), done.\n        line = line.decode(self.encoding) if isinstance(line, bytes) else line\n        if line.startswith(('warning:', 'error:', 'fatal:')):\n            return False\n\n        # find escape characters and cut them away - regex will not work with\n        # them as they are non-ascii. As git might expect a tty, it will send them\n        last_valid_index = None\n        for i, c in enumerate(reversed(line)):\n            if ord(c) < 32:\n                # its a slice index\n                last_valid_index = -i - 1\n            # END character was non-ascii\n        # END for each character in line\n        if last_valid_index is not None:\n            line = line[:last_valid_index]\n        # END cut away invalid part\n        line = line.rstrip()\n\n        cur_count, max_count = None, None\n        match = self.re_op_relative.match(line)\n        if match is None:\n            match = self.re_op_absolute.match(line)\n\n        if not match:\n            return False\n        # END could not get match\n\n        op_code = 0\n        _remote, op_name, _percent, cur_count, max_count, message = match.groups()\n\n        # get operation id\n        if op_name == \"Counting objects\":\n            op_code |= self.COUNTING\n        elif op_name == \"Compressing objects\":\n            op_code |= self.COMPRESSING\n        elif op_name == \"Writing objects\":\n            op_code |= self.WRITING\n        elif op_name == 'Receiving objects':\n            op_code |= self.RECEIVING\n        elif op_name == 'Resolving deltas':\n            op_code |= self.RESOLVING\n        elif op_name == 'Finding sources':\n            op_code |= self.FINDING_SOURCES\n        elif op_name == 'Checking out files':\n            op_code |= self.CHECKING_OUT\n        elif op_name == 'Enumerating objects':\n            op_code |= self.ENUMERATING\n        else:\n            # Note: On windows it can happen that partial lines are sent\n            # Hence we get something like \"CompreReceiving objects\", which is\n            # a blend of \"Compressing objects\" and \"Receiving objects\".\n            # This can't really be prevented.\n            lgr.debug(\n                'Output line matched a progress report of an unknown type: %s',\n                line)\n            # TODO investigate if there is any chance that we might swallow\n            # important info -- until them do not flag this line\n            # as progress\n            return False\n        # END handle op code\n\n        pbar_id = 'gitprogress-{}-{}'.format(id(self), op_code)\n\n        op_props = self._known_ops[op_code]\n\n        # figure out stage\n        if op_code not in self._seen_ops:\n            self._seen_ops.append(op_code)\n            op_code |= self.BEGIN\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start {} {}'.format(\n                    op_props[0].lower(),\n                    op_props[1].lower(),\n                ),\n                label=op_props[0],\n                unit=' {}'.format(op_props[1]),\n                total=float(max_count) if max_count else None,\n            )\n            self._pbars.add(pbar_id)\n        # END begin opcode\n\n        if message is None:\n            message = ''\n        # END message handling\n\n        done_progress = False\n        message = message.strip()\n        if message.endswith(self.DONE_TOKEN):\n            op_code |= self.END\n            message = message[:-len(self.DONE_TOKEN)]\n            done_progress = True\n        # END end message handling\n        message = message.strip(self.TOKEN_SEPARATOR)\n\n        if cur_count and max_count:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                line,\n                update=float(cur_count),\n                noninteractive_level=logging.DEBUG,\n            )\n\n        if done_progress:\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Finished {} {}'.format(\n                    op_props[0].lower(),\n                    op_props[1].lower(),\n                ),\n                noninteractive_level=logging.DEBUG,\n            )\n            self._pbars.discard(pbar_id)\n        return True\n\n\nclass StdOutCaptureWithGitProgress(GitProgress):\n    proc_out = True\n\n\nclass FetchInfo(dict):\n    \"\"\"\n    dict that carries results of a fetch operation of a single head\n\n    Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n\n    NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \\\n        FAST_FORWARD, ERROR = [1 << x for x in range(8)]\n\n    _re_fetch_result = re.compile(r'^\\s*(.) (\\[?[\\w\\s\\.$@]+\\]?)\\s+(.+) [-> ]+ ([^\\s]+)(    \\(.*\\)?$)?')\n\n    _flag_map = {\n        '!': ERROR,\n        '+': FORCED_UPDATE,\n        '*': 0,\n        '=': HEAD_UPTODATE,\n        ' ': FAST_FORWARD,\n        '-': TAG_UPDATE,\n    }\n    _operation_map = {\n        NEW_TAG: 'new-tag',\n        NEW_HEAD: 'new-branch',\n        HEAD_UPTODATE: 'uptodate',\n        TAG_UPDATE: 'tag-update',\n        REJECTED: 'rejected',\n        FORCED_UPDATE: 'forced-update',\n        FAST_FORWARD: 'fast-forward',\n        ERROR: 'error',\n    }\n\n    @classmethod\n    def _from_line(cls, line):\n        \"\"\"Parse information from the given line as returned by git-fetch -v\n        and return a new FetchInfo object representing this information.\n        \"\"\"\n        match = cls._re_fetch_result.match(line)\n        if match is None:\n            raise ValueError(\"Failed to parse line: %r\" % line)\n\n        # parse lines\n        control_character, operation, local_remote_ref, remote_local_ref, note = \\\n            match.groups()\n\n        # parse flags from control_character\n        flags = 0\n        try:\n            flags |= cls._flag_map[control_character]\n        except KeyError:\n            raise ValueError(\n                \"Control character %r unknown as parsed from line %r\"\n                % (control_character, line))\n        # END control char exception handling\n\n        # parse operation string for more info - makes no sense for symbolic refs,\n        # but we parse it anyway\n        old_commit = None\n        if 'rejected' in operation:\n            flags |= cls.REJECTED\n        if 'new tag' in operation:\n            flags |= cls.NEW_TAG\n        if 'tag update' in operation:\n            flags |= cls.TAG_UPDATE\n        if 'new branch' in operation:\n            flags |= cls.NEW_HEAD\n        if '...' in operation or '..' in operation:\n            split_token = '...'\n            if control_character == ' ':\n                split_token = split_token[:-1]\n            old_commit = operation.split(split_token)[0]\n        # END handle refspec\n\n        return cls(\n            ref=remote_local_ref.strip(),\n            local_ref=local_remote_ref.strip(),\n            # convert flag int into a list of operation labels\n            operations=[\n                cls._operation_map[o]\n                for o in cls._operation_map.keys()\n                if flags & o\n            ],\n            note=note,\n            old_commit=old_commit,\n        )\n\n\nclass PushInfo(dict):\n    \"\"\"dict that carries results of a push operation of a single head\n\n    Reduced variant of GitPython's RemoteProgress class\n\n    Original copyright:\n        Copyright (C) 2008, 2009 Michael Trier and contributors\n    Original license:\n        BSD 3-Clause \"New\" or \"Revised\" License\n    \"\"\"\n    NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \\\n        FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)]\n\n    _flag_map = {'X': NO_MATCH,\n                 '-': DELETED,\n                 '*': 0,\n                 '+': FORCED_UPDATE,\n                 ' ': FAST_FORWARD,\n                 '=': UP_TO_DATE,\n                 '!': ERROR}\n\n    _operation_map = {\n        NEW_TAG: 'new-tag',\n        NEW_HEAD: 'new-branch',\n        NO_MATCH: 'no-match',\n        REJECTED: 'rejected',\n        REMOTE_REJECTED: 'remote-rejected',\n        REMOTE_FAILURE: 'remote-failure',\n        DELETED: 'deleted',\n        FORCED_UPDATE: 'forced-update',\n        FAST_FORWARD: 'fast-forward',\n        UP_TO_DATE: 'uptodate',\n        ERROR: 'error',\n    }\n\n    @classmethod\n    def _from_line(cls, line):\n        \"\"\"Create a new PushInfo instance as parsed from line which is expected to be like\n            refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes\"\"\"\n        control_character, from_to, summary = line.split('\\t', 3)\n        flags = 0\n\n        # control character handling\n        try:\n            flags |= cls._flag_map[control_character]\n        except KeyError:\n            raise ValueError(\"Control character %r unknown as parsed from line %r\" % (control_character, line))\n        # END handle control character\n\n        # from_to handling\n        from_ref_string, to_ref_string = from_to.split(':')\n\n        # commit handling, could be message or commit info\n        old_commit = None\n        if summary.startswith('['):\n            if \"[rejected]\" in summary:\n                flags |= cls.REJECTED\n            elif \"[remote rejected]\" in summary:\n                flags |= cls.REMOTE_REJECTED\n            elif \"[remote failure]\" in summary:\n                flags |= cls.REMOTE_FAILURE\n            elif \"[no match]\" in summary:\n                flags |= cls.ERROR\n            elif \"[new tag]\" in summary:\n                flags |= cls.NEW_TAG\n            elif \"[new branch]\" in summary:\n                flags |= cls.NEW_HEAD\n            # up-to-date encoded in control character\n        else:\n            # fast-forward or forced update - was encoded in control character,\n            # but we parse the old and new commit\n            split_token = \"...\"\n            if control_character == \" \":\n                split_token = \"..\"\n            old_sha, _new_sha = summary.split(' ')[0].split(split_token)\n            # have to use constructor here as the sha usually is abbreviated\n            old_commit = old_sha\n        # END message handling\n\n        return cls(\n            from_ref=from_ref_string.strip(),\n            to_ref=to_ref_string.strip(),\n            # convert flag int into a list of operation labels\n            operations=[\n                cls._operation_map[o]\n                for o in cls._operation_map.keys()\n                if flags & o\n            ],\n            note=summary.strip(),\n            old_commit=old_commit,\n        )\n\n\n@path_based_str_repr\nclass GitRepo(CoreGitRepo):\n    \"\"\"Representation of a git repository\n\n    \"\"\"\n    # We must check git config to have name and email set, but\n    # should do it once\n    _config_checked = False\n\n    GIT_MIN_VERSION = \"2.19.1\"\n    git_version = None\n\n    @classmethod\n    def _check_git_version(cls):\n        external_versions.check(\"cmd:git\", min_version=cls.GIT_MIN_VERSION)\n        cls.git_version = external_versions['cmd:git']\n\n    # This is the least common denominator to claim that a user\n    # used DataLad.\n    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262\n    @due.dcite(Doi(\"10.21105/joss.03262\"),\n               # override path since there is no need ATM for such details\n               path=\"datalad\",\n               description=\"DataLad - Data management and distribution platform\")\n    def __init__(self, path, runner=None, create=True,\n                 git_opts=None, repo=None, fake_dates=False,\n                 create_sanity_checks=True,\n                 **kwargs):\n        \"\"\"Creates representation of git repository at `path`.\n\n        Can also be used to create a git repository at `path`.\n\n        Parameters\n        ----------\n        path: str\n          path to the git repository; In case it's not an absolute path,\n          it's relative to PWD\n        create: bool, optional\n          if true, creates a git repository at `path` if there is none. Also\n          creates `path`, if it doesn't exist.\n          If set to false, an exception is raised in case `path` doesn't exist\n          or doesn't contain a git repository.\n        repo: git.Repo, optional\n          This argument is ignored.\n        create_sanity_checks: bool, optional\n          Whether to perform sanity checks during initialization (when\n          `create=True` and target path is not a valid repo already), such as\n          that new repository is not created in the directory where git already\n          tracks some files.\n        kwargs:\n          keyword arguments serving as additional options to the git-init\n          command. Therefore, it makes sense only if called with `create`.\n\n          Generally, this way of passing options to the git executable is\n          (or will be) used a lot in this class. It's a transformation of\n          python-style keyword arguments (or a `dict`) to command line arguments,\n          provided by GitPython.\n\n          A single character keyword will be prefixed by '-', multiple characters\n          by '--'. An underscore in the keyword becomes a dash. The value of the\n          keyword argument is used as the value for the corresponding command\n          line argument. Assigning a boolean creates a flag.\n\n          Examples:\n          no_commit=True => --no-commit\n          C='/my/path'   => -C /my/path\n\n        \"\"\"\n        # this will set up .pathobj and .dot_git\n        super().__init__(path)\n\n        if self.git_version is None:\n            self._check_git_version()\n\n        # BEGIN Repo validity test\n        # We want to fail early for tests, that would be performed a lot. In\n        # particular this is about GitRepo.is_valid_repo. We would use the\n        # latter to decide whether or not to call GitRepo() only for __init__ to\n        # then test the same things again. If we fail early we can save the\n        # additional test from outer scope.\n        self.path = path\n\n        # Note, that the following three path objects are used often and\n        # therefore are stored for performance. Path object creation comes with\n        # a cost. Most notably, this is used for validity checking of the\n        # repository.\n        _valid_repo = self.is_valid_git()\n\n        do_create = False\n        if create and not _valid_repo:\n            if repo is not None:\n                # `repo` passed with `create`, which doesn't make sense\n                raise TypeError(\"argument 'repo' must not be used with 'create'\")\n            do_create = True\n        else:\n            # Note: We used to call gitpy.Repo(path) here, which potentially\n            # raised NoSuchPathError or InvalidGitRepositoryError. This is\n            # used by callers of GitRepo.__init__() to detect whether we have a\n            # valid repo at `path`. Now, with switching to lazy loading property\n            # `repo`, we detect those cases without instantiating a\n            # gitpy.Repo().\n\n            if not exists(path):\n                raise NoSuchPathError(path)\n            if not _valid_repo:\n                raise InvalidGitRepositoryError(path)\n        # END Repo validity test\n\n        # So that we \"share\" control paths with git/git-annex\n        if ssh_manager:\n            ssh_manager.ensure_initialized()\n\n        # note: we may also want to distinguish between a path to the worktree\n        # and the actual repository\n\n        if git_opts is None:\n            git_opts = {}\n        if kwargs:\n            git_opts.update(kwargs)\n\n        self._cfg = None\n\n        if do_create:  # we figured it out earlier\n            from_cmdline = git_opts.pop('_from_cmdline_', [])\n            self.init(\n                sanity_checks=create_sanity_checks,\n                init_options=from_cmdline + to_options(**git_opts),\n            )\n\n        # with DryRunProtocol path might still not exist\n        if exists(self.path):\n            self.inode = os.stat(self.path).st_ino\n        else:\n            self.inode = None\n\n        if fake_dates:\n            self.configure_fake_dates()\n\n    @property\n    def bare(self):\n        \"\"\"Returns a bool indicating whether the repository is bare\n\n        Importantly, this is not reporting the configuration value\n        of 'core.bare', in order to be usable at a stage where a\n        Repo instance is not yet equipped with a ConfigManager.\n        Instead, it is testing whether the repository path and its\n        \"dot_git\" are identical. The value of 'core.bare' can be query\n        from the ConfigManager in a fully initialized instance.\n        \"\"\"\n        return self.pathobj == self.dot_git\n\n    @classmethod\n    def clone(cls, url, path, *args, clone_options=None, **kwargs):\n        \"\"\"Clone url into path\n\n        Provides workarounds for known issues (e.g.\n        https://github.com/datalad/datalad/issues/785)\n\n        Parameters\n        ----------\n        url : str\n        path : str\n        clone_options : dict or list\n          Arbitrary options that will be passed on to the underlying call to\n          `git-clone`. This may be a list of plain options or key-value pairs\n          that will be converted to a list of plain options with `to_options`.\n        expect_fail : bool\n          Whether expect that command might fail, so error should be logged then\n          at DEBUG level instead of ERROR\n        kwargs:\n          Passed to the Repo class constructor.\n        \"\"\"\n\n        if 'repo' in kwargs:\n            raise TypeError(\"argument 'repo' conflicts with cloning\")\n            # TODO: what about 'create'?\n\n        expect_fail = kwargs.pop('expect_fail', False)\n        # fail early on non-empty target:\n        from os import listdir\n        if exists(path) and listdir(path):\n            raise ValueError(\n                \"destination path '%s' already exists and is not an \"\n                \"empty directory.\" % path)\n        else:\n            # protect against cloning into existing and obviously dangling\n            # instance for that location\n            try:\n                del cls._unique_instances[path]\n            except KeyError:\n                # didn't exist - all fine\n                pass\n\n        # Massage URL\n        url_ri = RI(url) if not isinstance(url, RI) else url\n        if on_windows:\n            # When we're cloning from a local path on Windows, the URL at\n            # this point is platform-specific (e.g., \"..\\\\origin\"). According \n            # to Git clone's manpage, clone urls can't have backslashes.\n            # While Git does manage to clone a URL with backslashes, \n            # in the case of subdatasets cloned from relative paths it nevertheless\n            # messed up the resulting remote url, resulting in a mix of\n            # front and backslashes (see also gh-7180): \n            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\\\origin' \n            # Therefore, we're turning it to Posix now.\n            if isinstance(url_ri, PathRI):\n                url = Path(url).as_posix()\n                url_ri = PathRI(url)\n\n        else:\n            # if we are on windows, the local path of a URL\n            # would not end up being a proper local path and cloning\n            # would fail. Don't try to be smart and just pass the\n            # URL along unmodified\n\n            # try to get a local path from `url`:\n            try:\n                url = url_ri.localpath\n                url_ri = RI(url)\n            except ValueError:\n                pass\n\n        if is_ssh(url_ri):\n            ssh_manager.get_connection(url).open()\n        else:\n            if isinstance(url_ri, PathRI):\n                # expand user, because execution not going through a shell\n                # doesn't work well otherwise\n                new_url = os.path.expanduser(url)\n                if url != new_url:\n                    lgr.info(\"Expanded source path to %s from %s\", new_url, url)\n                    url = new_url\n\n        cmd = cls._git_cmd_prefix + ['clone', '--progress']\n        if clone_options:\n            if isinstance(clone_options, Mapping):\n                clone_options = to_options(**clone_options)\n            cmd.extend(clone_options)\n        cmd.extend([url, path])\n\n        fix_annex = None\n        ntries = 5  # 3 is not enough for robust workaround\n        for trial in range(ntries):\n            try:\n                lgr.debug(\"Git clone from %s to %s\", url, path)\n\n                res = GitWitlessRunner().run(cmd, protocol=GitProgress)\n                # fish out non-critical warnings by git-clone\n                # (empty repo clone, etc.), all other content is logged\n                # by the progress helper to 'debug'\n                for errline in res['stderr'].splitlines():\n                    if errline.startswith('warning:'):\n                        lgr.warning(errline[8:].strip())\n                lgr.debug(\"Git clone completed\")\n                break\n            except CommandError as e:\n                # log here but let caller decide what to do\n                ce = CapturedException(e)\n                str_e = str(e)\n                # see https://github.com/datalad/datalad/issues/785\n                if re.search(\"Request for .*aborted.*Unable to find\", str_e,\n                             re.DOTALL) \\\n                        and trial < ntries - 1:\n                    lgr.info(\n                        \"Hit a known issue with Git (see GH#785). Trial #%d, \"\n                        \"retrying\",\n                        trial)\n                    continue\n                    #(lgr.debug if expect_fail else lgr.error)(e_str)\n\n                if \"Clone succeeded, but checkout failed.\" in str_e:\n                    fix_annex = ce\n                    break\n\n                raise\n\n        # get ourselves a repository instance\n        gr = cls(path, *args, **kwargs)\n        if fix_annex:\n            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then\n            # would need to import our own subclass\n            if hasattr(gr, 'is_valid_annex'):\n                lgr.warning(\"Experienced issues while cloning. \"\n                            \"Trying to fix it, using git-annex-fsck.\")\n                if not gr.is_initialized():\n                    gr._init()\n                gr.fsck()\n            else:\n                lgr.warning(\"Experienced issues while cloning: %s\", fix_annex)\n        # ensure that Git doesn't mangle relative paths into obscure absolute\n        # paths: https://github.com/datalad/datalad/issues/3538\n        if isinstance(url_ri, PathRI):\n            url_path = Path(url)\n            if not url_path.is_absolute():\n                # get git-created path\n                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'\n                git_url = gr.config.get(remote_url)\n                if Path(git_url).is_absolute():\n                    # Git created an absolute path from a relative URL.\n                    git_url = op.relpath(git_url, gr.path)\n                path = Path(git_url)\n                # always in POSIX even on Windows\n                path = path.as_posix()\n                gr.config.set(remote_url, path,\n                              scope='local', force=True)\n        return gr\n\n    # Note: __del__ shouldn't be needed anymore as we switched to\n    #       `weakref.finalize`.\n    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods\n    #\n    #       Keeping both methods and this comment around as a reminder to not\n    #       use __del__, if we figure there's a need for cleanup in the future.\n    #\n    # def __del__(self):\n    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird\n    #     # stalls etc\n    #     self._cfg = None\n\n    def is_valid_git(self):\n        \"\"\"Returns whether the underlying repository appears to be still valid\n\n        Note, that this almost identical to the classmethod is_valid_repo().\n        However, if we are testing an existing instance, we can save Path object\n        creations. Since this testing is done a lot, this is relevant. Creation\n        of the Path objects in is_valid_repo() takes nearly half the time of the\n        entire function.\n\n        Also note, that this method is bound to an instance but still\n        class-dependent, meaning that a subclass cannot simply overwrite it.\n        This is particularly important for the call from within __init__(),\n        which in turn is called by the subclasses' __init__. Using an overwrite\n        would lead to the wrong thing being called.\n        \"\"\"\n        return self.is_valid()\n\n    @classmethod\n    def is_valid_repo(cls, path):\n        \"\"\"Returns if a given path points to a git repository\"\"\"\n        return cls.is_valid(path)\n\n    @staticmethod\n    def get_git_dir(repo):\n        \"\"\"figure out a repo's gitdir\n\n        '.git' might be a  directory, a symlink or a file\n\n        Note\n        ----\n        This method is likely to get deprecated, please use GitRepo.dot_git instead!\n        That one's not static, but it's cheaper and you should avoid\n        not having an instance of a repo you're working on anyway.\n        Note, that the property in opposition to this method returns an absolute path.\n\n\n        Parameters\n        ----------\n        repo: path or Repo instance\n          currently expected to be the repos base dir\n\n        Returns\n        -------\n        str\n          relative path to the repo's git dir; So, default would be \".git\"\n        \"\"\"\n        if isinstance(repo, GitRepo):\n            return str(repo.dot_git)\n        pathobj = Path(repo)\n        dot_git = _get_dot_git(pathobj, ok_missing=False)\n        try:\n            dot_git = dot_git.relative_to(pathobj)\n        except ValueError:\n            # is not a subpath, return as is\n            lgr.debug(\"Path %r is not subpath of %r\", dot_git, pathobj)\n        return str(dot_git)\n\n    @property\n    def config(self):\n        # just proxy the core repo APIs property for backward-compatibility\n        return self.cfg\n\n    def is_with_annex(self):\n        \"\"\"Report if GitRepo (assumed) has (remotes with) a git-annex branch\n        \"\"\"\n        return any(\n            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])\n        )\n\n    @classmethod\n    def get_toppath(cls, path, follow_up=True, git_options=None):\n        \"\"\"Return top-level of a repository given the path.\n\n        Parameters\n        -----------\n        follow_up : bool\n          If path has symlinks -- they get resolved by git.  If follow_up is\n          True, we will follow original path up until we hit the same resolved\n          path.  If no such path found, resolved one would be returned.\n        git_options: list of str\n          options to be passed to the git rev-parse call\n\n        Return None if no parent directory contains a git repository.\n        \"\"\"\n        cmd = ['git']\n        if git_options:\n            cmd.extend(git_options)\n        cmd += [\"rev-parse\", \"--show-toplevel\"]\n        try:\n            out = GitWitlessRunner(cwd=path).run(\n                cmd, protocol=StdOutErrCapture)\n            toppath = out['stdout'].rstrip('\\n\\r')\n        except CommandError:\n            return None\n        except OSError:\n            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,\n                                          git_options=git_options)\n\n        # normalize the report, because, e.g. on windows it can come out\n        # with improper directory separators (C:/Users/datalad)\n        toppath = str(Path(toppath))\n\n        if follow_up:\n            path_ = path\n            path_prev = \"\"\n            while path_ and path_ != path_prev:  # on top /.. = /\n                if str(Path(path_).resolve()) == toppath:\n                    toppath = path_\n                    break\n                path_prev = path_\n                path_ = dirname(path_)\n\n        return toppath\n\n    @normalize_paths\n    def add(self, files, git=True, git_options=None, update=False):\n        \"\"\"Adds file(s) to the repository.\n\n        Parameters\n        ----------\n        files: list\n          list of paths to add\n        git: bool\n          somewhat ugly construction to be compatible with AnnexRepo.add();\n          has to be always true.\n        update: bool\n          --update option for git-add. From git's manpage:\n           Update the index just where it already has an entry matching\n           <pathspec>. This removes as well as modifies index entries to match\n           the working tree, but adds no new files.\n\n           If no <pathspec> is given when --update option is used, all tracked\n           files in the entire working tree are updated (old versions of Git\n           used to limit the update to the current directory and its\n           subdirectories).\n\n        Returns\n        -------\n        list\n          Of status dicts.\n        \"\"\"\n        # under all circumstances call this class' add_ (otherwise\n        # AnnexRepo.add would go into a loop\n        return list(GitRepo.add_(self, files, git=git, git_options=git_options,\n                    update=update))\n\n    def add_(self, files, git=True, git_options=None, update=False):\n        \"\"\"Like `add`, but returns a generator\"\"\"\n        # TODO: git_options is used as options for the git-add here,\n        # instead of options to the git executable => rename for consistency\n\n        if not git:\n            lgr.warning(\n                'GitRepo.add() called with git=%s, this should not happen',\n                git)\n            git = True\n\n        # there is no other way then to collect all files into a list\n        # at this point, because we need to pass them at once to a single\n        # `git add` call\n        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]\n\n        if not (files or git_options or update):\n            # wondering why just a warning? in cmdline this is also not an error\n            lgr.warning(\"add was called with empty file list and no options.\")\n            return\n\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.gitaddtoannex to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.gitaddtoannex=false', 'add'] +\n                ensure_list(git_options) +\n                to_options(update=update) + ['--verbose'],\n                files=files,\n                pathspec_from_file=True,\n                read_only=False,\n            )\n            # get all the entries\n            for o in self._process_git_get_output(*add_out):\n                yield o\n            # Note: as opposed to git cmdline, force is True by default in\n            #       gitpython, which would lead to add things, that are\n            #       ignored or excluded otherwise\n            # 2. Note: There is an issue with globbing (like adding '.'),\n            #       which apparently doesn't care for 'force' and therefore\n            #       adds '.git/...'. May be it's expanded at the wrong\n            #       point in time or sth. like that.\n            # For now, use direct call to git add.\n            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,\n            #                      force=False)\n            # TODO: May be make use of 'fprogress'-option to indicate\n            # progress\n            # But then, we don't have it for git-annex add, anyway.\n            #\n            # TODO: Is write=True a reasonable way to do it?\n            # May be should not write until success of operation is\n            # confirmed?\n            # What's best in case of a list of files?\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n        # Make sure return value from GitRepo is consistent with AnnexRepo\n        # currently simulating similar return value, assuming success\n        # for all files:\n        # TODO: Make return values consistent across both *Repo classes!\n        return\n\n    @staticmethod\n    def _process_git_get_output(stdout, stderr=None):\n        \"\"\"Given both outputs (stderr is ignored atm) of git add - process it\n\n        Primarily to centralize handling in both indirect annex and direct\n        modes when ran through proxy\n        \"\"\"\n        return [{u'file': f, u'success': True}\n                for f in re.findall(\"'(.*)'[\\n$]\", ensure_unicode(stdout))]\n\n    @normalize_paths(match_return_type=False)\n    def remove(self, files, recursive=False, **kwargs):\n        \"\"\"Remove files.\n\n        Calls git-rm.\n\n        Parameters\n        ----------\n        files: list of str\n          list of paths to remove\n        recursive: False\n          whether to allow recursive removal from subdirectories\n        kwargs:\n          see `__init__`\n\n        Returns\n        -------\n        [str]\n          list of successfully removed files.\n        \"\"\"\n        if recursive:\n            kwargs['r'] = True\n\n        # the name is chosen badly, but the purpose is to make sure that\n        # any pending operations actually manifest themselves in the Git repo\n        # on disk (in case of an AnnexRepo, it could be pending batch\n        # processes that need closing)\n        self.precommit()\n\n        # output per removed file is expected to be \"rm 'PATH'\":\n        return [\n            line.strip()[4:-1]\n            for line in self.call_git_items_(\n                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)\n        ]\n\n    def precommit(self):\n        \"\"\"Perform pre-commit maintenance tasks\n        \"\"\"\n        # we used to clean up GitPython here\n        pass\n\n    @staticmethod\n    def _get_prefixed_commit_msg(msg):\n        DATALAD_PREFIX = \"[DATALAD]\"\n        return DATALAD_PREFIX if not msg else \"%s %s\" % (DATALAD_PREFIX, msg)\n\n    def configure_fake_dates(self):\n        \"\"\"Configure repository to use fake dates.\n        \"\"\"\n        lgr.debug(\"Enabling fake dates\")\n        self.config.set(\"datalad.fake-dates\", \"true\")\n\n    @property\n    def fake_dates_enabled(self):\n        \"\"\"Is the repository configured to use fake dates?\n        \"\"\"\n        # this turned into a private property of the CoreGitRepo\n        return self._fake_dates_enabled\n\n    def add_fake_dates(self, env):\n        # was renamed in CoreGitRepo\n        return self.add_fake_dates_to_env(env)\n\n    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,\n               files=None, date=None, index_file=None):\n        \"\"\"Commit changes to git.\n\n        Parameters\n        ----------\n        msg: str, optional\n          commit-message\n        options: list of str, optional\n          cmdline options for git-commit\n        _datalad_msg: bool, optional\n          To signal that commit is automated commit by datalad, so\n          it would carry the [DATALAD] prefix\n        careless: bool, optional\n          if False, raise when there's nothing actually committed;\n          if True, don't care\n        files: list of str, optional\n          path(s) to commit\n        date: str, optional\n          Date in one of the formats git understands\n        index_file: str, optional\n          An alternative index to use\n        \"\"\"\n\n        self.precommit()\n\n        # assemble commandline\n        cmd = ['commit']\n        options = ensure_list(options)\n\n        if date:\n            options += [\"--date\", date]\n\n        orig_msg = msg\n        if not msg:\n            if '--amend' in options:\n                if '--no-edit' not in options:\n                    # don't overwrite old commit message with our default\n                    # message by default, but re-use old one. In other words:\n                    # Make --no-edit the default:\n                    options += [\"--no-edit\"]\n            else:\n                msg = 'Recorded changes'\n                _datalad_msg = True\n\n        if _datalad_msg:\n            msg = self._get_prefixed_commit_msg(msg)\n\n        if msg:\n            options += [\"-m\", msg]\n        cmd.extend(options)\n\n        # set up env for commit\n        env = self.add_fake_dates(None) \\\n            if self.fake_dates_enabled else os.environ.copy()\n        if index_file:\n            env['GIT_INDEX_FILE'] = index_file\n\n        lgr.debug(\"Committing via direct call of git: %s\", cmd)\n\n        prev_sha = self.get_hexsha()\n\n        # Old code was doing clever --amend'ing of chunked series of commits manually\n        # here, but with pathspec_from_file it is no longer needed.\n        # store pre-commit state to be able to check if anything was committed\n        try:\n            # Note: call_git operates via joining call_git_items_ and that one wipes out\n            # .stdout from exception and collects/repopulates stderr only. Let's use\n            # _call_git which returns both outputs and collects/re-populates both stdout\n            # **and** stderr\n            _ = self._call_git(\n                    cmd,\n                    files=files,\n                    env=env,\n                    pathspec_from_file=True,\n            )\n        except CommandError as e:\n            # real errors first\n            if \"did not match any file(s) known to git\" in e.stderr:\n                raise FileNotInRepositoryError(\n                    cmd=e.cmd,\n                    msg=\"File(s) unknown to git\",\n                    code=e.code,\n                    filename=linesep.join([\n                        l for l in e.stderr.splitlines()\n                        if l.startswith(\"error: pathspec\")\n                    ])\n                )\n            # behavior choices now\n            elif not careless:\n                # not willing to compromise at all\n                raise\n            elif 'nothing to commit' in e.stdout:\n                lgr.debug(\"nothing to commit in %s. Ignored.\", self)\n            elif 'no changes added to commit' in e.stdout or \\\n                    'nothing added to commit' in e.stdout:\n                lgr.debug(\"no changes added to commit in %s. Ignored.\", self)\n            else:\n                raise\n        if orig_msg \\\n                or '--dry-run' in cmd \\\n                or prev_sha == self.get_hexsha() \\\n                or ('--amend' in cmd and '--no-edit' in cmd) \\\n                or (not is_interactive()) \\\n                or self.config.obtain('datalad.save.no-message') != 'interactive':\n            # we had a message given, or nothing was committed, or prev. commit\n            # was amended, or we are not connected to a terminal, or no\n            # interactive message input is desired:\n            # we can go home\n            return\n\n        # handle interactive message entry by running another `git-commit`\n        self._git_runner.run(\n            cmd + ['--amend', '--edit'],\n            protocol=NoCapture,\n            stdin=None,\n            env=env,\n        )\n\n    # TODO usage is only in the tests, consider making a test helper and\n    # remove from GitRepo API\n    def get_indexed_files(self):\n        \"\"\"Get a list of files in git's index\n\n        Returns\n        -------\n        list\n            list of paths rooting in git's base dir\n        \"\"\"\n\n        return [\n            str(r.relative_to(self.pathobj))\n            for r in self.get_content_info(\n                paths=None, ref=None, untracked='no')\n        ]\n\n    def format_commit(self, fmt, commitish=None):\n        \"\"\"Return `git show` output for `commitish`.\n\n        Parameters\n        ----------\n        fmt : str\n            A format string accepted by `git show`.\n        commitish: str, optional\n          Any commit identifier (defaults to \"HEAD\").\n\n        Returns\n        -------\n        str or, if there are not commits yet, None.\n        \"\"\"\n        # use git-log and not git-show due to faster performance with\n        # complex commits (e.g. octopus merges)\n        # https://github.com/datalad/datalad/issues/4801\n        cmd = ['log', '-1', '-z', '--format=' + fmt]\n        if commitish is not None:\n            cmd.append(commitish + \"^{commit}\")\n        # make sure Git takes our argument as a revision\n        cmd.append('--')\n        try:\n            stdout = self.call_git(\n                cmd, expect_stderr=True, expect_fail=True,\n                read_only=True)\n        except CommandError as e:\n            if 'bad revision' in e.stderr:\n                raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n            elif 'does not have any commits yet' in e.stderr:\n                return None\n            else:\n                raise e\n        # This trailing null is coming from the -z above, which avoids the\n        # newline that Git would append to the output. We could drop -z and\n        # strip the newline directly, but then we'd have to worry about\n        # compatibility across platforms.\n        return stdout.rsplit(\"\\0\", 1)[0]\n\n    def get_hexsha(self, commitish=None, short=False):\n        \"\"\"Return a hexsha for a given commitish.\n\n        Parameters\n        ----------\n        commitish : str, optional\n          Any identifier that refers to a commit (defaults to \"HEAD\").\n        short : bool, optional\n          Return the abbreviated form of the hexsha.\n\n        Returns\n        -------\n        str or, if no commitish was given and there are no commits yet, None.\n\n        Raises\n        ------\n        ValueError\n          If a commitish was given, but no corresponding commit could be\n          determined.\n        \"\"\"\n        # use --quiet because the 'Needed a single revision' error message\n        # that is the result of running this in a repo with no commits\n        # isn't useful to report\n        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(\n            commitish if commitish else 'HEAD')\n        ]\n        if short:\n            cmd.append('--short')\n        try:\n            return self.call_git_oneline(cmd, read_only=True)\n        except CommandError as e:\n            if commitish is None:\n                return None\n            raise ValueError(\"Unknown commit identifier: %s\" % commitish)\n\n    @normalize_paths(match_return_type=False)\n    def get_last_commit_hexsha(self, files):\n        \"\"\"Return the hash of the last commit the modified any of the given\n        paths\"\"\"\n        try:\n            commit = self.call_git(\n                ['rev-list', '-n1', 'HEAD'],\n                files=files,\n                expect_fail=True,\n                read_only=True,\n            )\n            commit = commit.strip()\n            return commit if commit else None\n        except CommandError:\n            if self.get_hexsha() is None:\n                # unborn branch, don't freak out\n                return None\n            raise\n\n    def get_revisions(self, revrange=None, fmt=\"%H\", options=None):\n        \"\"\"Return list of revisions in `revrange`.\n\n        Parameters\n        ----------\n        revrange : str or list of str or None, optional\n            Revisions or revision ranges to walk. If None, revision defaults to\n            HEAD unless a revision-modifying option like `--all` or\n            `--branches` is included in `options`.\n        fmt : string, optional\n            Format accepted by `--format` option of `git log`. This should not\n            contain new lines because the output is split on new lines.\n        options : list of str, optional\n            Options to pass to `git log`.  This should not include `--format`.\n\n        Returns\n        -------\n        List of revisions (str), formatted according to `fmt`.\n        \"\"\"\n        if revrange is None:\n            revrange = []\n        elif isinstance(revrange, str):\n            revrange = [revrange]\n\n        cmd = [\"log\", \"--format={}\".format(fmt)]\n        cmd.extend((options or []) + revrange + [\"--\"])\n        try:\n            stdout = self.call_git(cmd, expect_fail=True, read_only=True)\n        except CommandError as e:\n            if \"does not have any commits\" in e.stderr:\n                return []\n            raise\n        return stdout.splitlines()\n\n    def commit_exists(self, commitish):\n        \"\"\"Does `commitish` exist in the repo?\n\n        Parameters\n        ----------\n        commitish : str\n            A commit or an object that can be dereferenced to one.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        # Note: The peeling operator \"^{commit}\" is required so that rev-parse\n        # doesn't succeed if passed a full hexsha that is valid but doesn't\n        # exist.\n        return self.call_git_success(\n            [\"rev-parse\", \"--verify\", commitish + \"^{commit}\"],\n            read_only=True,\n        )\n\n    def get_merge_base(self, commitishes):\n        \"\"\"Get a merge base hexsha\n\n        Parameters\n        ----------\n        commitishes: str or list of str\n          List of commitishes (branches, hexshas, etc) to determine the merge\n          base of. If a single value provided, returns merge_base with the\n          current branch.\n\n        Returns\n        -------\n        str or None\n          If no merge-base for given commits, or specified treeish doesn't\n          exist, None returned\n        \"\"\"\n        if isinstance(commitishes, str):\n            commitishes = [commitishes]\n        if not commitishes:\n            raise ValueError(\"Provide at least a single value\")\n        elif len(commitishes) == 1:\n            commitishes = commitishes + [self.get_active_branch()]\n\n        try:\n            base = self.call_git_oneline(['merge-base'] + commitishes,\n                                         read_only=True)\n        except CommandError as exc:\n            if exc.code == 1 and not (exc.stdout or exc.stderr):\n                # No merge base was found (unrelated commits).\n                return None\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                return None\n            raise\n\n        return base\n\n    def is_ancestor(self, reva, revb):\n        \"\"\"Is `reva` an ancestor of `revb`?\n\n        Parameters\n        ----------\n        reva, revb : str\n            Revisions.\n\n        Returns\n        -------\n        bool\n        \"\"\"\n        return self.call_git_success(\n            [\"merge-base\", \"--is-ancestor\", reva, revb],\n            read_only=True)\n\n    def get_commit_date(self, branch=None, date='authored'):\n        \"\"\"Get the date stamp of the last commit (in a branch or head otherwise)\n\n        Parameters\n        ----------\n        date: {'authored', 'committed'}\n          Which date to return.  \"authored\" will be the date shown by \"git show\"\n          and the one possibly specified via --date to `git commit`\n\n        Returns\n        -------\n        int or None\n          None if no commit\n        \"\"\"\n        if date == 'committed':\n            format = '%ct'\n        elif date == 'authored':\n            format = '%at'\n        else:\n            raise ValueError('unknow date type: {}'.format(date))\n        d = self.format_commit(format, commitish=branch)\n        return int(d) if d else None\n\n    def get_active_branch(self):\n        \"\"\"Get the name of the active branch\n\n        Returns\n        -------\n        str or None\n          Returns None if there is no active branch, i.e. detached HEAD,\n          and the branch name otherwise.\n        \"\"\"\n        try:\n            out = self.call_git([\"symbolic-ref\", \"HEAD\"], expect_fail=True,\n                                read_only=True)\n        except CommandError as e:\n            if 'HEAD is not a symbolic ref' in e.stderr:\n                lgr.debug(\"detached HEAD in %s\", self)\n                return None\n            else:\n                raise e\n        return out.strip()[11:]  # strip refs/heads/\n\n    def get_corresponding_branch(self, branch=None):\n        \"\"\"Always returns None, a plain GitRepo has no managed branches\"\"\"\n        return None\n\n    def get_branches(self):\n        \"\"\"Get all branches of the repo.\n\n        Returns\n        -------\n        [str]\n            Names of all branches of this repository.\n        \"\"\"\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')\n        ]\n\n    def get_remote_branches(self):\n        \"\"\"Get all branches of all remotes of the repo.\n\n        Returns\n        -----------\n        [str]\n            Names of all remote branches.\n        \"\"\"\n        # TODO: Reconsider melting with get_branches()\n\n        # TODO: treat entries like this: origin/HEAD -> origin/master'\n        # currently this is done in collection\n\n        return [\n            b['refname:strip=2']\n            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')\n        ]\n\n    def get_remotes(self, with_urls_only=False):\n        \"\"\"Get known remotes of the repository\n\n        Parameters\n        ----------\n        with_urls_only : bool, optional\n          return only remotes which have urls\n\n        Returns\n        -------\n        remotes : list of str\n          List of names of the remotes\n        \"\"\"\n        from datalad.utils import unique\n\n        self.config.reload()\n        remotes = unique([x[7:] for x in self.config.sections()\n                          if x.startswith(\"remote.\")])\n\n        if with_urls_only:\n            remotes = [\n                r for r in remotes\n                if self.config.get('remote.%s.url' % r)\n            ]\n        return remotes\n\n    # TODO this is practically unused outside the tests, consider turning\n    # into a test helper and trim from the API\n    def get_files(self, branch=None):\n        \"\"\"Get a list of files in git.\n\n        Lists the files in the (remote) branch.\n\n        Parameters\n        ----------\n        branch: str\n          Name of the branch to query. Default: active branch.\n\n        Returns\n        -------\n        [str]\n          list of files.\n        \"\"\"\n        return [\n            str(p.relative_to(self.pathobj))\n            for p in self.get_content_info(\n                paths=None, ref=branch, untracked='no')\n            ]\n\n    def add_remote(self, name, url, options=None):\n        \"\"\"Register remote pointing to a url\n        \"\"\"\n        cmd = ['remote', 'add']\n        if options:\n            cmd += options\n        cmd += [name, url]\n\n        # for historical reasons this method returns stdout and\n        # stderr, keeping that for now\n        result = self._call_git(cmd)\n        self.config.reload()\n        return result\n\n    def remove_remote(self, name):\n        \"\"\"Remove existing remote\n        \"\"\"\n\n        # TODO: testing and error handling!\n        from .exceptions import RemoteNotAvailableError\n        try:\n            self.call_git(['remote', 'remove', name])\n        except CommandError as e:\n            if 'No such remote' in e.stderr:\n                raise RemoteNotAvailableError(name,\n                                              cmd=\"git remote remove\",\n                                              msg=\"No such remote\",\n                                              stdout=e.stdout,\n                                              stderr=e.stderr)\n            else:\n                raise e\n\n        # config.reload necessary, because the associated remote config\n        # will vanish\n        self.config.reload()\n        return\n\n    def _maybe_open_ssh_connection(self, remote, prefer_push=True):\n        \"\"\"Open connection if `remote` has an SSH URL.\n\n        Doing so enables SSH caching, preventing datalad-sshrun subprocesses\n        from opening (and then closing) their own.\n\n        Parameters\n        ----------\n        remote : str\n        prefer_push : bool, optional\n            Use `remote.<remote>.pushurl` if there is one, falling back to\n            `remote.<remote>.url`.\n        \"\"\"\n        if remote:\n            url = None\n            if prefer_push:\n                url = self.get_remote_url(remote, push=True)\n            url = url or self.get_remote_url(remote)\n            if url and is_ssh(url):\n                ssh_manager.get_connection(url).open()\n\n    def update_remote(self, name=None, verbose=False):\n        \"\"\"\n        \"\"\"\n        options = [\"-v\"] if verbose else []\n        self._maybe_open_ssh_connection(name)\n        name = [name] if name else []\n        self.call_git(\n            ['remote'] + name + ['update'] + options,\n            expect_stderr=True\n        )\n\n    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,\n              **kwargs):\n        \"\"\"Fetches changes from a remote (or all remotes).\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to fetch from. If no remote is given and\n          `all_` is not set, the tracking branch is fetched.\n        refspec : str or list, optional\n          refspec(s) to fetch.\n        all_ : bool, optional\n          fetch all remotes (and all of their branches).\n          Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-fetch.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-fetch.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        return list(\n            self.fetch_(\n                remote=remote,\n                refspec=refspec,\n                all_=all_,\n                git_options=git_options,\n            )\n        )\n\n    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `fetch`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],\n            action='fetch',\n            urlvars=('remote.{}.url', 'remote.{}.url'),\n            protocol=GitProgress,\n            info_cls=FetchInfo,\n            info_from='stderr',\n            add_remote=False,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def push(self, remote=None, refspec=None, all_remotes=False,\n             all_=False, git_options=None, **kwargs):\n        \"\"\"Push changes to a remote (or all remotes).\n\n        If remote and refspec are specified, and remote has\n        `remote.{remote}.datalad-push-default-first` configuration variable\n        set (e.g. by `create-sibling-github`), we will first push the first\n        refspec separately to possibly ensure that the first refspec is chosen\n        by remote as the \"default branch\".\n        See https://github.com/datalad/datalad/issues/4997\n        Upon successful push if this variable was set in the local git config,\n        we unset it, so subsequent pushes would proceed normally.\n\n        Parameters\n        ----------\n        remote : str, optional\n          name of the remote to push to. If no remote is given and\n          `all_` is not set, the tracking branch is pushed.\n        refspec : str or list, optional\n          refspec(s) to push.\n        all_ : bool, optional\n          push to all remotes. Fails if `remote` was given.\n        git_options : list, optional\n          Additional command line options for git-push.\n        kwargs :\n          Deprecated. GitPython-style keyword argument for git-push.\n          Will be appended to any git_options.\n        \"\"\"\n        git_options = ensure_list(git_options)\n        if kwargs:\n            git_options.extend(to_options(**kwargs))\n        if all_remotes:\n            # be nice to the elderly\n            all_ = True\n\n        push_refspecs = [refspec]\n        cfg = self.config  # shortcut\n        cfg_push_var = \"remote.{}.datalad-push-default-first\".format(remote)\n        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):\n            refspec = ensure_list(refspec)\n            lgr.debug(\"As indicated by %s pushing first refspec %s separately first\",\n                      cfg_push_var, refspec[0])\n            push_refspecs = [[refspec[0]], refspec[1:]]\n\n        push_res = []\n        for refspecs in push_refspecs:\n            push_res.extend(\n                self.push_(\n                    remote=remote,\n                    refspec=refspecs,\n                    all_=all_,\n                    git_options=git_options,\n                )\n            )\n        # note: above push_ should raise exception if errors out\n        if '--dry-run' not in git_options \\\n            and cfg.get_from_source('local', cfg_push_var) is not None:\n            lgr.debug(\"Removing %s variable from local git config after successful push\", cfg_push_var)\n            cfg.unset(cfg_push_var, 'local')\n        return push_res\n\n    def push_(self, remote=None, refspec=None, all_=False, git_options=None):\n        \"\"\"Like `push`, but returns a generator\"\"\"\n        yield from self._fetch_push_helper(\n            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],\n            action='push',\n            urlvars=('remote.{}.pushurl', 'remote.{}.url'),\n            protocol=StdOutCaptureWithGitProgress,\n            info_cls=PushInfo,\n            info_from='stdout',\n            add_remote=True,\n            remote=remote,\n            refspec=refspec,\n            all_=all_,\n            git_options=git_options)\n\n    def _fetch_push_helper(\n            self,\n            base_cmd,     # arg list\n            action,       # label fetch|push\n            urlvars,      # variables to query for URLs\n            protocol,     # processor for output\n            info_cls,     # Push|FetchInfo\n            info_from,    # stdout, stderr\n            add_remote,   # whether to add a 'remote' field to the info dict\n            remote=None, refspec=None, all_=False, git_options=None):\n\n        git_options = ensure_list(git_options)\n\n        cmd = base_cmd + git_options\n\n        if remote is None:\n            if refspec:\n                # conflicts with using tracking branch or push all remotes\n                # For now: Just fail.\n                # TODO: May be check whether it fits to tracking branch\n                raise ValueError(\n                    \"refspec specified without a remote. ({})\".format(refspec))\n            if all_:\n                remotes_to_process = self.get_remotes(with_urls_only=True)\n            else:\n                # No explicit remote to fetch.\n                # => get tracking branch:\n                tb_remote, refspec = self.get_tracking_branch()\n                if tb_remote is not None:\n                    remotes_to_process = [tb_remote]\n                else:\n                    # No remote, no tracking branch\n                    # => fail\n                    raise ValueError(\n                        \"Neither a remote is specified to {} \"\n                        \"from nor a tracking branch is set up.\".format(action))\n        else:\n            if all_:\n                raise ValueError(\n                    \"Option 'all_' conflicts with specified remote \"\n                    \"'{}'.\".format(remote))\n            remotes_to_process = [remote]\n\n        if refspec:\n            # prep for appending to cmd\n            refspec = ensure_list(refspec)\n\n        # no need for progress report, when there is just one remote\n        log_remote_progress = len(remotes_to_process) > 1\n        if log_remote_progress:\n            pbar_id = '{}remotes-{}'.format(action, id(self))\n            log_progress(\n                lgr.info,\n                pbar_id,\n                'Start %sing remotes for %s', action, self,\n                total=len(remotes_to_process),\n                label=action.capitalize(),\n                unit=' Remotes',\n            )\n        try:\n            for remote in remotes_to_process:\n                r_cmd = cmd + [remote]\n                if refspec:\n                    r_cmd += refspec\n\n                if log_remote_progress:\n                    log_progress(\n                        lgr.info,\n                        pbar_id,\n                        '{}ing remote %s'.format(action.capitalize()),\n                        remote,\n                        update=1,\n                        increment=True,\n                    )\n                # best effort to enable SSH connection caching\n                url = self.config.get(\n                    # make two attempts to get a URL\n                    urlvars[0].format(remote),\n                    self.config.get(\n                        urlvars[1].format(remote),\n                        None)\n                )\n                if url and is_ssh(url):\n                    ssh_manager.get_connection(url).open()\n                try:\n                    out = self._git_runner.run(\n                        r_cmd,\n                        protocol=protocol,\n                    )\n                    output = out[info_from] or ''\n                except CommandError as e:\n                    output = None\n                    # intercept some errors that we express as an error report\n                    # in the info dicts\n                    if re.match(\n                            '.*^error: failed to (push|fetch) some refs',\n                            e.stderr,\n                            re.DOTALL | re.MULTILINE):\n                        output = getattr(e, info_from)\n                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()\n                                          if l.startswith('hint: ')])\n                        if output is None:\n                            output = ''\n                    if not output:\n                        raise\n\n                for line in output.splitlines():\n                    try:\n                        # push info doesn't identify a remote, add it here\n                        pi = info_cls._from_line(line)\n                        if add_remote:\n                            pi['remote'] = remote\n                        # There were errors, but Git provided hints\n                        if 'error' in pi['operations']:\n                            pi['hints'] = hints or None\n                        yield pi\n                    except Exception:\n                        # it is not progress and no push info\n                        # don't hide it completely\n                        lgr.debug('git-%s reported: %s', action, line)\n        finally:\n            if log_remote_progress:\n                log_progress(\n                    lgr.info,\n                    pbar_id,\n                    'Finished %sing remotes for %s', action, self,\n                )\n\n    def get_remote_url(self, name, push=False):\n        \"\"\"Get the url of a remote.\n\n        Reads the configuration of remote `name` and returns its url or None,\n        if there is no url configured.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        push: bool\n          if True, get the pushurl instead of the fetch url.\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        return self.config.get(var, None)\n\n    def set_remote_url(self, name, url, push=False):\n        \"\"\"Set the URL a remote is pointing to\n\n        Sets the URL of the remote `name`. Requires the remote to already exist.\n\n        Parameters\n        ----------\n        name: str\n          name of the remote\n        url: str\n        push: bool\n          if True, set the push URL, otherwise the fetch URL\n        \"\"\"\n\n        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')\n        self.config.set(var, url, scope='local', reload=True)\n\n    def get_branch_commits_(self, branch=None, limit=None, stop=None):\n        \"\"\"Return commit hexshas for a branch\n\n        Parameters\n        ----------\n        branch: str, optional\n          If not provided, assumes current branch\n        limit: None | 'left-only', optional\n          Limit which commits to report.  If None -- all commits (merged or not),\n          if 'left-only' -- only the commits from the left side of the tree upon\n          merges\n        stop: str, optional\n          hexsha of the commit at which stop reporting (matched one is not\n          reported either)\n\n        Yields\n        ------\n        str\n        \"\"\"\n        cmd = ['rev-list']\n        if limit == 'left-only':\n            cmd.append('--left-only')\n        if not branch:\n            branch = self.get_active_branch()\n        cmd.append(branch)\n        # and trailing -- marker to make sure that Git never confused the branch\n        # with a potentially existing directory of the same name\n        cmd.append('--')\n        for r in self.call_git_items_(cmd):\n            if stop and stop == r:\n                return\n            yield r\n\n    def checkout(self, name, options=None):\n        \"\"\"\n        \"\"\"\n        # TODO: May be check for the need of -b options herein?\n        cmd = ['checkout']\n        if options:\n            cmd += options\n        cmd += [str(name)]\n\n        self.call_git(cmd, expect_stderr=True)\n        # checkout can change committed config, or create branch config\n        self.config.reload()\n\n    # TODO: Before implementing annex merge, find usages and check for a needed\n    # change to call super().merge\n    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):\n        if options is None:\n            options = []\n        if msg:\n            options = options + [\"-m\", msg]\n        options += ['--allow-unrelated-histories']\n        self.call_git(\n            ['merge'] + options + [name],\n            **kwargs\n        )\n\n    def remove_branch(self, branch):\n        self.call_git(['branch', '-D', branch])\n\n    def cherry_pick(self, commit):\n        \"\"\"Cherry pick `commit` to the current branch.\n\n        Parameters\n        ----------\n        commit : str\n            A single commit.\n        \"\"\"\n        self.call_git([\"cherry-pick\", commit])\n\n    @property\n    def dirty(self):\n        \"\"\"Is the repository dirty?\n\n        Note: This provides a quick answer when you simply want to know if\n        there are any untracked changes or modifications in this repository or\n        its submodules. For finer-grained control and more detailed reporting,\n        use status() instead.\n        \"\"\"\n        stdout = self.call_git(\n            [\"status\", \"--porcelain\",\n             # Ensure the result isn't influenced by status.showUntrackedFiles.\n             \"--untracked-files=normal\",\n             # Ensure the result isn't influenced by diff.ignoreSubmodules.\n             \"--ignore-submodules=none\"])\n        if bool(stdout.strip()):\n            # The quick `git status`-based check can give a different answer\n            # than `datalad status` for submodules on an adjusted branch.\n            st = self.diffstatus(fr=\"HEAD\" if self.get_hexsha() else None,\n                                 to=None, untracked=\"normal\")\n            return any(r.get(\"state\") != \"clean\" for r in st.values())\n        return False\n\n    @property\n    def untracked_files(self):\n        \"\"\"Legacy interface, do not use! Use the status() method instead.\n\n        Despite its name, it also reports on untracked datasets, and\n        yields their names with trailing path separators.\n        \"\"\"\n        return [\n            '{}{}'.format(\n                str(p.relative_to(self.pathobj)),\n                os.sep if props['type'] != 'file' else ''\n            )\n            for p, props in self.status(\n                    untracked='all', eval_submodule_state='no').items()\n            if props.get('state', None) == 'untracked'\n        ]\n\n    def gc(self, allow_background=False, auto=False):\n        \"\"\"Perform house keeping (garbage collection, repacking)\"\"\"\n        cmd_options = []\n        if not allow_background:\n            cmd_options += ['-c', 'gc.autodetach=0']\n        cmd_options += ['gc', '--aggressive']\n        if auto:\n            cmd_options += ['--auto']\n        self.call_git(cmd_options)\n\n    def _parse_gitmodules(self):\n        # TODO read .gitconfig from Git blob?\n        gitmodules = self.pathobj / '.gitmodules'\n        if not gitmodules.exists():\n            return {}\n        # pull out file content\n        out = self.call_git(\n            ['config', '-z', '-l', '--file', '.gitmodules'],\n            read_only=True)\n        # abuse our config parser\n        # disable multi-value report, because we could not deal with them\n        # anyways, and they should not appear in a normal .gitmodules file\n        # but could easily appear when duplicates are included. In this case,\n        # we better not crash\n        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)\n        mods = {}\n        for k, v in db.items():\n            if not k.startswith('submodule.'):\n                # we don't know what this is\n                lgr.warning(\"Skip unrecognized .gitmodule specification: %s=%s\", k, v)\n                continue\n            k_l = k.split('.')\n            # module name is everything after 'submodule.' that is not the variable\n            # name\n            mod_name = '.'.join(k_l[1:-1])\n            mod = mods.get(mod_name, {})\n            # variable name is the last 'dot-free' segment in the key\n            mod[k_l[-1]] = v\n            mods[mod_name] = mod\n\n        out = {}\n        # bring into traditional shape\n        for name, props in mods.items():\n            if 'path' not in props:\n                lgr.warning(\"Failed to get '%s.path', skipping this submodule\", name)\n                continue\n            modprops = {'gitmodule_{}'.format(k): v\n                        for k, v in props.items()\n                        if not k.startswith('__')}\n            # Keep as PurePosixPath for possible normalization of / in the path etc\n            modpath = PurePosixPath(props['path'])\n            modprops['gitmodule_name'] = name\n            out[modpath] = modprops\n        return out\n\n    def get_submodules_(self, paths=None):\n        \"\"\"Yield submodules in this repository.\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`. Paths must be relative\n            to the resolved repository root, and must be normed to match the\n            reporting done by Git, i.e. no parent dir components\n            (ala \"some/../this\").\n\n        Returns\n        -------\n        A generator that yields a dictionary with information for each\n        submodule.\n        \"\"\"\n        if not (self.pathobj / \".gitmodules\").exists():\n            return\n\n        modinfo = self._parse_gitmodules()\n        if not modinfo:\n            # we exit early, if there is nothing on record (even though\n            # a .gitmodules file exists).\n            # without this conditional exit, we would be able to discover\n            # subprojects even when they are not recorded in .gitmodules,\n            # but at the cost of running an unconstrained ls-files call\n            # below\n            return\n\n        posix_mod_paths = [m.as_posix() for m in modinfo]\n        if paths:\n            # harmonize them into relative to the repository\n            posix_paths = []\n            for path in paths:\n                path = ut.PurePath(path)\n                if path.is_absolute():\n                    try:\n                        path = path.relative_to(self.pathobj)\n                    except ValueError as exc:\n                        lgr.debug(\n                            \"Path %s it not underneath %s, skipping since nothing should match it: %s\",\n                            path, self.pathobj, CapturedException(exc)\n                        )\n                        continue\n                posix_paths.append(path.as_posix())\n\n            # constrain the report by the given paths, make sure all paths are POSIX\n            posix_mod_paths = list(get_filtered_paths_(\n                posix_mod_paths,\n                posix_paths,\n                include_within_path=True,\n            ))\n\n        for r in self.call_git_items_(\n            ['ls-files', '--stage', '-z'],\n            sep='\\0',\n            files=posix_mod_paths,\n            read_only=True,\n            keep_ends=True,\n        ):\n            if not r.startswith('160000'):\n                # make sure this method never talks about non-dataset\n                # content\n                continue\n            props, rpath = r.split('\\t')\n            mode, gitsha, stage = props.split(' ')\n            if stage not in ('0', '2'):\n                # we either have non-merge situation, or a simple merge\n                # situation (i.e. stage=0). the reported gitsha always\n                # matches what we have locally.\n                # or we are in a three-way merge, in which case stage=2\n                # is what we want to report, because it matches the\n                # current HEAD (see git-read-tree manpage).\n                # there is either a stage 2 or stage 0, never both\n                continue\n            # remove the expected line separator from the path\n            rpath = rpath[:-1]\n            path = PurePosixPath(rpath)\n            yield dict(\n                path=self.pathobj / rpath, # full path returned here\n                type='dataset',\n                gitshasum=gitsha,\n                **modinfo.get(path, {})\n            )\n\n    def get_submodules(self, sorted_=True, paths=None):\n        \"\"\"Return list of submodules.\n\n        Parameters\n        ----------\n        sorted_ : bool, optional\n            Sort submodules by path name.\n        paths : list(pathlib.PurePath), optional\n            Restrict submodules to those under `paths`.\n\n        Returns\n        -------\n        List of submodule namedtuples if `compat` is true or otherwise a list\n        of dictionaries as returned by `get_submodules_`.\n        \"\"\"\n        xs = self.get_submodules_(paths=paths)\n\n        if sorted_:\n            xs = sorted(xs, key=lambda x: x[\"path\"])\n        return list(xs)\n\n    def update_ref(self, ref, value, oldvalue=None, symbolic=False):\n        \"\"\"Update the object name stored in a ref \"safely\".\n\n        Just a shim for `git update-ref` call if not symbolic, and\n        `git symbolic-ref` if symbolic\n\n        Parameters\n        ----------\n        ref : str\n          Reference, such as `ref/heads/BRANCHNAME` or HEAD.\n        value : str\n          Value to update to, e.g. hexsha of a commit when updating for a\n          branch ref, or branch ref if updating HEAD\n        oldvalue: str\n          Value to update from. Safeguard to be verified by git. This is only\n          valid if `symbolic` is not True.\n        symbolic : None\n          To instruct if ref is symbolic, e.g. should be used in case of\n          ref=HEAD\n        \"\"\"\n        if symbolic:\n            if oldvalue:\n                raise ValueError(\"oldvalue and symbolic must not be given both\")\n            cmd = ['symbolic-ref', ref, value]\n        else:\n            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])\n        self.call_git(cmd)\n\n    def tag(self, tag, message=None, commit=None, options=None):\n        \"\"\"Tag a commit\n\n        Parameters\n        ----------\n        tag : str\n          Custom tag label. Must be a valid tag name.\n        message : str, optional\n          If provided, adds ['-m', <message>] to the list of `git tag`\n          arguments.\n        commit : str, optional\n          If provided, will be appended as last argument to the `git tag` call,\n          and can be used to identify the commit that shall be tagged, if\n          not HEAD.\n        options : list, optional\n          Additional command options, inserted prior a potential `commit`\n          argument.\n        \"\"\"\n        # TODO: call in save.py complains about extensive logging. When does it\n        # happen in what way? Figure out, whether to just silence it or raise or\n        # whatever else.\n        args = ['tag']\n        if message:\n            args += ['-m', message]\n        if options is not None:\n            args.extend(options)\n        args.append(tag)\n        if commit:\n            args.append(commit)\n        self.call_git(args)\n\n    def get_tags(self, output=None):\n        \"\"\"Get list of tags\n\n        Parameters\n        ----------\n        output : str, optional\n          If given, limit the return value to a list of values matching that\n          particular key of the tag properties.\n\n        Returns\n        -------\n        list\n          Each item is a dictionary with information on a tag. At present\n          this includes 'hexsha', and 'name', where the latter is the string\n          label of the tag, and the former the hexsha of the object the tag\n          is attached to. The list is sorted by the creator date (committer\n          date for lightweight tags and tagger date for annotated tags), with\n          the most recent commit being the last element.\n        \"\"\"\n        tags = [\n            dict(\n                name=t['refname:strip=2'],\n                hexsha=t['object'] if t['object'] else t['objectname'],\n            )\n            for t in self.for_each_ref_(\n                fields=['refname:strip=2', 'objectname', 'object'],\n                pattern='refs/tags',\n                sort='creatordate')\n        ]\n        if output:\n            return [t[output] for t in tags]\n        else:\n            return tags\n\n    def describe(self, commitish=None, **kwargs):\n        \"\"\" Quick and dirty implementation to call git-describe\n\n        Parameters\n        ----------\n        kwargs:\n            transformed to cmdline options for git-describe;\n            see __init__ for description of the transformation\n        \"\"\"\n        # TODO: be more precise what failure to expect when and raise actual\n        # errors\n        cmd = ['describe'] + to_options(**kwargs)\n        if commitish is not None:\n            cmd.append(commitish)\n        try:\n            describe = self.call_git(cmd, expect_fail=True)\n            return describe.strip()\n        # TODO: WTF \"catch everything\"?\n        except:\n            return None\n\n    def get_tracking_branch(self, branch=None, remote_only=False):\n        \"\"\"Get the tracking branch for `branch` if there is any.\n\n        Parameters\n        ----------\n        branch: str\n            local branch to look up. If none is given, active branch is used.\n        remote_only : bool\n            Don't return a value if the upstream remote is set to \".\" (meaning\n            this repository).\n\n        Returns\n        -------\n        tuple\n            (remote or None, refspec or None) of the tracking branch\n        \"\"\"\n        if branch is None:\n            branch = self.get_corresponding_branch() or self.get_active_branch()\n            if branch is None:\n                return None, None\n\n        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)\n        if remote_only and track_remote == \".\":\n            return None, None\n        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)\n        return track_remote, track_branch\n\n    @property\n    def count_objects(self):\n        \"\"\"return dictionary with count, size(in KiB) information of git objects\n        \"\"\"\n\n        count_cmd = ['count-objects', '-v']\n        count_str = self.call_git(count_cmd)\n        count = {key: int(value)\n                 for key, value in [item.split(': ')\n                                    for item in count_str.split('\\n')\n                                    if len(item.split(': ')) == 2]}\n        return count\n\n    def get_git_attributes(self):\n        \"\"\"Query gitattributes which apply to top level directory\n\n        It is a thin compatibility/shortcut wrapper around more versatile\n        get_gitattributes which operates on a list of paths and returns\n        a dictionary per each path\n\n        Returns\n        -------\n        dict:\n          a dictionary with attribute name and value items relevant for the\n          top ('.') directory of the repository, and thus most likely the\n          default ones (if not overwritten with more rules) for all files within\n          repo.\n        \"\"\"\n        return self.get_gitattributes('.')['.']\n\n    def get_gitattributes(self, path, index_only=False):\n        \"\"\"Query gitattributes for one or more paths\n\n        Parameters\n        ----------\n        path: path or list\n          Path(s) to query. Paths may be relative or absolute.\n        index_only: bool\n          Flag whether to consider only gitattribute setting that are reflected\n          in the repository index, not just in the work tree content.\n\n        Returns\n        -------\n        dict:\n          Each key is a queried path (always relative to the repository root),\n          each value is a dictionary with attribute\n          name and value items. Attribute values are either True or False,\n          for set and unset attributes, or are the literal attribute value.\n        \"\"\"\n        path = ensure_list(path)\n        cmd = [\"check-attr\", \"-z\", \"--all\"]\n        if index_only:\n            cmd.append('--cached')\n        # make sure we have one entry for each query path to\n        # simplify work with the result\n        attributes = {p: {} for p in path}\n        attr = []\n        for item in self.call_git_items_(cmd, files=path, sep='\\0',\n                                         read_only=True):\n            attr.append(item)\n            if len(attr) < 3:\n                continue\n            # we have a full record\n            p, name, value = attr\n            attrs = attributes[p]\n            attrs[name] = \\\n                True if value == 'set' else False if value == 'unset' else value\n            # done, reset item\n            attr = []\n        return {relpath(k, self.path) if isabs(k) else k: v\n                for k, v in attributes.items()}\n\n    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):\n        \"\"\"Set gitattributes\n\n        By default appends additional lines to `attrfile`. Note, that later\n        lines in `attrfile` overrule earlier ones, which may or may not be\n        what you want. Set `mode` to 'w' to replace the entire file by\n        what you provided in `attrs`.\n\n        Parameters\n        ----------\n        attrs : list\n          Each item is a 2-tuple, where the first element is a path pattern,\n          and the second element is a dictionary with attribute key/value\n          pairs. The attribute dictionary must use the same semantics as those\n          returned by `get_gitattributes()`. Path patterns can use absolute paths,\n          in which case they will be normalized relative to the directory\n          that contains the target .gitattributes file (see `attrfile`).\n        attrfile: path\n          Path relative to the repository root of the .gitattributes file the\n          attributes shall be set in.\n        mode: str\n          'a' to append .gitattributes, 'w' to replace it\n        \"\"\"\n\n        git_attributes_file = op.join(self.path, attrfile)\n        attrdir = op.dirname(git_attributes_file)\n        if not op.exists(attrdir):\n            os.makedirs(attrdir)\n\n        with open(git_attributes_file, mode + '+') as f:\n            # for append, fix existing files that do not end with \\n\n            if mode == 'a' and f.tell():\n                f.seek(max(0, f.tell() - len(os.linesep)))\n                if not f.read().endswith('\\n'):\n                    f.write('\\n')\n\n            for pattern, attr in sorted(attrs, key=lambda x: x[0]):\n                # normalize the pattern relative to the target .gitattributes file\n                npath = _normalize_path(\n                    op.join(self.path, op.dirname(attrfile)), pattern)\n                # paths in gitattributes always have to be POSIX\n                npath = Path(npath).as_posix()\n                attrline = u''\n                if npath.count(' '):\n                    # quote patterns with spaces\n                    attrline += u'\"{}\"'.format(npath.replace('\"', '\\\\\"'))\n                else:\n                    attrline += npath\n                for a in sorted(attr):\n                    val = attr[a]\n                    if val is True:\n                        attrline += ' {}'.format(a)\n                    elif val is False:\n                        attrline += ' -{}'.format(a)\n                    else:\n                        attrline += ' {}={}'.format(a, val)\n                f.write('{}\\n'.format(attrline))\n\n    def get_content_info(self, paths=None, ref=None, untracked='all'):\n        \"\"\"Get identifier and type information from repository content.\n\n        This is simplified front-end for `git ls-files/tree`.\n\n        Both commands differ in their behavior when queried about subdataset\n        paths. ls-files will not report anything, ls-tree will report on the\n        subdataset record. This function uniformly follows the behavior of\n        ls-tree (report on the respective subdataset mount).\n\n        Parameters\n        ----------\n        paths : list(pathlib.PurePath) or None\n          Specific paths, relative to the resolved repository root, to query\n          info for. Paths must be normed to match the reporting done by Git,\n          i.e. no parent dir components (ala \"some/../this\").\n          If `None`, info is reported for all content.\n        ref : gitref or None\n          If given, content information is retrieved for this Git reference\n          (via ls-tree), otherwise content information is produced for the\n          present work tree (via ls-files). With a given reference, the\n          reported content properties also contain a 'bytesize' record,\n          stating the size of a file in bytes.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when no `ref` was given:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n\n          `gitshasum`\n            SHASUM of the item as tracked by Git, or None, if not\n            tracked. This could be different from the SHASUM of the file\n            in the worktree, if it was modified.\n\n        Raises\n        ------\n        ValueError\n          In case of an invalid Git reference (e.g. 'HEAD' in an empty\n          repository)\n        \"\"\"\n        lgr.debug('%s.get_content_info(...)', self)\n        # TODO limit by file type to replace code in subdatasets command\n        info = dict()\n\n        if paths:  # is not None separate after\n            # path matching will happen against what Git reports\n            # and Git always reports POSIX paths\n            # any incoming path has to be relative already, so we can simply\n            # convert unconditionally\n            # note: will be list-ified below\n            posix_paths = [ut.PurePath(p).as_posix() for p in paths]\n        elif paths is not None:\n            return info\n        else:\n            posix_paths = None\n\n        if posix_paths and (not ref or external_versions[\"cmd:git\"] >= \"2.29.0\"):\n            # If a path points within a submodule, we need to map it to the\n            # containing submodule before feeding it to ls-files or ls-tree.\n            #\n            # Before Git 2.29.0, ls-tree and ls-files differed in how they\n            # reported paths within submodules: ls-files provided no output,\n            # and ls-tree listed the submodule. Now they both return no output.\n            submodules = [s[\"path\"].relative_to(self.pathobj).as_posix()\n                          for s in self.get_submodules_()]\n            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well\n            posix_paths = get_parent_paths(posix_paths, submodules)\n\n        # this will not work in direct mode, but everything else should be\n        # just fine\n        if not ref:\n            # make sure no operations are pending before we figure things\n            # out in the worktree\n            self.precommit()\n\n            # --exclude-standard will make sure to honor and standard way\n            # git can be instructed to ignore content, and will prevent\n            # crap from contaminating untracked file reports\n            cmd = ['ls-files', '--stage', '-z']\n            # untracked report mode, using labels from `git diff` option style\n            if untracked == 'all':\n                cmd += ['--exclude-standard', '-o']\n            elif untracked == 'normal':\n                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']\n            elif untracked == 'no':\n                pass\n            else:\n                raise ValueError(\n                    'unknown value for `untracked`: {}'.format(untracked))\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\\t(?P<fname>.*)$')\n        else:\n            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']\n            props_re = re.compile(\n                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\\s]*(?P<size>[0-9-]+)\\t(?P<fname>.*)$')\n\n        lgr.debug('Query repo: %s', cmd)\n        try:\n            stdout = self.call_git(\n                cmd,\n                files=posix_paths,\n                expect_fail=True,\n                read_only=True)\n        except CommandError as exc:\n            if \"fatal: Not a valid object name\" in exc.stderr:\n                raise InvalidGitReferenceError(ref)\n            raise\n        lgr.debug('Done query repo: %s', cmd)\n\n        self._get_content_info_line_helper(\n            ref,\n            info,\n            stdout.split('\\0'),\n            props_re)\n\n        lgr.debug('Done %s.get_content_info(...)', self)\n        return info\n\n    def _get_content_info_line_helper(self, ref, info, lines, props_re):\n        \"\"\"Internal helper of get_content_info() to parse Git output\"\"\"\n        mode_type_map = {\n            '100644': 'file',\n            '100755': 'file',\n            '120000': 'symlink',\n            '160000': 'dataset',\n        }\n        for line in lines:\n            if not line:\n                continue\n            inf = {}\n            props = props_re.match(line)\n            if not props:\n                # Kludge: Filter out paths starting with .git/ to work around\n                # an `ls-files -o` bug that was fixed in Git 2.25.\n                #\n                # TODO: Drop this condition when GIT_MIN_VERSION is at least\n                # 2.25.\n                if line.startswith(\".git/\"):\n                    lgr.debug(\"Filtering out .git/ file: %s\", line)\n                    continue\n                # not known to Git, but Git always reports POSIX\n                path = ut.PurePosixPath(line)\n                inf['gitshasum'] = None\n            else:\n                # again Git reports always in POSIX\n                path = ut.PurePosixPath(props.group('fname'))\n\n            # revisit the file props after this path has not been rejected\n            if props:\n                inf['gitshasum'] = props.group('sha')\n                inf['type'] = mode_type_map.get(\n                    props.group('type'), props.group('type'))\n\n                if ref and inf['type'] == 'file':\n                    inf['bytesize'] = int(props.group('size'))\n\n            # join item path with repo path to get a universally useful\n            # path representation with auto-conversion and tons of other\n            # stuff\n            path = self.pathobj.joinpath(path)\n            if 'type' not in inf:\n                # be nice and assign types for untracked content\n                inf['type'] = 'symlink' if path.is_symlink() \\\n                    else 'directory' if path.is_dir() else 'file'\n            info[path] = inf\n\n    def status(self, paths=None, untracked='all', eval_submodule_state='full'):\n        \"\"\"Simplified `git status` equivalent.\n\n        Parameters\n        ----------\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list. If a query path points\n          into a subdataset, a report is made on the subdataset record\n          within the queried dataset only (no recursion).\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        lgr.debug('Query status of %r for %s paths',\n                  self, len(paths) if paths is not None else 'all')\n        return self.diffstatus(\n            fr='HEAD' if self.get_hexsha() else None,\n            to=None,\n            paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state)\n\n    def diff(self, fr, to, paths=None, untracked='all',\n             eval_submodule_state='full'):\n        \"\"\"Like status(), but reports changes between to arbitrary revisions\n\n        Parameters\n        ----------\n        fr : str or None\n          Revision specification (anything that Git understands). Passing\n          `None` considers anything in the target state as new.\n        to : str or None\n          Revision specification (anything that Git understands), or None\n          to compare to the state of the work tree.\n        paths : list or None\n          If given, limits the query to the specified paths. To query all\n          paths specify `None`, not an empty list.\n        untracked : {'no', 'normal', 'all'}\n          If and how untracked content is reported when `to` is None:\n          'no': no untracked files are reported; 'normal': untracked files\n          and entire untracked directories are reported as such; 'all': report\n          individual files even in fully untracked directories.\n        eval_submodule_state : {'full', 'commit', 'no'}\n          If 'full' (the default), the state of a submodule is evaluated by\n          considering all modifications, with the treatment of untracked files\n          determined by `untracked`. If 'commit', the modification check is\n          restricted to comparing the submodule's HEAD commit to the one\n          recorded in the superdataset. If 'no', the state of the subdataset is\n          not evaluated.\n\n        Returns\n        -------\n        dict\n          Each content item has an entry under a pathlib `Path` object instance\n          pointing to its absolute path inside the repository (this path is\n          guaranteed to be underneath `Repo.path`).\n          Each value is a dictionary with properties:\n\n          `type`\n            Can be 'file', 'symlink', 'dataset', 'directory'\n          `state`\n            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.\n        \"\"\"\n        return {k: v for k, v in self.diffstatus(\n            fr=fr, to=to, paths=paths,\n            untracked=untracked,\n            eval_submodule_state=eval_submodule_state).items()\n            if v.get('state', None) != 'clean'}\n\n    def diffstatus(self, fr, to, paths=None, untracked='all',\n                   eval_submodule_state='full', _cache=None):\n        \"\"\"Like diff(), but reports the status of 'clean' content too.\n\n        It supports an additional submodule evaluation state 'global'.\n        If given, it will return a single 'modified'\n        (vs. 'clean') state label for the entire repository, as soon as\n        it can.\n        \"\"\"\n        def _get_cache_key(label, paths, ref, untracked=None):\n            return self.path, label, tuple(paths) if paths else None, \\\n                ref, untracked\n\n        if _cache is None:\n            _cache = {}\n\n        if paths is not None:\n            # at this point we must normalize paths to the form that\n            # Git would report them, to easy matching later on\n            paths = map(ut.Path, paths)\n            paths = [\n                p.relative_to(self.pathobj) if p.is_absolute() else p\n                for p in paths\n            ]\n\n        # TODO report more info from get_content_info() calls in return\n        # value, those are cheap and possibly useful to a consumer\n        # we need (at most) three calls to git\n        if to is None:\n            # everything we know about the worktree, including os.stat\n            # for each file\n            key = _get_cache_key('ci', paths, None, untracked)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(\n                    paths=paths, ref=None, untracked=untracked)\n                _cache[key] = to_state\n            # we want Git to tell us what it considers modified and avoid\n            # reimplementing logic ourselves\n            key = _get_cache_key('mod', paths, None)\n            if key in _cache:\n                modified = _cache[key]\n            else:\n                # from Git 2.31.0 onwards ls-files has --deduplicate\n                # by for backward compatibility keep doing deduplication here\n                modified = set(\n                    self.pathobj.joinpath(ut.PurePosixPath(p))\n                    for p in self.call_git_items_(\n                        # we must also look for deleted files, for the logic\n                        # below to work. Only from Git 2.31.0 would they be\n                        # included with `-m` alone\n                        ['ls-files', '-z', '-m', '-d'],\n                        # low-level code cannot handle pathobjs\n                        files=[str(p) for p in paths] if paths is not None else None,\n                        sep='\\0',\n                        read_only=True)\n                    if p)\n                _cache[key] = modified\n        else:\n            key = _get_cache_key('ci', paths, to)\n            if key in _cache:\n                to_state = _cache[key]\n            else:\n                to_state = self.get_content_info(paths=paths, ref=to)\n                _cache[key] = to_state\n            # we do not need worktree modification detection in this case\n            modified = None\n        # origin state\n        key = _get_cache_key('ci', paths, fr)\n        if key in _cache:\n            from_state = _cache[key]\n        else:\n            if fr:\n                from_state = self.get_content_info(paths=paths, ref=fr)\n            else:\n                # no ref means from nothing\n                from_state = {}\n            _cache[key] = from_state\n\n        status = dict()\n        for f, to_state_r in to_state.items():\n            props = self._diffstatus_get_state_props(\n                f,\n                from_state.get(f, None),\n                to_state_r,\n                # are we comparing against a recorded commit or the worktree\n                to is not None,\n                # if we have worktree modification info, report if\n                # path is reported as modified in it\n                modified and f in modified,\n                eval_submodule_state)\n            # potential early exit in \"global\" eval mode\n            if eval_submodule_state == 'global' and \\\n                    props.get('state', None) not in ('clean', None):\n                # any modification means globally 'modified'\n                return 'modified'\n            status[f] = props\n\n        for f, from_state_r in from_state.items():\n            if f not in to_state:\n                # we new this, but now it is gone and Git is not complaining\n                # about it being missing -> properly deleted and deletion\n                # stages\n                status[f] = dict(\n                    state='deleted',\n                    type=from_state_r['type'],\n                    # report the shasum to distinguish from a plainly vanished\n                    # file\n                    gitshasum=from_state_r['gitshasum'],\n                )\n                if eval_submodule_state == 'global':\n                    return 'modified'\n\n        if to is not None or eval_submodule_state == 'no':\n            # if we have `to` we are specifically comparing against\n            # a recorded state, and this function only attempts\n            # to label the state of a subdataset, not investigate\n            # specifically what the changes in subdatasets are\n            # this is done by a high-level command like rev-diff\n            # so the comparison within this repo and the present\n            # `state` label are all we need, and they are done already\n            if eval_submodule_state == 'global':\n                return 'clean'\n            else:\n                return status\n\n        # loop over all subdatasets and look for additional modifications\n        for f, st in status.items():\n            f = str(f)\n            if 'state' in st or not st['type'] == 'dataset':\n                # no business here\n                continue\n            if not GitRepo.is_valid_repo(f):\n                # submodule is not present, no chance for a conflict\n                st['state'] = 'clean'\n                continue\n            # we have to recurse into the dataset and get its status\n            subrepo = repo_from_path(f)\n            # get the HEAD commit, or the one of the corresponding branch\n            # only that one counts re super-sub relationship\n            # save() syncs the corresponding branch each time\n            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())\n            st['gitshasum'] = subrepo_commit\n            # subdataset records must be labeled clean up to this point\n            # test if current commit in subdataset deviates from what is\n            # recorded in the dataset\n            st['state'] = 'modified' \\\n                if st['prev_gitshasum'] != subrepo_commit \\\n                else 'clean'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n            if eval_submodule_state == 'commit':\n                continue\n            # the recorded commit did not change, so we need to make\n            # a more expensive traversal\n            st['state'] = subrepo.diffstatus(\n                # we can use 'HEAD' because we know that the commit\n                # did not change. using 'HEAD' will facilitate\n                # caching the result\n                fr='HEAD',\n                to=None,\n                paths=None,\n                untracked=untracked,\n                eval_submodule_state='global',\n                _cache=_cache) if st['state'] == 'clean' else 'modified'\n            if eval_submodule_state == 'global' and st['state'] == 'modified':\n                return 'modified'\n\n        if eval_submodule_state == 'global':\n            return 'clean'\n        else:\n            return status\n\n    def _diffstatus_get_state_props(self, f, from_state, to_state,\n                                    against_commit,\n                                    modified_in_worktree,\n                                    eval_submodule_state):\n        \"\"\"Helper to determine diff properties for a single path\n\n        Parameters\n        ----------\n        f : Path\n        from_state : dict\n        to_state : dict\n        against_commit : bool\n          Flag whether `to_state` reflects a commit or the worktree.\n        modified_in_worktree : bool\n          Flag whether a worktree modification is reported. This is ignored\n          when `against_commit` is True.\n        eval_submodule_state : {'commit', 'no', ...}\n        \"\"\"\n        if against_commit:\n            # we can ignore any worktree modification reported when\n            # comparing against a commit\n            modified_in_worktree = False\n\n        props = {}\n        if 'type' in to_state:\n            props['type'] = to_state['type']\n\n        to_sha = to_state['gitshasum']\n        from_sha = from_state['gitshasum'] if from_state else None\n\n        # determine the state of `f` from from_state and to_state records, if\n        # it can be determined conclusively from it. If not, it will\n        # stay None for now\n        state = None\n        if not from_state:\n            # this is new, or rather not known to the previous state\n            state = 'added' if to_sha else 'untracked'\n        elif to_sha == from_sha and not modified_in_worktree:\n            # something that is seemingly unmodified, based on the info\n            # gathered so far\n            if to_state['type'] == 'dataset':\n                if against_commit or eval_submodule_state == 'commit':\n                    # we compare against a recorded state, just based on\n                    # the shas we can be confident, otherwise the state\n                    # of a subdataset isn't fully known yet, because\n                    # `modified_in_worktree` will only reflect changes\n                    # in the commit of a subdataset without looking into\n                    # it for uncommitted changes. Such tests are done\n                    # later and based on further conditionals for\n                    # performance reasons\n                    state = 'clean'\n            else:\n                # no change in git record, and no change on disk\n                # at this point we know that the reported object ids\n                # for this file are identical in the to and from\n                # records.  If to is None, we're comparing to the\n                # working tree and a deleted file will still have an\n                # identical id, so we need to check whether the file is\n                # gone before declaring it clean. This working tree\n                # check is irrelevant and wrong if to is a ref.\n                state = 'clean' \\\n                    if against_commit or (f.exists() or f.is_symlink()) \\\n                    else 'deleted'\n        else:\n            # change in git record, or on disk\n            # for subdatasets leave the 'modified' judgement to the caller\n            # for supporting corner cases, such as adjusted branch\n            # which require inspection of a subdataset\n            # TODO we could have a new file that is already staged\n            # but had subsequent modifications done to it that are\n            # unstaged. Such file would presently show up as 'added'\n            # ATM I think this is OK, but worth stating...\n            state = ('modified'\n                     if against_commit or to_state['type'] != 'dataset'\n                     else None\n                    ) if f.exists() or f.is_symlink() else 'deleted'\n            # TODO record before and after state for diff-like use\n            # cases\n\n        if state in ('clean', 'added', 'modified', None):\n            # assign present gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['gitshasum'] = to_sha\n            if 'bytesize' in to_state:\n                # if we got this cheap, report it\n                props['bytesize'] = to_state['bytesize']\n            elif state == 'clean' and 'bytesize' in from_state:\n                # no change, we can take this old size info\n                props['bytesize'] = from_state['bytesize']\n        if state in ('clean', 'modified', 'deleted', None):\n            # assign previous gitsha to any record\n            # state==None can only happen for subdatasets that\n            # already existed, so also assign a sha for them\n            props['prev_gitshasum'] = from_sha\n        if state:\n            # only report a state if we could determine any\n            # outside code tests for existence of the property\n            # and not (always) for the value\n            props['state'] = state\n        return props\n\n    def _save_pre(self, paths, _status, **kwargs):\n        # helper to get an actionable status report\n        if paths is not None and not paths and not _status:\n            return\n        if _status is None:\n            if 'untracked' not in kwargs:\n                kwargs['untracked'] = 'normal'\n            status = self.status(\n                paths=paths,\n                **{k: kwargs[k] for k in kwargs\n                   if k in ('untracked', 'eval_submodule_state')})\n        else:\n            # we want to be able to add items down the line\n            # make sure to detach from prev. owner\n            status = _status.copy()\n        return status\n\n    def get_staged_paths(self):\n        \"\"\"Returns a list of any stage repository path(s)\n\n        This is a rather fast call, as it will not depend on what is going on\n        in the worktree.\n        \"\"\"\n        try:\n            return list(self.call_git_items_(\n                ['diff', '--name-only', '--staged'],\n                expect_stderr=True))\n        except CommandError as e:\n            lgr.debug(CapturedException(e))\n            return []\n\n    def _save_post(self, message, files, partial_commit, amend=False,\n                   allow_empty=False):\n        # helper to commit changes reported in status\n\n        # TODO remove pathobj stringification when commit() can\n        # handle it\n        to_commit = [str(f.relative_to(self.pathobj))\n                     for f in files] \\\n                    if partial_commit else None\n        if not partial_commit or to_commit or allow_empty or \\\n                (amend and message):\n            # we directly call GitRepo.commit() to avoid a whole slew\n            # if direct-mode safeguards and workarounds in the AnnexRepo\n            # implementation (which also run an additional dry-run commit\n            GitRepo.commit(\n                self,\n                files=to_commit,\n                msg=message,\n                options=to_options(amend=amend, allow_empty=allow_empty),\n                # do not raise on empty commit\n                # it could be that the `add` in this save-cycle has already\n                # brought back a 'modified' file into a clean state\n                careless=True,\n            )\n\n    def save(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Save dataset content.\n\n        Parameters\n        ----------\n        message : str or None\n          A message to accompany the changeset in the log. If None,\n          a default message is used.\n        paths : list or None\n          Any content with path matching any of the paths given in this\n          list will be saved. Matching will be performed against the\n          dataset status (GitRepo.status()), or a custom status provided\n          via `_status`. If no paths are provided, ALL non-clean paths\n          present in the repo status or `_status` will be saved.\n        _status : dict or None\n          If None, Repo.status() will be queried for the given `ds`. If\n          a dict is given, its content will be used as a constraint.\n          For example, to save only modified content, but no untracked\n          content, set `paths` to None and provide a `_status` that has\n          no entries for untracked content.\n        **kwargs :\n          Additional arguments that are passed to underlying Repo methods.\n          Supported:\n\n          - git : bool (passed to Repo.add()\n          - eval_submodule_state : {'full', 'commit', 'no'}\n            passed to Repo.status()\n          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()\n          - amend : bool (passed to GitRepo.commit)\n        \"\"\"\n        return list(\n            self.save_(\n                message=message,\n                paths=paths,\n                _status=_status,\n                **kwargs\n            )\n        )\n\n    def save_(self, message=None, paths=None, _status=None, **kwargs):\n        \"\"\"Like `save()` but working as a generator.\"\"\"\n        from datalad.interface.results import get_status_dict\n\n        status_state = _get_save_status_state(\n            self._save_pre(paths, _status, **kwargs) or {}\n        )\n        amend = kwargs.get('amend', False)\n\n        # TODO: check on those None's -- may be those are also \"nothing to worry about\"\n        # and we could just return?\n        if not any(status_state.values()) and not (message and amend):\n            # all clean, nothing todo\n            lgr.debug('Nothing to save in %r, exiting early', self)\n            return\n\n        # three things are to be done:\n        # - remove (deleted if not already staged)\n        # - add (modified/untracked)\n        # - commit (with all paths that have been touched, to bypass\n        #   potential pre-staged bits)\n\n        staged_paths = self.get_staged_paths()\n        need_partial_commit = bool(staged_paths)\n        if need_partial_commit and hasattr(self, \"call_annex\"):\n            # so we have some staged content. let's check which ones\n            # are symlinks -- those could be annex key links that\n            # are broken after a `git-mv` operation\n            # https://github.com/datalad/datalad/issues/4967\n            # call `git-annex pre-commit` on them to rectify this before\n            # saving the wrong symlinks\n            added = status_state['added']\n            tofix = [\n                sp for sp in staged_paths\n                if added.get(self.pathobj / sp, {}).get(\"type\") == \"symlink\"\n            ]\n            if tofix:\n                self.call_annex(['pre-commit'], files=tofix)\n\n        submodule_change = False\n\n        if status_state['deleted']:\n            vanished_subds = [\n                str(f.relative_to(self.pathobj))\n                for f, props in status_state['deleted'].items()\n                if props.get('type') == 'dataset'\n            ]\n            if vanished_subds:\n                # we submodule removal we use `git-rm`, because the clean-up\n                # is more complex than just an index update -- make no\n                # sense to have a duplicate implementation.\n                # we do not yield here, but only altogether below -- we are just\n                # processing gone components, should always be quick.\n                self._call_git(['rm', '-q'], files=vanished_subds)\n                submodule_change = True\n            # remove anything from the index that was found to be gone\n            self._call_git(\n                ['update-index', '--remove'],\n                files=[\n                    str(f.relative_to(self.pathobj))\n                    for f, props in status_state['deleted'].items()\n                    # do not update the index, if there is already\n                    # something staged for this path (e.g.,\n                    # a directory was removed and a file staged\n                    # in its place)\n                    if not props.get('gitshasum')\n                    # we already did the submodules\n                    and props.get('type') != 'dataset'\n                ]\n            )\n            # now yield all deletions\n            for p, props in status_state['deleted'].items():\n                yield get_status_dict(\n                    action='delete',\n                    refds=self.pathobj,\n                    type=props.get('type'),\n                    path=p,\n                    status='ok',\n                    logger=lgr)\n\n        # TODO this additional query should not be, based on status as given\n        # if anyhow possible, however, when paths are given, status may\n        # not contain all required information. In case of path=None AND\n        # _status=None, we should be able to avoid this, because\n        # status should have the full info already\n        # looks for contained repositories\n        untracked_dirs = [\n            f.relative_to(self.pathobj)\n            for f, props in status_state['untracked'].items()\n            if props.get('type', None) == 'directory']\n        to_add_submodules = []\n        if untracked_dirs:\n            to_add_submodules = [\n                sm for sm, sm_props in\n                self.get_content_info(\n                    untracked_dirs,\n                    ref=None,\n                    # request exhaustive list, so that everything that is\n                    # still reported as a directory must be its own repository\n                    untracked='all').items()\n                if sm_props.get('type', None) == 'directory']\n            to_add_submodules = _prune_deeper_repos(to_add_submodules)\n\n        to_stage_submodules = {\n            f: props\n            for f, props in status_state['modified_or_untracked'].items()\n            if props.get('type', None) == 'dataset'}\n        if to_stage_submodules:\n            lgr.debug(\n                '%i submodule path(s) to stage in %r %s',\n                len(to_stage_submodules), self,\n                to_stage_submodules\n                if len(to_stage_submodules) < 10 else '')\n            to_add_submodules += list(to_stage_submodules)\n\n        if to_add_submodules:\n            for r in self._save_add_submodules(to_add_submodules):\n                if r.get('status', None) == 'ok':\n                    submodule_change = True\n                yield r\n\n        if submodule_change:\n            # this will alter the config, reload\n            self.config.reload()\n            # need to include .gitmodules in what needs committing\n            f = self.pathobj.joinpath('.gitmodules')\n            status_state['modified_or_untracked'][f] = \\\n                status_state['modified'][f] = \\\n                dict(type='file', state='modified')\n            # now stage .gitmodules\n            self._call_git(['update-index', '--add'], files=['.gitmodules'])\n            # and report on it\n            yield get_status_dict(\n                action='add',\n                refds=self.pathobj,\n                type='file',\n                path=f,\n                status='ok',\n                logger=lgr)\n\n        to_add = {\n            # TODO remove pathobj stringification when add() can\n            # handle it\n            str(f.relative_to(self.pathobj)): props\n            for f, props in status_state['modified_or_untracked'].items()\n            if not (f in to_add_submodules or f in to_stage_submodules)}\n        if to_add:\n            compat_config = \\\n                self.config.obtain(\"datalad.save.windows-compat-warning\")\n            to_add, problems = self._check_for_win_compat(to_add, compat_config)\n            lgr.debug(\n                '%i path(s) to add to %s %s',\n                len(to_add), self, to_add if len(to_add) < 10 else '')\n\n            if to_add:\n                yield from self._save_add(\n                        to_add,\n                        git_opts=None,\n                        **{k: kwargs[k] for k in kwargs\n                           if k in (('git',) if hasattr(self, 'uuid')\n                                    else tuple())})\n            if problems:\n                from datalad.interface.results import get_status_dict\n                msg = \\\n                    'Incompatible name for Windows systems; disable with ' \\\n                    'datalad.save.windows-compat-warning.',\n                for path in problems:\n                    yield get_status_dict(\n                        action='save',\n                        refds=self.pathobj,\n                        type='file',\n                        path=(self.pathobj / ut.PurePosixPath(path)),\n                        status='impossible',\n                        message=msg,\n                        logger=lgr)\n\n\n        # https://github.com/datalad/datalad/issues/6558\n        # file could have become a directory. Unfortunately git\n        # would then mistakenly refuse to commit if that old path is also\n        # given to commit, so we better filter it out\n        if status_state['deleted'] and status_state['added']:\n            # check if any \"deleted\" is a directory now. Then for those\n            # there should be some other path under that directory in 'added'\n            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:\n                # this could potentially be expensive if lots of files become\n                # directories, but it is unlikely to happen often\n                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish\n                # path_is_subpath faster, also if comparing to \"in f.parents\"\n                f_str = str(f)\n                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):\n                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post\n\n        # Note, that allow_empty is always ok when we amend. Required when we\n        # amend an empty commit while the amendment is empty, too (though\n        # possibly different message). If an empty commit was okay before, it's\n        # okay now.\n        status_state.pop('modified_or_untracked')  # pop the hybrid state\n        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,\n                        allow_empty=amend)\n        # TODO yield result for commit, prev helper checked hexsha pre\n        # and post...\n\n    def _check_for_win_compat(self, files, config):\n        \"\"\"Check file names for illegal characters or reserved names on Windows\n\n        In the case that a non-Windows-compatible file is detected, warn users\n        about potential interoperability issues.\n\n        Parameters\n        ----------\n        files\n          list of files to add\n        config\n          value of self.config.obtain(\"datalad.save.windows-compat-warning\"),\n          used to choose appropriate behavior. \"none\" performs no check,\n          \"warning\" warns in case of incompatibilities, and \"error\" results in\n          an error result in case of incompatibilities\n        \"\"\"\n        # don't perform any check when the configuration is set to 'none'\n        if config == 'none':\n            return files, None\n\n        from collections import defaultdict\n        problems = defaultdict(list)\n        for file in files:\n            for part in Path(file).parts:\n                # check every component of the path for incompatibilities\n                if Path(part).stem.upper() in RESERVED_NAMES_WIN:\n                    problems['Elements using a reserved filename:'].append(part)\n                    problems['paths'].append(file)\n                if re.search(ILLEGAL_CHARS_WIN, part):\n                    problems['Elements with illegal characters:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith('.'):\n                    problems['Elements ending with a dot:'].append(part)\n                    problems['paths'].append(file)\n                if part.endswith(' '):\n                    problems['Elements ending with a space:'].append(part)\n                    problems['paths'].append(file)\n        if not problems:\n            return files, None\n        msg = \\\n            \"Some elements of your dataset are not compatible with \" \\\n            \"Windows systems. Disable this check by changing \" \\\n            \"datalad.save.windows-compat-warning or consider renaming \" \\\n            \"the following elements: \"\n        for k, v in problems.items():\n            # use the key as an explanation, and report filenames only once\n            msg += f\"\\n{k} {[*{*v}]}\" if k != 'paths' else ''\n        if config == 'warning':\n            lgr.warning(msg)\n            return files, None\n\n        elif config == 'error':\n            # take the problematic files out of to_add\n            for path in [*{*problems['paths']}]:\n                files.pop(path)\n            return files, [*{*problems['paths']}]\n\n    def _save_add(self, files, git_opts=None):\n        \"\"\"Simple helper to add files in save()\"\"\"\n        from datalad.interface.results import get_status_dict\n        try:\n            # without --verbose git 2.9.3  add does not return anything\n            add_out = self._call_git(\n                # Set annex.largefiles to prevent storing files in\n                # annex with a v6+ annex repo.\n                ['-c', 'annex.largefiles=nothing', 'add'] +\n                ensure_list(git_opts) + ['--verbose'],\n                files=list(files.keys()),\n                pathspec_from_file=True,\n            )\n            # get all the entries\n            for r in self._process_git_get_output(*add_out):\n                yield get_status_dict(\n                    action=r.get('command', 'add'),\n                    refds=self.pathobj,\n                    type='file',\n                    path=(self.pathobj / ut.PurePosixPath(r['file']))\n                    if 'file' in r else None,\n                    status='ok' if r.get('success', None) else 'error',\n                    key=r.get('key', None),\n                    # while there is no git-annex underneath here, we\n                    # tend to fake its behavior, so we can also support\n                    # this type of messaging\n                    message='\\n'.join(r['error-messages'])\n                    if 'error-messages' in r else None,\n                    logger=lgr)\n        except OSError as e:\n            lgr.error(\"add: %s\", e)\n            raise\n\n    def _save_add_submodules(self, paths):\n        \"\"\"Add new submodules, or updates records of existing ones\n\n        This method does not use `git submodule add`, but aims to be more\n        efficient by limiting the scope to mere in-place registration of\n        multiple already present repositories.\n\n        Parameters\n        ----------\n        paths : list(Path)\n\n        Yields\n        ------\n        dict\n          Result records\n        \"\"\"\n        from datalad.interface.results import get_status_dict\n\n        # first gather info from all datasets in read-only fashion, and then\n        # update index, .gitmodules and .git/config at once\n        info = []\n        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843\n        # We must not add already known submodules explicitly since \"untracked\"\n        # can be assigned even for known ones (TODO: add issue, might have been closed)?\n        # Not sure if operating on relative paths would provide any speed up so use full\n        known_sub_paths = {s['path'] for s in self.get_submodules_()}\n        for path in paths:\n            already_known = path in known_sub_paths\n            rpath = str(path.relative_to(self.pathobj).as_posix())\n            subm = repo_from_path(path)\n            # if there is a corresponding branch, we want to record it's state.\n            # we rely on the corresponding branch being synced already.\n            # `save` should do that each time it runs.\n            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())\n            if not subm_commit:\n                yield get_status_dict(\n                    action='add_submodule',\n                    ds=self,\n                    path=path,\n                    status='error',\n                    message=('cannot add subdataset %s with no commits', subm),\n                    logger=lgr)\n                continue\n            # make an attempt to configure a submodule source URL based on the\n            # discovered remote configuration\n            remote, branch = subm.get_tracking_branch()\n            url = subm.get_remote_url(remote) if remote else None\n            if url is None:\n                url = './{}'.format(rpath)\n            subm_id = subm.config.get('datalad.dataset.id', None)\n            info.append(\n                dict(\n                     # if we have additional information on this path, pass it on.\n                     # if not, treat it as an untracked directory\n                     paths[path] if isinstance(paths, dict)\n                     else dict(type='directory', state='untracked'),\n                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,\n                     url=url, known=already_known))\n\n        # bypass any convenience or safe-manipulator for speed reasons\n        # use case: saving many new subdatasets in a single run\n        with (self.pathobj / '.gitmodules').open('a') as gmf, \\\n             (self.pathobj / '.git' / 'config').open('a') as gcf:\n            for i in info:\n                # we update the subproject commit unconditionally\n                self.call_git([\n                    'update-index', '--add', '--replace', '--cacheinfo', '160000',\n                    i['commit'], i['rpath']\n                ])\n                # only write the .gitmodules/.config changes when this is not yet\n                # a subdataset and not yet already known\n                # TODO: we could update the URL, and branch info at this point,\n                # even for previously registered subdatasets\n                if not i['known'] and (i['type'] != 'dataset' or (\n                        i['type'] == 'dataset' and i['state'] == 'untracked')):\n                    gmprops = dict(path=i['rpath'], url=i['url'])\n                    if i['id']:\n                        gmprops['datalad-id'] = i['id']\n                    write_config_section(\n                        gmf, 'submodule', i['rpath'], gmprops)\n                    write_config_section(\n                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))\n\n                # This mirrors the result structure yielded for\n                # to_stage_submodules below.\n                yield get_status_dict(\n                    action='add',\n                    refds=self.pathobj,\n                    type='dataset',\n                    key=None,\n                    path=i['path'],\n                    status='ok',\n                    logger=lgr)\n\n\ndef _get_save_status_state(status):\n    \"\"\"\n    Returns\n    -------\n    dict\n      By status category by file path, mapped to status properties.\n    \"\"\"\n    # Sort status into status by state with explicit list of states\n    # (excluding clean we do not care about) we expect to be present\n    # and which we know of (unless None), and modified_or_untracked hybrid\n    # since it is used below\n    status_state = {\n        k: {}\n        for k in (None,  # not cared of explicitly here\n                  'added',  # not cared of explicitly here\n                  # 'clean'  # not even wanted since nothing to do about those\n                  'deleted',\n                  'modified',\n                  'untracked',\n                  'modified_or_untracked',  # hybrid group created here\n                  )}\n    for f, props in status.items():\n        state = props.get('state', None)\n        if state == 'clean':\n            # we don't care about clean\n            continue\n        if state == 'modified' and props.get('gitshasum') \\\n                and props.get('gitshasum') == props.get('prev_gitshasum'):\n            # reported as modified, but with identical shasums -> typechange\n            # a subdataset maybe? do increasingly expensive tests for\n            # speed reasons\n            if props.get('type') != 'dataset' and f.is_dir() \\\n                    and GitRepo.is_valid_repo(f):\n                # it was not a dataset, but now there is one.\n                # we declare it untracked to engage the discovery tooling.\n                state = 'untracked'\n                props = dict(type='dataset', state='untracked')\n        status_state[state][f] = props\n        # The hybrid one to retain the same order as in original status\n        if state in ('modified', 'untracked'):\n            status_state['modified_or_untracked'][f] = props\n    return status_state\n\n\n# used in in the get command and GitRepo.add_submodule(), the\n# latter is not used outside the tests\ndef _fixup_submodule_dotgit_setup(ds, relativepath):\n    \"\"\"Implementation of our current of .git in a subdataset\n\n    Each subdataset/module has its own .git directory where a standalone\n    repository would have it. No gitdir files, no symlinks.\n    \"\"\"\n    # move .git to superrepo's .git/modules, remove .git, create\n    # .git-file\n    path = opj(ds.path, relativepath)\n    subds_dotgit = opj(path, \".git\")\n\n    repo = GitRepo(path, create=False)\n    if repo.dot_git.parent == repo.pathobj:\n        # this is what we want\n        return\n\n    # first we want to remove any conflicting worktree setup\n    # done by git to find the checkout at the mountpoint of the\n    # submodule, if we keep that, any git command will fail\n    # after we move .git\n    # Ben: Shouldn't we re-setup a possible worktree afterwards?\n    repo.config.unset('core.worktree', scope='local')\n    # what we have here is some kind of reference, remove and\n    # replace by the target\n    os.remove(subds_dotgit)\n    # make absolute\n    src_dotgit = str(repo.dot_git)\n    # move .git\n    from os import (\n        listdir,\n        rename,\n        rmdir,\n    )\n    ensure_dir(subds_dotgit)\n    for dot_git_entry in listdir(src_dotgit):\n        rename(opj(src_dotgit, dot_git_entry),\n               opj(subds_dotgit, dot_git_entry))\n    assert not listdir(src_dotgit)\n    rmdir(src_dotgit)\n\n\n# try retro-fitting GitRepo with deprecated functionality\n# must be done last in this file\ntry:\n    from datalad_deprecated.gitrepo import DeprecatedGitRepoMethods\n    for symbol in dir(DeprecatedGitRepoMethods):\n        if symbol.startswith('__'):\n            # ignore Python internals\n            continue\n        if hasattr(GitRepo, symbol):\n            lgr.debug(\n                'Not retro-fitted GitRepo with deprecated %s, '\n                'name-space conflict', symbol)\n            # do not override existing symbols\n            continue\n        # assign deprecated symbol to GitRepo\n        setattr(GitRepo, symbol, getattr(DeprecatedGitRepoMethods, symbol))\n        lgr.debug('Retro-fitted GitRepo with deprecated %s', symbol)\nexcept ImportError as e:\n    ce = CapturedException(e)\n    lgr.debug(\n        'Not retro-fitting GitRepo with deprecated symbols, '\n        'datalad-deprecated package not found')\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** GitRepo.merge Allows Merging Unrelated Histories Without Explicit Permission

**Description:**
When attempting to merge two independent repositories, the `GitRepo.merge` function proceeds without requiring explicit permission to allow unrelated histories. This behavior can lead to unintended merges, causing repository inconsistencies and potential data conflicts.

**Example Code:**
```python
from datalad import Dataset
from datalad.support.annexrepo import AnnexRepo

# Create two separate repositories
ds = Dataset('/tmp/repo1').create()
repo = AnnexRepo('/tmp/repo2')
file = '/tmp/repo2/file.dat'
with open(file, 'w') as f:
    f.write("data")
repo.add(file)
repo.commit()

# Attempt to add and merge the unrelated repository
ds.siblings(action='add', name='repo', url=repo.path)
result = ds.update(how='merge', on_failure='ignore')

# Expecting the merge to fail due to unrelated histories
```

**Expected Behavior:**
The merge operation should fail and return an error message indicating that merging unrelated histories is not allowed unless explicitly permitted. This prevents accidental merges that can complicate the repository's history.

**Actual Behavior:**
The merge operation completes successfully without any errors, even though the histories of the two repositories are unrelated. This can result in an inconsistent state of the repository and make it difficult to trace changes accurately.

[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### datalad/support/gitrepo.py
# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 et:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the datalad package for the
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Internal low-level interface to Git repositories

"""

import logging
import os
import os.path as op
import posixpath
import re
import warnings
from collections.abc import Mapping
from functools import wraps
from itertools import chain
from os import linesep
from os.path import (
    commonprefix,
    curdir,
    dirname,
    exists,
    isabs,
)
from os.path import join as opj
from os.path import (
    pardir,
    relpath,
    sep,
)

import datalad.utils as ut
from datalad import ssh_manager
from datalad.cmd import (
    BatchedCommand,
    GitWitlessRunner,
    NoCapture,
    StdOutErrCapture,
    WitlessProtocol,
)
from datalad.config import (
    parse_gitconfig_dump,
    write_config_section,
)
from datalad.consts import (
    ILLEGAL_CHARS_WIN,
    RESERVED_NAMES_WIN,
)
from datalad.core.local.repo import repo_from_path
from datalad.dataset.gitrepo import GitRepo as CoreGitRepo
from datalad.dataset.gitrepo import (
    _get_dot_git,
    path_based_str_repr,
)
from datalad.log import log_progress
from datalad.support.due import (
    Doi,
    due,
)
from datalad.utils import (
    Path,
    PurePosixPath,
    ensure_dir,
    ensure_list,
    ensure_unicode,
    generate_file_chunks,
    getpwd,
    is_interactive,
    on_windows,
    optional_args,
    path_is_subpath,
    posix_relpath,
)

from .exceptions import (
    CapturedException,
    CommandError,
    FileNotInRepositoryError,
    InvalidGitReferenceError,
    InvalidGitRepositoryError,
    NoSuchPathError,
)
# imports from same module:
from .external_versions import external_versions
from .network import (
    RI,
    PathRI,
    is_ssh,
)
from .path import get_parent_paths, get_filtered_paths_

# shortcuts
_curdirsep = curdir + sep
_pardirsep = pardir + sep


lgr = logging.getLogger('datalad.gitrepo')


# outside the repo base classes only used in ConfigManager
def to_options(split_single_char_options=True, **kwargs):
    """Transform keyword arguments into a list of cmdline options

    Imported from GitPython.

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License

    Parameters
    ----------
    split_single_char_options: bool

    kwargs:

    Returns
    -------
    list
    """
    def dashify(string):
        return string.replace('_', '-')

    def transform_kwarg(name, value, split_single_char_options):
        if len(name) == 1:
            if value is True:
                return ["-%s" % name]
            elif value not in (False, None):
                if split_single_char_options:
                    return ["-%s" % name, "%s" % value]
                else:
                    return ["-%s%s" % (name, value)]
        else:
            if value is True:
                return ["--%s" % dashify(name)]
            elif value is not False and value is not None:
                return ["--%s=%s" % (dashify(name), value)]
        return []

    args = []
    kwargs = dict(sorted(kwargs.items(), key=lambda x: x[0]))
    for k, v in kwargs.items():
        if isinstance(v, (list, tuple)):
            for value in v:
                args += transform_kwarg(k, value, split_single_char_options)
        else:
            args += transform_kwarg(k, v, split_single_char_options)
    return args


def _normalize_path(base_dir, path):
    """Helper to check paths passed to methods of this class.

    Checks whether `path` is beneath `base_dir` and normalizes it.
    Additionally paths are converted into relative paths with respect to
    `base_dir`, considering PWD in case of relative paths. This
    is intended to be used in repository classes, which means that
    `base_dir` usually will be the repository's base directory.

    Parameters
    ----------
    base_dir: str
        directory to serve as base to normalized, relative paths
    path: str
        path to be normalized

    Returns
    -------
    str:
        path, that is a relative path with respect to `base_dir`
    """
    if not path:
        return path
    pathobj = Path(path)

    # do absolute() in addition to always get an absolute path
    # even with non-existing base_dirs on windows
    base_dir = str(Path(base_dir).resolve().absolute())  # realpath OK

    # path = normpath(path)
    # Note: disabled normpath, because it may break paths containing symlinks;
    # But we don't want to realpath relative paths, in case cwd isn't the
    # correct base.

    if pathobj.is_absolute():
        # path might already be a symlink pointing to annex etc,
        # so realpath only its directory, to get "inline" with
        # realpath(base_dir) above
        path = str(pathobj.parent.resolve() / pathobj.name)  # realpath OK
    # Executive decision was made to not do this kind of magic!
    #
    # elif commonprefix([realpath(getpwd()), base_dir]) == base_dir:
    #     # If we are inside repository, rebuilt relative paths.
    #     path = opj(realpath(getpwd()), path)
    #
    # BUT with relative curdir/pardir start it would assume relative to curdir
    #
    elif path.startswith(_curdirsep) or path.startswith(_pardirsep):
        path = str(Path(getpwd()).resolve() / pathobj)  # realpath OK
    else:
        # We were called from outside the repo. Therefore relative paths
        # are interpreted as being relative to self.path already.
        return path

    if commonprefix([path, base_dir]) != base_dir:
        raise FileNotInRepositoryError(msg="Path outside repository: %s"
                                           % base_dir, filename=path)

    return relpath(path, start=base_dir)


@optional_args
def normalize_path(func):
    """Decorator to provide unified path conversion for a single file

    Unlike normalize_paths, intended to be used for functions dealing with a
    single filename at a time

    Note
    ----
    This is intended to be used within the repository classes and therefore
    returns a class method!

    The decorated function is expected to take a path at
    first positional argument (after 'self'). Additionally the class `func`
    is a member of, is expected to have an attribute 'path'.
    """

    @wraps(func)
    def  _wrap_normalize_path(self, file_, *args, **kwargs):
        file_new = _normalize_path(self.path, file_)
        return func(self, file_new, *args, **kwargs)

    return  _wrap_normalize_path


@optional_args
def normalize_paths(func, match_return_type=True, map_filenames_back=False,
                    serialize=False):
    """Decorator to provide unified path conversions.

    Note
    ----
    This is intended to be used within the repository classes and therefore
    returns a class method!

    The decorated function is expected to take a path or a list of paths at
    first positional argument (after 'self'). Additionally the class `func`
    is a member of, is expected to have an attribute 'path'.

    Accepts either a list of paths or a single path in a str. Passes a list
    to decorated function either way, but would return based on the value of
    match_return_type and possibly input argument.

    If a call to the wrapped function includes normalize_path and it is False
    no normalization happens for that function call (used for calls to wrapped
    functions within wrapped functions, while possible CWD is within a
    repository)

    Parameters
    ----------
    match_return_type : bool, optional
      If True, and a single string was passed in, it would return the first
      element of the output (after verifying that it is a list of length 1).
      It makes easier to work with single files input.
    map_filenames_back : bool, optional
      If True and returned value is a dictionary, it assumes to carry entries
      one per file, and then filenames are mapped back to as provided from the
      normalized (from the root of the repo) paths
    serialize : bool, optional
      Loop through files giving only a single one to the function one at a time.
      This allows to simplify implementation and interface to annex commands
      which do not take multiple args in the same call (e.g. checkpresentkey)
    """

    @wraps(func)
    def  _wrap_normalize_paths(self, files, *args, **kwargs):

        normalize = _normalize_path if kwargs.pop('normalize_paths', True) \
            else lambda rpath, filepath: filepath

        if files:
            if isinstance(files, str) or not files:
                files_new = [normalize(self.path, files)]
                single_file = True
            elif isinstance(files, list):
                files_new = [normalize(self.path, path) for path in files]
                single_file = False
            else:
                raise ValueError("_files_decorator: Don't know how to handle "
                                 "instance of %s." % type(files))
        else:
            single_file = None
            files_new = []

        if map_filenames_back:
            def remap_filenames(out):
                """Helper to map files back to non-normalized paths"""
                if isinstance(out, dict):
                    assert(len(out) == len(files_new))
                    files_ = [files] if single_file else files
                    mapped = out.__class__()
                    for fin, fout in zip(files_, files_new):
                        mapped[fin] = out[fout]
                    return mapped
                else:
                    return out
        else:
            remap_filenames = lambda x: x

        if serialize:  # and not single_file:
            result = [
                func(self, f, *args, **kwargs)
                for f in files_new
            ]
        else:
            result = func(self, files_new, *args, **kwargs)

        if single_file is None:
            # no files were provided, nothing we can do really
            return result
        elif (result is None) or not match_return_type or not single_file:
            # If function doesn't return anything or no denormalization
            # was requested or it was not a single file
            return remap_filenames(result)
        elif single_file:
            if len(result) != 1:
                # Magic doesn't apply
                return remap_filenames(result)
            elif isinstance(result, (list, tuple)):
                return result[0]
            elif isinstance(result, dict) and tuple(result)[0] == files_new[0]:
                # assume that returned dictionary has files as keys.
                return tuple(result.values())[0]
            else:
                # no magic can apply
                return remap_filenames(result)
        else:
            return RuntimeError("should have not got here... check logic")

    return  _wrap_normalize_paths


if "2.24.0" <= external_versions["cmd:git"] < "2.25.0":
    # An unintentional change in Git 2.24.0 led to `ls-files -o` traversing
    # into untracked submodules when multiple pathspecs are given, returning
    # repositories that are deeper than the first level. This helper filters
    # these deeper levels out so that save_() doesn't fail trying to add them.
    #
    # This regression fixed with upstream's 072a231016 (2019-12-10).
    def _prune_deeper_repos(repos):
        firstlevel_repos = []
        prev = None
        for repo in sorted(repos):
            if not (prev and str(repo).startswith(prev)):
                prev = str(repo)
                firstlevel_repos.append(repo)
        return firstlevel_repos
else:
    def _prune_deeper_repos(repos):
        return repos


class GitProgress(WitlessProtocol):
    """Reduced variant of GitPython's RemoteProgress class

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License
    """
    # inform super-class to capture stderr
    proc_err = True

    _num_op_codes = 10
    BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING, FINDING_SOURCES, CHECKING_OUT, ENUMERATING = \
        [1 << x for x in range(_num_op_codes)]
    STAGE_MASK = BEGIN | END
    OP_MASK = ~STAGE_MASK

    DONE_TOKEN = 'done.'
    TOKEN_SEPARATOR = ', '

    _known_ops = {
        COUNTING: ("Counting", "Objects"),
        ENUMERATING: ("Enumerating", "Objects"),
        COMPRESSING: ("Compressing", "Objects"),
        WRITING: ("Writing", "Objects"),
        RECEIVING: ("Receiving", "Objects"),
        RESOLVING: ("Resolving", "Deltas"),
        FINDING_SOURCES: ("Finding", "Sources"),
        CHECKING_OUT: ("Check out", "Things"),
    }

    __slots__ = ('_seen_ops', '_pbars', '_encoding')

    re_op_absolute = re.compile(r"(remote: )?([\w\s]+):\s+()(\d+)()(.*)")
    re_op_relative = re.compile(r"(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")

    def __init__(self, *args):
        super().__init__(*args)
        self._unprocessed = None

    def connection_made(self, transport):
        super().connection_made(transport)
        self._seen_ops = []
        self._pbars = set()

    def process_exited(self):
        # take down any progress bars that were not closed orderly
        for pbar_id in self._pbars:
            log_progress(
                lgr.info,
                pbar_id,
                'Finished',
            )
        super().process_exited()

    def pipe_data_received(self, fd, byts):
        # progress reports only come from stderr
        if fd != 2:
            # let the base class decide what to do with it
            super().pipe_data_received(fd, byts)
            return
        for line in byts.splitlines(keepends=True):
            # put any unprocessed content back in front
            line = self._unprocessed + line if self._unprocessed else line
            self._unprocessed = None
            if not self._parse_progress_line(line):
                # anything that doesn't look like a progress report
                # is retained and returned
                # in case of partial progress lines, this can lead to
                # leakage of progress info into the output, but
                # it is better to enable better (maybe more expensive)
                # subsequent filtering than hiding lines with
                # unknown, potentially important info
                lgr.debug('Non-progress stderr: %s', line)
                if line.endswith((b'\r', b'\n')):
                    # complete non-progress line, pass on
                    super().pipe_data_received(fd, line)
                else:
                    # an incomplete line, maybe the next batch completes
                    # it to become a recognizable progress report
                    self._unprocessed = line

    def _parse_progress_line(self, line):
        """Process a single line

        Parameters
        ----------
        line : bytes

        Returns
        -------
        bool
          Flag whether the line was recognized as a Git progress report.
        """
        # handle
        # Counting objects: 4, done.
        # Compressing objects:  50% (1/2)
        # Compressing objects: 100% (2/2)
        # Compressing objects: 100% (2/2), done.
        line = line.decode(self.encoding) if isinstance(line, bytes) else line
        if line.startswith(('warning:', 'error:', 'fatal:')):
            return False

        # find escape characters and cut them away - regex will not work with
        # them as they are non-ascii. As git might expect a tty, it will send them
        last_valid_index = None
        for i, c in enumerate(reversed(line)):
            if ord(c) < 32:
                # its a slice index
                last_valid_index = -i - 1
            # END character was non-ascii
        # END for each character in line
        if last_valid_index is not None:
            line = line[:last_valid_index]
        # END cut away invalid part
        line = line.rstrip()

        cur_count, max_count = None, None
        match = self.re_op_relative.match(line)
        if match is None:
            match = self.re_op_absolute.match(line)

        if not match:
            return False
        # END could not get match

        op_code = 0
        _remote, op_name, _percent, cur_count, max_count, message = match.groups()

        # get operation id
        if op_name == "Counting objects":
            op_code |= self.COUNTING
        elif op_name == "Compressing objects":
            op_code |= self.COMPRESSING
        elif op_name == "Writing objects":
            op_code |= self.WRITING
        elif op_name == 'Receiving objects':
            op_code |= self.RECEIVING
        elif op_name == 'Resolving deltas':
            op_code |= self.RESOLVING
        elif op_name == 'Finding sources':
            op_code |= self.FINDING_SOURCES
        elif op_name == 'Checking out files':
            op_code |= self.CHECKING_OUT
        elif op_name == 'Enumerating objects':
            op_code |= self.ENUMERATING
        else:
            # Note: On windows it can happen that partial lines are sent
            # Hence we get something like "CompreReceiving objects", which is
            # a blend of "Compressing objects" and "Receiving objects".
            # This can't really be prevented.
            lgr.debug(
                'Output line matched a progress report of an unknown type: %s',
                line)
            # TODO investigate if there is any chance that we might swallow
            # important info -- until them do not flag this line
            # as progress
            return False
        # END handle op code

        pbar_id = 'gitprogress-{}-{}'.format(id(self), op_code)

        op_props = self._known_ops[op_code]

        # figure out stage
        if op_code not in self._seen_ops:
            self._seen_ops.append(op_code)
            op_code |= self.BEGIN
            log_progress(
                lgr.info,
                pbar_id,
                'Start {} {}'.format(
                    op_props[0].lower(),
                    op_props[1].lower(),
                ),
                label=op_props[0],
                unit=' {}'.format(op_props[1]),
                total=float(max_count) if max_count else None,
            )
            self._pbars.add(pbar_id)
        # END begin opcode

        if message is None:
            message = ''
        # END message handling

        done_progress = False
        message = message.strip()
        if message.endswith(self.DONE_TOKEN):
            op_code |= self.END
            message = message[:-len(self.DONE_TOKEN)]
            done_progress = True
        # END end message handling
        message = message.strip(self.TOKEN_SEPARATOR)

        if cur_count and max_count:
            log_progress(
                lgr.info,
                pbar_id,
                line,
                update=float(cur_count),
                noninteractive_level=logging.DEBUG,
            )

        if done_progress:
            log_progress(
                lgr.info,
                pbar_id,
                'Finished {} {}'.format(
                    op_props[0].lower(),
                    op_props[1].lower(),
                ),
                noninteractive_level=logging.DEBUG,
            )
            self._pbars.discard(pbar_id)
        return True


class StdOutCaptureWithGitProgress(GitProgress):
    proc_out = True


class FetchInfo(dict):
    """
    dict that carries results of a fetch operation of a single head

    Reduced variant of GitPython's RemoteProgress class

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License
    """

    NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \
        FAST_FORWARD, ERROR = [1 << x for x in range(8)]

    _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) [-> ]+ ([^\s]+)(    \(.*\)?$)?')

    _flag_map = {
        '!': ERROR,
        '+': FORCED_UPDATE,
        '*': 0,
        '=': HEAD_UPTODATE,
        ' ': FAST_FORWARD,
        '-': TAG_UPDATE,
    }
    _operation_map = {
        NEW_TAG: 'new-tag',
        NEW_HEAD: 'new-branch',
        HEAD_UPTODATE: 'uptodate',
        TAG_UPDATE: 'tag-update',
        REJECTED: 'rejected',
        FORCED_UPDATE: 'forced-update',
        FAST_FORWARD: 'fast-forward',
        ERROR: 'error',
    }

    @classmethod
    def _from_line(cls, line):
        """Parse information from the given line as returned by git-fetch -v
        and return a new FetchInfo object representing this information.
        """
        match = cls._re_fetch_result.match(line)
        if match is None:
            raise ValueError("Failed to parse line: %r" % line)

        # parse lines
        control_character, operation, local_remote_ref, remote_local_ref, note = \
            match.groups()

        # parse flags from control_character
        flags = 0
        try:
            flags |= cls._flag_map[control_character]
        except KeyError:
            raise ValueError(
                "Control character %r unknown as parsed from line %r"
                % (control_character, line))
        # END control char exception handling

        # parse operation string for more info - makes no sense for symbolic refs,
        # but we parse it anyway
        old_commit = None
        if 'rejected' in operation:
            flags |= cls.REJECTED
        if 'new tag' in operation:
            flags |= cls.NEW_TAG
        if 'tag update' in operation:
            flags |= cls.TAG_UPDATE
        if 'new branch' in operation:
            flags |= cls.NEW_HEAD
        if '...' in operation or '..' in operation:
            split_token = '...'
            if control_character == ' ':
                split_token = split_token[:-1]
            old_commit = operation.split(split_token)[0]
        # END handle refspec

        return cls(
            ref=remote_local_ref.strip(),
            local_ref=local_remote_ref.strip(),
            # convert flag int into a list of operation labels
            operations=[
                cls._operation_map[o]
                for o in cls._operation_map.keys()
                if flags & o
            ],
            note=note,
            old_commit=old_commit,
        )


class PushInfo(dict):
    """dict that carries results of a push operation of a single head

    Reduced variant of GitPython's RemoteProgress class

    Original copyright:
        Copyright (C) 2008, 2009 Michael Trier and contributors
    Original license:
        BSD 3-Clause "New" or "Revised" License
    """
    NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \
        FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [1 << x for x in range(11)]

    _flag_map = {'X': NO_MATCH,
                 '-': DELETED,
                 '*': 0,
                 '+': FORCED_UPDATE,
                 ' ': FAST_FORWARD,
                 '=': UP_TO_DATE,
                 '!': ERROR}

    _operation_map = {
        NEW_TAG: 'new-tag',
        NEW_HEAD: 'new-branch',
        NO_MATCH: 'no-match',
        REJECTED: 'rejected',
        REMOTE_REJECTED: 'remote-rejected',
        REMOTE_FAILURE: 'remote-failure',
        DELETED: 'deleted',
        FORCED_UPDATE: 'forced-update',
        FAST_FORWARD: 'fast-forward',
        UP_TO_DATE: 'uptodate',
        ERROR: 'error',
    }

    @classmethod
    def _from_line(cls, line):
        """Create a new PushInfo instance as parsed from line which is expected to be like
            refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes"""
        control_character, from_to, summary = line.split('\t', 3)
        flags = 0

        # control character handling
        try:
            flags |= cls._flag_map[control_character]
        except KeyError:
            raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line))
        # END handle control character

        # from_to handling
        from_ref_string, to_ref_string = from_to.split(':')

        # commit handling, could be message or commit info
        old_commit = None
        if summary.startswith('['):
            if "[rejected]" in summary:
                flags |= cls.REJECTED
            elif "[remote rejected]" in summary:
                flags |= cls.REMOTE_REJECTED
            elif "[remote failure]" in summary:
                flags |= cls.REMOTE_FAILURE
            elif "[no match]" in summary:
                flags |= cls.ERROR
            elif "[new tag]" in summary:
                flags |= cls.NEW_TAG
            elif "[new branch]" in summary:
                flags |= cls.NEW_HEAD
            # up-to-date encoded in control character
        else:
            # fast-forward or forced update - was encoded in control character,
            # but we parse the old and new commit
            split_token = "..."
            if control_character == " ":
                split_token = ".."
            old_sha, _new_sha = summary.split(' ')[0].split(split_token)
            # have to use constructor here as the sha usually is abbreviated
            old_commit = old_sha
        # END message handling

        return cls(
            from_ref=from_ref_string.strip(),
            to_ref=to_ref_string.strip(),
            # convert flag int into a list of operation labels
            operations=[
                cls._operation_map[o]
                for o in cls._operation_map.keys()
                if flags & o
            ],
            note=summary.strip(),
            old_commit=old_commit,
        )


@path_based_str_repr
class GitRepo(CoreGitRepo):
    """Representation of a git repository

    """
    # We must check git config to have name and email set, but
    # should do it once
    _config_checked = False

    GIT_MIN_VERSION = "2.19.1"
    git_version = None

    @classmethod
    def _check_git_version(cls):
        external_versions.check("cmd:git", min_version=cls.GIT_MIN_VERSION)
        cls.git_version = external_versions['cmd:git']

    # This is the least common denominator to claim that a user
    # used DataLad.
    # Citing JOSS publication https://joss.theoj.org/papers/10.21105/joss.03262
    @due.dcite(Doi("10.21105/joss.03262"),
               # override path since there is no need ATM for such details
               path="datalad",
               description="DataLad - Data management and distribution platform")
    def __init__(self, path, runner=None, create=True,
                 git_opts=None, repo=None, fake_dates=False,
                 create_sanity_checks=True,
                 **kwargs):
        """Creates representation of git repository at `path`.

        Can also be used to create a git repository at `path`.

        Parameters
        ----------
        path: str
          path to the git repository; In case it's not an absolute path,
          it's relative to PWD
        create: bool, optional
          if true, creates a git repository at `path` if there is none. Also
          creates `path`, if it doesn't exist.
          If set to false, an exception is raised in case `path` doesn't exist
          or doesn't contain a git repository.
        repo: git.Repo, optional
          This argument is ignored.
        create_sanity_checks: bool, optional
          Whether to perform sanity checks during initialization (when
          `create=True` and target path is not a valid repo already), such as
          that new repository is not created in the directory where git already
          tracks some files.
        kwargs:
          keyword arguments serving as additional options to the git-init
          command. Therefore, it makes sense only if called with `create`.

          Generally, this way of passing options to the git executable is
          (or will be) used a lot in this class. It's a transformation of
          python-style keyword arguments (or a `dict`) to command line arguments,
          provided by GitPython.

          A single character keyword will be prefixed by '-', multiple characters
          by '--'. An underscore in the keyword becomes a dash. The value of the
          keyword argument is used as the value for the corresponding command
          line argument. Assigning a boolean creates a flag.

          Examples:
          no_commit=True => --no-commit
          C='/my/path'   => -C /my/path

        """
        # this will set up .pathobj and .dot_git
        super().__init__(path)

        if self.git_version is None:
            self._check_git_version()

        # BEGIN Repo validity test
        # We want to fail early for tests, that would be performed a lot. In
        # particular this is about GitRepo.is_valid_repo. We would use the
        # latter to decide whether or not to call GitRepo() only for __init__ to
        # then test the same things again. If we fail early we can save the
        # additional test from outer scope.
        self.path = path

        # Note, that the following three path objects are used often and
        # therefore are stored for performance. Path object creation comes with
        # a cost. Most notably, this is used for validity checking of the
        # repository.
        _valid_repo = self.is_valid_git()

        do_create = False
        if create and not _valid_repo:
            if repo is not None:
                # `repo` passed with `create`, which doesn't make sense
                raise TypeError("argument 'repo' must not be used with 'create'")
            do_create = True
        else:
            # Note: We used to call gitpy.Repo(path) here, which potentially
            # raised NoSuchPathError or InvalidGitRepositoryError. This is
            # used by callers of GitRepo.__init__() to detect whether we have a
            # valid repo at `path`. Now, with switching to lazy loading property
            # `repo`, we detect those cases without instantiating a
            # gitpy.Repo().

            if not exists(path):
                raise NoSuchPathError(path)
            if not _valid_repo:
                raise InvalidGitRepositoryError(path)
        # END Repo validity test

        # So that we "share" control paths with git/git-annex
        if ssh_manager:
            ssh_manager.ensure_initialized()

        # note: we may also want to distinguish between a path to the worktree
        # and the actual repository

        if git_opts is None:
            git_opts = {}
        if kwargs:
            git_opts.update(kwargs)

        self._cfg = None

        if do_create:  # we figured it out earlier
            from_cmdline = git_opts.pop('_from_cmdline_', [])
            self.init(
                sanity_checks=create_sanity_checks,
                init_options=from_cmdline + to_options(**git_opts),
            )

        # with DryRunProtocol path might still not exist
        if exists(self.path):
            self.inode = os.stat(self.path).st_ino
        else:
            self.inode = None

        if fake_dates:
            self.configure_fake_dates()

    @property
    def bare(self):
        """Returns a bool indicating whether the repository is bare

        Importantly, this is not reporting the configuration value
        of 'core.bare', in order to be usable at a stage where a
        Repo instance is not yet equipped with a ConfigManager.
        Instead, it is testing whether the repository path and its
        "dot_git" are identical. The value of 'core.bare' can be query
        from the ConfigManager in a fully initialized instance.
        """
        return self.pathobj == self.dot_git

    @classmethod
    def clone(cls, url, path, *args, clone_options=None, **kwargs):
        """Clone url into path

        Provides workarounds for known issues (e.g.
        https://github.com/datalad/datalad/issues/785)

        Parameters
        ----------
        url : str
        path : str
        clone_options : dict or list
          Arbitrary options that will be passed on to the underlying call to
          `git-clone`. This may be a list of plain options or key-value pairs
          that will be converted to a list of plain options with `to_options`.
        expect_fail : bool
          Whether expect that command might fail, so error should be logged then
          at DEBUG level instead of ERROR
        kwargs:
          Passed to the Repo class constructor.
        """

        if 'repo' in kwargs:
            raise TypeError("argument 'repo' conflicts with cloning")
            # TODO: what about 'create'?

        expect_fail = kwargs.pop('expect_fail', False)
        # fail early on non-empty target:
        from os import listdir
        if exists(path) and listdir(path):
            raise ValueError(
                "destination path '%s' already exists and is not an "
                "empty directory." % path)
        else:
            # protect against cloning into existing and obviously dangling
            # instance for that location
            try:
                del cls._unique_instances[path]
            except KeyError:
                # didn't exist - all fine
                pass

        # Massage URL
        url_ri = RI(url) if not isinstance(url, RI) else url
        if on_windows:
            # When we're cloning from a local path on Windows, the URL at
            # this point is platform-specific (e.g., "..\\origin"). According 
            # to Git clone's manpage, clone urls can't have backslashes.
            # While Git does manage to clone a URL with backslashes, 
            # in the case of subdatasets cloned from relative paths it nevertheless
            # messed up the resulting remote url, resulting in a mix of
            # front and backslashes (see also gh-7180): 
            # 'C:/Users/adina/AppData/Local/Temp/datalad_temp_frvczceh/ds/..\\origin' 
            # Therefore, we're turning it to Posix now.
            if isinstance(url_ri, PathRI):
                url = Path(url).as_posix()
                url_ri = PathRI(url)

        else:
            # if we are on windows, the local path of a URL
            # would not end up being a proper local path and cloning
            # would fail. Don't try to be smart and just pass the
            # URL along unmodified

            # try to get a local path from `url`:
            try:
                url = url_ri.localpath
                url_ri = RI(url)
            except ValueError:
                pass

        if is_ssh(url_ri):
            ssh_manager.get_connection(url).open()
        else:
            if isinstance(url_ri, PathRI):
                # expand user, because execution not going through a shell
                # doesn't work well otherwise
                new_url = os.path.expanduser(url)
                if url != new_url:
                    lgr.info("Expanded source path to %s from %s", new_url, url)
                    url = new_url

        cmd = cls._git_cmd_prefix + ['clone', '--progress']
        if clone_options:
            if isinstance(clone_options, Mapping):
                clone_options = to_options(**clone_options)
            cmd.extend(clone_options)
        cmd.extend([url, path])

        fix_annex = None
        ntries = 5  # 3 is not enough for robust workaround
        for trial in range(ntries):
            try:
                lgr.debug("Git clone from %s to %s", url, path)

                res = GitWitlessRunner().run(cmd, protocol=GitProgress)
                # fish out non-critical warnings by git-clone
                # (empty repo clone, etc.), all other content is logged
                # by the progress helper to 'debug'
                for errline in res['stderr'].splitlines():
                    if errline.startswith('warning:'):
                        lgr.warning(errline[8:].strip())
                lgr.debug("Git clone completed")
                break
            except CommandError as e:
                # log here but let caller decide what to do
                ce = CapturedException(e)
                str_e = str(e)
                # see https://github.com/datalad/datalad/issues/785
                if re.search("Request for .*aborted.*Unable to find", str_e,
                             re.DOTALL) \
                        and trial < ntries - 1:
                    lgr.info(
                        "Hit a known issue with Git (see GH#785). Trial #%d, "
                        "retrying",
                        trial)
                    continue
                    #(lgr.debug if expect_fail else lgr.error)(e_str)

                if "Clone succeeded, but checkout failed." in str_e:
                    fix_annex = ce
                    break

                raise

        # get ourselves a repository instance
        gr = cls(path, *args, **kwargs)
        if fix_annex:
            # cheap check whether we deal with an AnnexRepo - we can't check the class of `gr` itself, since we then
            # would need to import our own subclass
            if hasattr(gr, 'is_valid_annex'):
                lgr.warning("Experienced issues while cloning. "
                            "Trying to fix it, using git-annex-fsck.")
                if not gr.is_initialized():
                    gr._init()
                gr.fsck()
            else:
                lgr.warning("Experienced issues while cloning: %s", fix_annex)
        # ensure that Git doesn't mangle relative paths into obscure absolute
        # paths: https://github.com/datalad/datalad/issues/3538
        if isinstance(url_ri, PathRI):
            url_path = Path(url)
            if not url_path.is_absolute():
                # get git-created path
                remote_url = 'remote.' + gr.get_remotes()[0] + '.url'
                git_url = gr.config.get(remote_url)
                if Path(git_url).is_absolute():
                    # Git created an absolute path from a relative URL.
                    git_url = op.relpath(git_url, gr.path)
                path = Path(git_url)
                # always in POSIX even on Windows
                path = path.as_posix()
                gr.config.set(remote_url, path,
                              scope='local', force=True)
        return gr

    # Note: __del__ shouldn't be needed anymore as we switched to
    #       `weakref.finalize`.
    #       https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods
    #
    #       Keeping both methods and this comment around as a reminder to not
    #       use __del__, if we figure there's a need for cleanup in the future.
    #
    # def __del__(self):
    #     # unbind possibly bound ConfigManager, to prevent all kinds of weird
    #     # stalls etc
    #     self._cfg = None

    def is_valid_git(self):
        """Returns whether the underlying repository appears to be still valid

        Note, that this almost identical to the classmethod is_valid_repo().
        However, if we are testing an existing instance, we can save Path object
        creations. Since this testing is done a lot, this is relevant. Creation
        of the Path objects in is_valid_repo() takes nearly half the time of the
        entire function.

        Also note, that this method is bound to an instance but still
        class-dependent, meaning that a subclass cannot simply overwrite it.
        This is particularly important for the call from within __init__(),
        which in turn is called by the subclasses' __init__. Using an overwrite
        would lead to the wrong thing being called.
        """
        return self.is_valid()

    @classmethod
    def is_valid_repo(cls, path):
        """Returns if a given path points to a git repository"""
        return cls.is_valid(path)

    @staticmethod
    def get_git_dir(repo):
        """figure out a repo's gitdir

        '.git' might be a  directory, a symlink or a file

        Note
        ----
        This method is likely to get deprecated, please use GitRepo.dot_git instead!
        That one's not static, but it's cheaper and you should avoid
        not having an instance of a repo you're working on anyway.
        Note, that the property in opposition to this method returns an absolute path.


        Parameters
        ----------
        repo: path or Repo instance
          currently expected to be the repos base dir

        Returns
        -------
        str
          relative path to the repo's git dir; So, default would be ".git"
        """
        if isinstance(repo, GitRepo):
            return str(repo.dot_git)
        pathobj = Path(repo)
        dot_git = _get_dot_git(pathobj, ok_missing=False)
        try:
            dot_git = dot_git.relative_to(pathobj)
        except ValueError:
            # is not a subpath, return as is
            lgr.debug("Path %r is not subpath of %r", dot_git, pathobj)
        return str(dot_git)

    @property
    def config(self):
        # just proxy the core repo APIs property for backward-compatibility
        return self.cfg

    def is_with_annex(self):
        """Report if GitRepo (assumed) has (remotes with) a git-annex branch
        """
        return any(
            b['refname:strip=2'] == 'git-annex' or b['refname:strip=2'].endswith('/git-annex')
            for b in self.for_each_ref_(fields='refname:strip=2', pattern=['refs/heads', 'refs/remotes'])
        )

    @classmethod
    def get_toppath(cls, path, follow_up=True, git_options=None):
        """Return top-level of a repository given the path.

        Parameters
        -----------
        follow_up : bool
          If path has symlinks -- they get resolved by git.  If follow_up is
          True, we will follow original path up until we hit the same resolved
          path.  If no such path found, resolved one would be returned.
        git_options: list of str
          options to be passed to the git rev-parse call

        Return None if no parent directory contains a git repository.
        """
        cmd = ['git']
        if git_options:
            cmd.extend(git_options)
        cmd += ["rev-parse", "--show-toplevel"]
        try:
            out = GitWitlessRunner(cwd=path).run(
                cmd, protocol=StdOutErrCapture)
            toppath = out['stdout'].rstrip('\n\r')
        except CommandError:
            return None
        except OSError:
            toppath = GitRepo.get_toppath(dirname(path), follow_up=follow_up,
                                          git_options=git_options)

        # normalize the report, because, e.g. on windows it can come out
        # with improper directory separators (C:/Users/datalad)
        toppath = str(Path(toppath))

        if follow_up:
            path_ = path
            path_prev = ""
            while path_ and path_ != path_prev:  # on top /.. = /
                if str(Path(path_).resolve()) == toppath:
                    toppath = path_
                    break
                path_prev = path_
                path_ = dirname(path_)

        return toppath

    @normalize_paths
    def add(self, files, git=True, git_options=None, update=False):
        """Adds file(s) to the repository.

        Parameters
        ----------
        files: list
          list of paths to add
        git: bool
          somewhat ugly construction to be compatible with AnnexRepo.add();
          has to be always true.
        update: bool
          --update option for git-add. From git's manpage:
           Update the index just where it already has an entry matching
           <pathspec>. This removes as well as modifies index entries to match
           the working tree, but adds no new files.

           If no <pathspec> is given when --update option is used, all tracked
           files in the entire working tree are updated (old versions of Git
           used to limit the update to the current directory and its
           subdirectories).

        Returns
        -------
        list
          Of status dicts.
        """
        # under all circumstances call this class' add_ (otherwise
        # AnnexRepo.add would go into a loop
        return list(GitRepo.add_(self, files, git=git, git_options=git_options,
                    update=update))

    def add_(self, files, git=True, git_options=None, update=False):
        """Like `add`, but returns a generator"""
        # TODO: git_options is used as options for the git-add here,
        # instead of options to the git executable => rename for consistency

        if not git:
            lgr.warning(
                'GitRepo.add() called with git=%s, this should not happen',
                git)
            git = True

        # there is no other way then to collect all files into a list
        # at this point, because we need to pass them at once to a single
        # `git add` call
        files = [_normalize_path(self.path, f) for f in ensure_list(files) if f]

        if not (files or git_options or update):
            # wondering why just a warning? in cmdline this is also not an error
            lgr.warning("add was called with empty file list and no options.")
            return

        try:
            # without --verbose git 2.9.3  add does not return anything
            add_out = self._call_git(
                # Set annex.gitaddtoannex to prevent storing files in
                # annex with a v6+ annex repo.
                ['-c', 'annex.gitaddtoannex=false', 'add'] +
                ensure_list(git_options) +
                to_options(update=update) + ['--verbose'],
                files=files,
                pathspec_from_file=True,
                read_only=False,
            )
            # get all the entries
            for o in self._process_git_get_output(*add_out):
                yield o
            # Note: as opposed to git cmdline, force is True by default in
            #       gitpython, which would lead to add things, that are
            #       ignored or excluded otherwise
            # 2. Note: There is an issue with globbing (like adding '.'),
            #       which apparently doesn't care for 'force' and therefore
            #       adds '.git/...'. May be it's expanded at the wrong
            #       point in time or sth. like that.
            # For now, use direct call to git add.
            #self.cmd_call_wrapper(self.repo.index.add, files, write=True,
            #                      force=False)
            # TODO: May be make use of 'fprogress'-option to indicate
            # progress
            # But then, we don't have it for git-annex add, anyway.
            #
            # TODO: Is write=True a reasonable way to do it?
            # May be should not write until success of operation is
            # confirmed?
            # What's best in case of a list of files?
        except OSError as e:
            lgr.error("add: %s", e)
            raise

        # Make sure return value from GitRepo is consistent with AnnexRepo
        # currently simulating similar return value, assuming success
        # for all files:
        # TODO: Make return values consistent across both *Repo classes!
        return

    @staticmethod
    def _process_git_get_output(stdout, stderr=None):
        """Given both outputs (stderr is ignored atm) of git add - process it

        Primarily to centralize handling in both indirect annex and direct
        modes when ran through proxy
        """
        return [{u'file': f, u'success': True}
                for f in re.findall("'(.*)'[\n$]", ensure_unicode(stdout))]

    @normalize_paths(match_return_type=False)
    def remove(self, files, recursive=False, **kwargs):
        """Remove files.

        Calls git-rm.

        Parameters
        ----------
        files: list of str
          list of paths to remove
        recursive: False
          whether to allow recursive removal from subdirectories
        kwargs:
          see `__init__`

        Returns
        -------
        [str]
          list of successfully removed files.
        """
        if recursive:
            kwargs['r'] = True

        # the name is chosen badly, but the purpose is to make sure that
        # any pending operations actually manifest themselves in the Git repo
        # on disk (in case of an AnnexRepo, it could be pending batch
        # processes that need closing)
        self.precommit()

        # output per removed file is expected to be "rm 'PATH'":
        return [
            line.strip()[4:-1]
            for line in self.call_git_items_(
                ['rm'] + to_options(**kwargs), files=files, pathspec_from_file=True)
        ]

    def precommit(self):
        """Perform pre-commit maintenance tasks
        """
        # we used to clean up GitPython here
        pass

    @staticmethod
    def _get_prefixed_commit_msg(msg):
        DATALAD_PREFIX = "[DATALAD]"
        return DATALAD_PREFIX if not msg else "%s %s" % (DATALAD_PREFIX, msg)

    def configure_fake_dates(self):
        """Configure repository to use fake dates.
        """
        lgr.debug("Enabling fake dates")
        self.config.set("datalad.fake-dates", "true")

    @property
    def fake_dates_enabled(self):
        """Is the repository configured to use fake dates?
        """
        # this turned into a private property of the CoreGitRepo
        return self._fake_dates_enabled

    def add_fake_dates(self, env):
        # was renamed in CoreGitRepo
        return self.add_fake_dates_to_env(env)

    def commit(self, msg=None, options=None, _datalad_msg=False, careless=True,
               files=None, date=None, index_file=None):
        """Commit changes to git.

        Parameters
        ----------
        msg: str, optional
          commit-message
        options: list of str, optional
          cmdline options for git-commit
        _datalad_msg: bool, optional
          To signal that commit is automated commit by datalad, so
          it would carry the [DATALAD] prefix
        careless: bool, optional
          if False, raise when there's nothing actually committed;
          if True, don't care
        files: list of str, optional
          path(s) to commit
        date: str, optional
          Date in one of the formats git understands
        index_file: str, optional
          An alternative index to use
        """

        self.precommit()

        # assemble commandline
        cmd = ['commit']
        options = ensure_list(options)

        if date:
            options += ["--date", date]

        orig_msg = msg
        if not msg:
            if '--amend' in options:
                if '--no-edit' not in options:
                    # don't overwrite old commit message with our default
                    # message by default, but re-use old one. In other words:
                    # Make --no-edit the default:
                    options += ["--no-edit"]
            else:
                msg = 'Recorded changes'
                _datalad_msg = True

        if _datalad_msg:
            msg = self._get_prefixed_commit_msg(msg)

        if msg:
            options += ["-m", msg]
        cmd.extend(options)

        # set up env for commit
        env = self.add_fake_dates(None) \
            if self.fake_dates_enabled else os.environ.copy()
        if index_file:
            env['GIT_INDEX_FILE'] = index_file

        lgr.debug("Committing via direct call of git: %s", cmd)

        prev_sha = self.get_hexsha()

        # Old code was doing clever --amend'ing of chunked series of commits manually
        # here, but with pathspec_from_file it is no longer needed.
        # store pre-commit state to be able to check if anything was committed
        try:
            # Note: call_git operates via joining call_git_items_ and that one wipes out
            # .stdout from exception and collects/repopulates stderr only. Let's use
            # _call_git which returns both outputs and collects/re-populates both stdout
            # **and** stderr
            _ = self._call_git(
                    cmd,
                    files=files,
                    env=env,
                    pathspec_from_file=True,
            )
        except CommandError as e:
            # real errors first
            if "did not match any file(s) known to git" in e.stderr:
                raise FileNotInRepositoryError(
                    cmd=e.cmd,
                    msg="File(s) unknown to git",
                    code=e.code,
                    filename=linesep.join([
                        l for l in e.stderr.splitlines()
                        if l.startswith("error: pathspec")
                    ])
                )
            # behavior choices now
            elif not careless:
                # not willing to compromise at all
                raise
            elif 'nothing to commit' in e.stdout:
                lgr.debug("nothing to commit in %s. Ignored.", self)
            elif 'no changes added to commit' in e.stdout or \
                    'nothing added to commit' in e.stdout:
                lgr.debug("no changes added to commit in %s. Ignored.", self)
            else:
                raise
        if orig_msg \
                or '--dry-run' in cmd \
                or prev_sha == self.get_hexsha() \
                or ('--amend' in cmd and '--no-edit' in cmd) \
                or (not is_interactive()) \
                or self.config.obtain('datalad.save.no-message') != 'interactive':
            # we had a message given, or nothing was committed, or prev. commit
            # was amended, or we are not connected to a terminal, or no
            # interactive message input is desired:
            # we can go home
            return

        # handle interactive message entry by running another `git-commit`
        self._git_runner.run(
            cmd + ['--amend', '--edit'],
            protocol=NoCapture,
            stdin=None,
            env=env,
        )

    # TODO usage is only in the tests, consider making a test helper and
    # remove from GitRepo API
    def get_indexed_files(self):
        """Get a list of files in git's index

        Returns
        -------
        list
            list of paths rooting in git's base dir
        """

        return [
            str(r.relative_to(self.pathobj))
            for r in self.get_content_info(
                paths=None, ref=None, untracked='no')
        ]

    def format_commit(self, fmt, commitish=None):
        """Return `git show` output for `commitish`.

        Parameters
        ----------
        fmt : str
            A format string accepted by `git show`.
        commitish: str, optional
          Any commit identifier (defaults to "HEAD").

        Returns
        -------
        str or, if there are not commits yet, None.
        """
        # use git-log and not git-show due to faster performance with
        # complex commits (e.g. octopus merges)
        # https://github.com/datalad/datalad/issues/4801
        cmd = ['log', '-1', '-z', '--format=' + fmt]
        if commitish is not None:
            cmd.append(commitish + "^{commit}")
        # make sure Git takes our argument as a revision
        cmd.append('--')
        try:
            stdout = self.call_git(
                cmd, expect_stderr=True, expect_fail=True,
                read_only=True)
        except CommandError as e:
            if 'bad revision' in e.stderr:
                raise ValueError("Unknown commit identifier: %s" % commitish)
            elif 'does not have any commits yet' in e.stderr:
                return None
            else:
                raise e
        # This trailing null is coming from the -z above, which avoids the
        # newline that Git would append to the output. We could drop -z and
        # strip the newline directly, but then we'd have to worry about
        # compatibility across platforms.
        return stdout.rsplit("\0", 1)[0]

    def get_hexsha(self, commitish=None, short=False):
        """Return a hexsha for a given commitish.

        Parameters
        ----------
        commitish : str, optional
          Any identifier that refers to a commit (defaults to "HEAD").
        short : bool, optional
          Return the abbreviated form of the hexsha.

        Returns
        -------
        str or, if no commitish was given and there are no commits yet, None.

        Raises
        ------
        ValueError
          If a commitish was given, but no corresponding commit could be
          determined.
        """
        # use --quiet because the 'Needed a single revision' error message
        # that is the result of running this in a repo with no commits
        # isn't useful to report
        cmd = ['rev-parse', '--quiet', '--verify', '{}^{{commit}}'.format(
            commitish if commitish else 'HEAD')
        ]
        if short:
            cmd.append('--short')
        try:
            return self.call_git_oneline(cmd, read_only=True)
        except CommandError as e:
            if commitish is None:
                return None
            raise ValueError("Unknown commit identifier: %s" % commitish)

    @normalize_paths(match_return_type=False)
    def get_last_commit_hexsha(self, files):
        """Return the hash of the last commit the modified any of the given
        paths"""
        try:
            commit = self.call_git(
                ['rev-list', '-n1', 'HEAD'],
                files=files,
                expect_fail=True,
                read_only=True,
            )
            commit = commit.strip()
            return commit if commit else None
        except CommandError:
            if self.get_hexsha() is None:
                # unborn branch, don't freak out
                return None
            raise

    def get_revisions(self, revrange=None, fmt="%H", options=None):
        """Return list of revisions in `revrange`.

        Parameters
        ----------
        revrange : str or list of str or None, optional
            Revisions or revision ranges to walk. If None, revision defaults to
            HEAD unless a revision-modifying option like `--all` or
            `--branches` is included in `options`.
        fmt : string, optional
            Format accepted by `--format` option of `git log`. This should not
            contain new lines because the output is split on new lines.
        options : list of str, optional
            Options to pass to `git log`.  This should not include `--format`.

        Returns
        -------
        List of revisions (str), formatted according to `fmt`.
        """
        if revrange is None:
            revrange = []
        elif isinstance(revrange, str):
            revrange = [revrange]

        cmd = ["log", "--format={}".format(fmt)]
        cmd.extend((options or []) + revrange + ["--"])
        try:
            stdout = self.call_git(cmd, expect_fail=True, read_only=True)
        except CommandError as e:
            if "does not have any commits" in e.stderr:
                return []
            raise
        return stdout.splitlines()

    def commit_exists(self, commitish):
        """Does `commitish` exist in the repo?

        Parameters
        ----------
        commitish : str
            A commit or an object that can be dereferenced to one.

        Returns
        -------
        bool
        """
        # Note: The peeling operator "^{commit}" is required so that rev-parse
        # doesn't succeed if passed a full hexsha that is valid but doesn't
        # exist.
        return self.call_git_success(
            ["rev-parse", "--verify", commitish + "^{commit}"],
            read_only=True,
        )

    def get_merge_base(self, commitishes):
        """Get a merge base hexsha

        Parameters
        ----------
        commitishes: str or list of str
          List of commitishes (branches, hexshas, etc) to determine the merge
          base of. If a single value provided, returns merge_base with the
          current branch.

        Returns
        -------
        str or None
          If no merge-base for given commits, or specified treeish doesn't
          exist, None returned
        """
        if isinstance(commitishes, str):
            commitishes = [commitishes]
        if not commitishes:
            raise ValueError("Provide at least a single value")
        elif len(commitishes) == 1:
            commitishes = commitishes + [self.get_active_branch()]

        try:
            base = self.call_git_oneline(['merge-base'] + commitishes,
                                         read_only=True)
        except CommandError as exc:
            if exc.code == 1 and not (exc.stdout or exc.stderr):
                # No merge base was found (unrelated commits).
                return None
            if "fatal: Not a valid object name" in exc.stderr:
                return None
            raise

        return base

    def is_ancestor(self, reva, revb):
        """Is `reva` an ancestor of `revb`?

        Parameters
        ----------
        reva, revb : str
            Revisions.

        Returns
        -------
        bool
        """
        return self.call_git_success(
            ["merge-base", "--is-ancestor", reva, revb],
            read_only=True)

    def get_commit_date(self, branch=None, date='authored'):
        """Get the date stamp of the last commit (in a branch or head otherwise)

        Parameters
        ----------
        date: {'authored', 'committed'}
          Which date to return.  "authored" will be the date shown by "git show"
          and the one possibly specified via --date to `git commit`

        Returns
        -------
        int or None
          None if no commit
        """
        if date == 'committed':
            format = '%ct'
        elif date == 'authored':
            format = '%at'
        else:
            raise ValueError('unknow date type: {}'.format(date))
        d = self.format_commit(format, commitish=branch)
        return int(d) if d else None

    def get_active_branch(self):
        """Get the name of the active branch

        Returns
        -------
        str or None
          Returns None if there is no active branch, i.e. detached HEAD,
          and the branch name otherwise.
        """
        try:
            out = self.call_git(["symbolic-ref", "HEAD"], expect_fail=True,
                                read_only=True)
        except CommandError as e:
            if 'HEAD is not a symbolic ref' in e.stderr:
                lgr.debug("detached HEAD in %s", self)
                return None
            else:
                raise e
        return out.strip()[11:]  # strip refs/heads/

    def get_corresponding_branch(self, branch=None):
        """Always returns None, a plain GitRepo has no managed branches"""
        return None

    def get_branches(self):
        """Get all branches of the repo.

        Returns
        -------
        [str]
            Names of all branches of this repository.
        """

        return [
            b['refname:strip=2']
            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/heads')
        ]

    def get_remote_branches(self):
        """Get all branches of all remotes of the repo.

        Returns
        -----------
        [str]
            Names of all remote branches.
        """
        # TODO: Reconsider melting with get_branches()

        # TODO: treat entries like this: origin/HEAD -> origin/master'
        # currently this is done in collection

        return [
            b['refname:strip=2']
            for b in self.for_each_ref_(fields='refname:strip=2', pattern='refs/remotes')
        ]

    def get_remotes(self, with_urls_only=False):
        """Get known remotes of the repository

        Parameters
        ----------
        with_urls_only : bool, optional
          return only remotes which have urls

        Returns
        -------
        remotes : list of str
          List of names of the remotes
        """
        from datalad.utils import unique

        self.config.reload()
        remotes = unique([x[7:] for x in self.config.sections()
                          if x.startswith("remote.")])

        if with_urls_only:
            remotes = [
                r for r in remotes
                if self.config.get('remote.%s.url' % r)
            ]
        return remotes

    # TODO this is practically unused outside the tests, consider turning
    # into a test helper and trim from the API
    def get_files(self, branch=None):
        """Get a list of files in git.

        Lists the files in the (remote) branch.

        Parameters
        ----------
        branch: str
          Name of the branch to query. Default: active branch.

        Returns
        -------
        [str]
          list of files.
        """
        return [
            str(p.relative_to(self.pathobj))
            for p in self.get_content_info(
                paths=None, ref=branch, untracked='no')
            ]

    def add_remote(self, name, url, options=None):
        """Register remote pointing to a url
        """
        cmd = ['remote', 'add']
        if options:
            cmd += options
        cmd += [name, url]

        # for historical reasons this method returns stdout and
        # stderr, keeping that for now
        result = self._call_git(cmd)
        self.config.reload()
        return result

    def remove_remote(self, name):
        """Remove existing remote
        """

        # TODO: testing and error handling!
        from .exceptions import RemoteNotAvailableError
        try:
            self.call_git(['remote', 'remove', name])
        except CommandError as e:
            if 'No such remote' in e.stderr:
                raise RemoteNotAvailableError(name,
                                              cmd="git remote remove",
                                              msg="No such remote",
                                              stdout=e.stdout,
                                              stderr=e.stderr)
            else:
                raise e

        # config.reload necessary, because the associated remote config
        # will vanish
        self.config.reload()
        return

    def _maybe_open_ssh_connection(self, remote, prefer_push=True):
        """Open connection if `remote` has an SSH URL.

        Doing so enables SSH caching, preventing datalad-sshrun subprocesses
        from opening (and then closing) their own.

        Parameters
        ----------
        remote : str
        prefer_push : bool, optional
            Use `remote.<remote>.pushurl` if there is one, falling back to
            `remote.<remote>.url`.
        """
        if remote:
            url = None
            if prefer_push:
                url = self.get_remote_url(remote, push=True)
            url = url or self.get_remote_url(remote)
            if url and is_ssh(url):
                ssh_manager.get_connection(url).open()

    def update_remote(self, name=None, verbose=False):
        """
        """
        options = ["-v"] if verbose else []
        self._maybe_open_ssh_connection(name)
        name = [name] if name else []
        self.call_git(
            ['remote'] + name + ['update'] + options,
            expect_stderr=True
        )

    def fetch(self, remote=None, refspec=None, all_=False, git_options=None,
              **kwargs):
        """Fetches changes from a remote (or all remotes).

        Parameters
        ----------
        remote : str, optional
          name of the remote to fetch from. If no remote is given and
          `all_` is not set, the tracking branch is fetched.
        refspec : str or list, optional
          refspec(s) to fetch.
        all_ : bool, optional
          fetch all remotes (and all of their branches).
          Fails if `remote` was given.
        git_options : list, optional
          Additional command line options for git-fetch.
        kwargs :
          Deprecated. GitPython-style keyword argument for git-fetch.
          Will be appended to any git_options.
        """
        git_options = ensure_list(git_options)
        if kwargs:
            git_options.extend(to_options(**kwargs))
        return list(
            self.fetch_(
                remote=remote,
                refspec=refspec,
                all_=all_,
                git_options=git_options,
            )
        )

    def fetch_(self, remote=None, refspec=None, all_=False, git_options=None):
        """Like `fetch`, but returns a generator"""
        yield from self._fetch_push_helper(
            base_cmd=self._git_cmd_prefix + ['fetch', '--verbose', '--progress'],
            action='fetch',
            urlvars=('remote.{}.url', 'remote.{}.url'),
            protocol=GitProgress,
            info_cls=FetchInfo,
            info_from='stderr',
            add_remote=False,
            remote=remote,
            refspec=refspec,
            all_=all_,
            git_options=git_options)

    def push(self, remote=None, refspec=None, all_remotes=False,
             all_=False, git_options=None, **kwargs):
        """Push changes to a remote (or all remotes).

        If remote and refspec are specified, and remote has
        `remote.{remote}.datalad-push-default-first` configuration variable
        set (e.g. by `create-sibling-github`), we will first push the first
        refspec separately to possibly ensure that the first refspec is chosen
        by remote as the "default branch".
        See https://github.com/datalad/datalad/issues/4997
        Upon successful push if this variable was set in the local git config,
        we unset it, so subsequent pushes would proceed normally.

        Parameters
        ----------
        remote : str, optional
          name of the remote to push to. If no remote is given and
          `all_` is not set, the tracking branch is pushed.
        refspec : str or list, optional
          refspec(s) to push.
        all_ : bool, optional
          push to all remotes. Fails if `remote` was given.
        git_options : list, optional
          Additional command line options for git-push.
        kwargs :
          Deprecated. GitPython-style keyword argument for git-push.
          Will be appended to any git_options.
        """
        git_options = ensure_list(git_options)
        if kwargs:
            git_options.extend(to_options(**kwargs))
        if all_remotes:
            # be nice to the elderly
            all_ = True

        push_refspecs = [refspec]
        cfg = self.config  # shortcut
        cfg_push_var = "remote.{}.datalad-push-default-first".format(remote)
        if remote and refspec and cfg.obtain(cfg_push_var, default=False, valtype=bool):
            refspec = ensure_list(refspec)
            lgr.debug("As indicated by %s pushing first refspec %s separately first",
                      cfg_push_var, refspec[0])
            push_refspecs = [[refspec[0]], refspec[1:]]

        push_res = []
        for refspecs in push_refspecs:
            push_res.extend(
                self.push_(
                    remote=remote,
                    refspec=refspecs,
                    all_=all_,
                    git_options=git_options,
                )
            )
        # note: above push_ should raise exception if errors out
        if '--dry-run' not in git_options \
            and cfg.get_from_source('local', cfg_push_var) is not None:
            lgr.debug("Removing %s variable from local git config after successful push", cfg_push_var)
            cfg.unset(cfg_push_var, 'local')
        return push_res

    def push_(self, remote=None, refspec=None, all_=False, git_options=None):
        """Like `push`, but returns a generator"""
        yield from self._fetch_push_helper(
            base_cmd=self._git_cmd_prefix + ['push', '--progress', '--porcelain'],
            action='push',
            urlvars=('remote.{}.pushurl', 'remote.{}.url'),
            protocol=StdOutCaptureWithGitProgress,
            info_cls=PushInfo,
            info_from='stdout',
            add_remote=True,
            remote=remote,
            refspec=refspec,
            all_=all_,
            git_options=git_options)

    def _fetch_push_helper(
            self,
            base_cmd,     # arg list
            action,       # label fetch|push
            urlvars,      # variables to query for URLs
            protocol,     # processor for output
            info_cls,     # Push|FetchInfo
            info_from,    # stdout, stderr
            add_remote,   # whether to add a 'remote' field to the info dict
            remote=None, refspec=None, all_=False, git_options=None):

        git_options = ensure_list(git_options)

        cmd = base_cmd + git_options

        if remote is None:
            if refspec:
                # conflicts with using tracking branch or push all remotes
                # For now: Just fail.
                # TODO: May be check whether it fits to tracking branch
                raise ValueError(
                    "refspec specified without a remote. ({})".format(refspec))
            if all_:
                remotes_to_process = self.get_remotes(with_urls_only=True)
            else:
                # No explicit remote to fetch.
                # => get tracking branch:
                tb_remote, refspec = self.get_tracking_branch()
                if tb_remote is not None:
                    remotes_to_process = [tb_remote]
                else:
                    # No remote, no tracking branch
                    # => fail
                    raise ValueError(
                        "Neither a remote is specified to {} "
                        "from nor a tracking branch is set up.".format(action))
        else:
            if all_:
                raise ValueError(
                    "Option 'all_' conflicts with specified remote "
                    "'{}'.".format(remote))
            remotes_to_process = [remote]

        if refspec:
            # prep for appending to cmd
            refspec = ensure_list(refspec)

        # no need for progress report, when there is just one remote
        log_remote_progress = len(remotes_to_process) > 1
        if log_remote_progress:
            pbar_id = '{}remotes-{}'.format(action, id(self))
            log_progress(
                lgr.info,
                pbar_id,
                'Start %sing remotes for %s', action, self,
                total=len(remotes_to_process),
                label=action.capitalize(),
                unit=' Remotes',
            )
        try:
            for remote in remotes_to_process:
                r_cmd = cmd + [remote]
                if refspec:
                    r_cmd += refspec

                if log_remote_progress:
                    log_progress(
                        lgr.info,
                        pbar_id,
                        '{}ing remote %s'.format(action.capitalize()),
                        remote,
                        update=1,
                        increment=True,
                    )
                # best effort to enable SSH connection caching
                url = self.config.get(
                    # make two attempts to get a URL
                    urlvars[0].format(remote),
                    self.config.get(
                        urlvars[1].format(remote),
                        None)
                )
                if url and is_ssh(url):
                    ssh_manager.get_connection(url).open()
                try:
                    out = self._git_runner.run(
                        r_cmd,
                        protocol=protocol,
                    )
                    output = out[info_from] or ''
                except CommandError as e:
                    output = None
                    # intercept some errors that we express as an error report
                    # in the info dicts
                    if re.match(
                            '.*^error: failed to (push|fetch) some refs',
                            e.stderr,
                            re.DOTALL | re.MULTILINE):
                        output = getattr(e, info_from)
                        hints = ' '.join([l[6:] for l in e.stderr.splitlines()
                                          if l.startswith('hint: ')])
                        if output is None:
                            output = ''
                    if not output:
                        raise

                for line in output.splitlines():
                    try:
                        # push info doesn't identify a remote, add it here
                        pi = info_cls._from_line(line)
                        if add_remote:
                            pi['remote'] = remote
                        # There were errors, but Git provided hints
                        if 'error' in pi['operations']:
                            pi['hints'] = hints or None
                        yield pi
                    except Exception:
                        # it is not progress and no push info
                        # don't hide it completely
                        lgr.debug('git-%s reported: %s', action, line)
        finally:
            if log_remote_progress:
                log_progress(
                    lgr.info,
                    pbar_id,
                    'Finished %sing remotes for %s', action, self,
                )

    def get_remote_url(self, name, push=False):
        """Get the url of a remote.

        Reads the configuration of remote `name` and returns its url or None,
        if there is no url configured.

        Parameters
        ----------
        name: str
          name of the remote
        push: bool
          if True, get the pushurl instead of the fetch url.
        """

        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')
        return self.config.get(var, None)

    def set_remote_url(self, name, url, push=False):
        """Set the URL a remote is pointing to

        Sets the URL of the remote `name`. Requires the remote to already exist.

        Parameters
        ----------
        name: str
          name of the remote
        url: str
        push: bool
          if True, set the push URL, otherwise the fetch URL
        """

        var = 'remote.{0}.{1}'.format(name, 'pushurl' if push else 'url')
        self.config.set(var, url, scope='local', reload=True)

    def get_branch_commits_(self, branch=None, limit=None, stop=None):
        """Return commit hexshas for a branch

        Parameters
        ----------
        branch: str, optional
          If not provided, assumes current branch
        limit: None | 'left-only', optional
          Limit which commits to report.  If None -- all commits (merged or not),
          if 'left-only' -- only the commits from the left side of the tree upon
          merges
        stop: str, optional
          hexsha of the commit at which stop reporting (matched one is not
          reported either)

        Yields
        ------
        str
        """
        cmd = ['rev-list']
        if limit == 'left-only':
            cmd.append('--left-only')
        if not branch:
            branch = self.get_active_branch()
        cmd.append(branch)
        # and trailing -- marker to make sure that Git never confused the branch
        # with a potentially existing directory of the same name
        cmd.append('--')
        for r in self.call_git_items_(cmd):
            if stop and stop == r:
                return
            yield r

    def checkout(self, name, options=None):
        """
        """
        # TODO: May be check for the need of -b options herein?
        cmd = ['checkout']
        if options:
            cmd += options
        cmd += [str(name)]

        self.call_git(cmd, expect_stderr=True)
        # checkout can change committed config, or create branch config
        self.config.reload()

    # TODO: Before implementing annex merge, find usages and check for a needed
    # change to call super().merge
    def merge(self, name, options=None, msg=None, allow_unrelated=False, **kwargs):
        if options is None:
            options = []
        if msg:
            options = options + ["-m", msg]
        options += ['--allow-unrelated-histories']
        self.call_git(
            ['merge'] + options + [name],
            **kwargs
        )

    def remove_branch(self, branch):
        self.call_git(['branch', '-D', branch])

    def cherry_pick(self, commit):
        """Cherry pick `commit` to the current branch.

        Parameters
        ----------
        commit : str
            A single commit.
        """
        self.call_git(["cherry-pick", commit])

    @property
    def dirty(self):
        """Is the repository dirty?

        Note: This provides a quick answer when you simply want to know if
        there are any untracked changes or modifications in this repository or
        its submodules. For finer-grained control and more detailed reporting,
        use status() instead.
        """
        stdout = self.call_git(
            ["status", "--porcelain",
             # Ensure the result isn't influenced by status.showUntrackedFiles.
             "--untracked-files=normal",
             # Ensure the result isn't influenced by diff.ignoreSubmodules.
             "--ignore-submodules=none"])
        if bool(stdout.strip()):
            # The quick `git status`-based check can give a different answer
            # than `datalad status` for submodules on an adjusted branch.
            st = self.diffstatus(fr="HEAD" if self.get_hexsha() else None,
                                 to=None, untracked="normal")
            return any(r.get("state") != "clean" for r in st.values())
        return False

    @property
    def untracked_files(self):
        """Legacy interface, do not use! Use the status() method instead.

        Despite its name, it also reports on untracked datasets, and
        yields their names with trailing path separators.
        """
        return [
            '{}{}'.format(
                str(p.relative_to(self.pathobj)),
                os.sep if props['type'] != 'file' else ''
            )
            for p, props in self.status(
                    untracked='all', eval_submodule_state='no').items()
            if props.get('state', None) == 'untracked'
        ]

    def gc(self, allow_background=False, auto=False):
        """Perform house keeping (garbage collection, repacking)"""
        cmd_options = []
        if not allow_background:
            cmd_options += ['-c', 'gc.autodetach=0']
        cmd_options += ['gc', '--aggressive']
        if auto:
            cmd_options += ['--auto']
        self.call_git(cmd_options)

    def _parse_gitmodules(self):
        # TODO read .gitconfig from Git blob?
        gitmodules = self.pathobj / '.gitmodules'
        if not gitmodules.exists():
            return {}
        # pull out file content
        out = self.call_git(
            ['config', '-z', '-l', '--file', '.gitmodules'],
            read_only=True)
        # abuse our config parser
        # disable multi-value report, because we could not deal with them
        # anyways, and they should not appear in a normal .gitmodules file
        # but could easily appear when duplicates are included. In this case,
        # we better not crash
        db, _ = parse_gitconfig_dump(out, cwd=self.path, multi_value=False)
        mods = {}
        for k, v in db.items():
            if not k.startswith('submodule.'):
                # we don't know what this is
                lgr.warning("Skip unrecognized .gitmodule specification: %s=%s", k, v)
                continue
            k_l = k.split('.')
            # module name is everything after 'submodule.' that is not the variable
            # name
            mod_name = '.'.join(k_l[1:-1])
            mod = mods.get(mod_name, {})
            # variable name is the last 'dot-free' segment in the key
            mod[k_l[-1]] = v
            mods[mod_name] = mod

        out = {}
        # bring into traditional shape
        for name, props in mods.items():
            if 'path' not in props:
                lgr.warning("Failed to get '%s.path', skipping this submodule", name)
                continue
            modprops = {'gitmodule_{}'.format(k): v
                        for k, v in props.items()
                        if not k.startswith('__')}
            # Keep as PurePosixPath for possible normalization of / in the path etc
            modpath = PurePosixPath(props['path'])
            modprops['gitmodule_name'] = name
            out[modpath] = modprops
        return out

    def get_submodules_(self, paths=None):
        """Yield submodules in this repository.

        Parameters
        ----------
        paths : list(pathlib.PurePath), optional
            Restrict submodules to those under `paths`. Paths must be relative
            to the resolved repository root, and must be normed to match the
            reporting done by Git, i.e. no parent dir components
            (ala "some/../this").

        Returns
        -------
        A generator that yields a dictionary with information for each
        submodule.
        """
        if not (self.pathobj / ".gitmodules").exists():
            return

        modinfo = self._parse_gitmodules()
        if not modinfo:
            # we exit early, if there is nothing on record (even though
            # a .gitmodules file exists).
            # without this conditional exit, we would be able to discover
            # subprojects even when they are not recorded in .gitmodules,
            # but at the cost of running an unconstrained ls-files call
            # below
            return

        posix_mod_paths = [m.as_posix() for m in modinfo]
        if paths:
            # harmonize them into relative to the repository
            posix_paths = []
            for path in paths:
                path = ut.PurePath(path)
                if path.is_absolute():
                    try:
                        path = path.relative_to(self.pathobj)
                    except ValueError as exc:
                        lgr.debug(
                            "Path %s it not underneath %s, skipping since nothing should match it: %s",
                            path, self.pathobj, CapturedException(exc)
                        )
                        continue
                posix_paths.append(path.as_posix())

            # constrain the report by the given paths, make sure all paths are POSIX
            posix_mod_paths = list(get_filtered_paths_(
                posix_mod_paths,
                posix_paths,
                include_within_path=True,
            ))

        for r in self.call_git_items_(
            ['ls-files', '--stage', '-z'],
            sep='\0',
            files=posix_mod_paths,
            read_only=True,
            keep_ends=True,
        ):
            if not r.startswith('160000'):
                # make sure this method never talks about non-dataset
                # content
                continue
            props, rpath = r.split('\t')
            mode, gitsha, stage = props.split(' ')
            if stage not in ('0', '2'):
                # we either have non-merge situation, or a simple merge
                # situation (i.e. stage=0). the reported gitsha always
                # matches what we have locally.
                # or we are in a three-way merge, in which case stage=2
                # is what we want to report, because it matches the
                # current HEAD (see git-read-tree manpage).
                # there is either a stage 2 or stage 0, never both
                continue
            # remove the expected line separator from the path
            rpath = rpath[:-1]
            path = PurePosixPath(rpath)
            yield dict(
                path=self.pathobj / rpath, # full path returned here
                type='dataset',
                gitshasum=gitsha,
                **modinfo.get(path, {})
            )

    def get_submodules(self, sorted_=True, paths=None):
        """Return list of submodules.

        Parameters
        ----------
        sorted_ : bool, optional
            Sort submodules by path name.
        paths : list(pathlib.PurePath), optional
            Restrict submodules to those under `paths`.

        Returns
        -------
        List of submodule namedtuples if `compat` is true or otherwise a list
        of dictionaries as returned by `get_submodules_`.
        """
        xs = self.get_submodules_(paths=paths)

        if sorted_:
            xs = sorted(xs, key=lambda x: x["path"])
        return list(xs)

    def update_ref(self, ref, value, oldvalue=None, symbolic=False):
        """Update the object name stored in a ref "safely".

        Just a shim for `git update-ref` call if not symbolic, and
        `git symbolic-ref` if symbolic

        Parameters
        ----------
        ref : str
          Reference, such as `ref/heads/BRANCHNAME` or HEAD.
        value : str
          Value to update to, e.g. hexsha of a commit when updating for a
          branch ref, or branch ref if updating HEAD
        oldvalue: str
          Value to update from. Safeguard to be verified by git. This is only
          valid if `symbolic` is not True.
        symbolic : None
          To instruct if ref is symbolic, e.g. should be used in case of
          ref=HEAD
        """
        if symbolic:
            if oldvalue:
                raise ValueError("oldvalue and symbolic must not be given both")
            cmd = ['symbolic-ref', ref, value]
        else:
            cmd = ['update-ref', ref, value] + ([oldvalue] if oldvalue else [])
        self.call_git(cmd)

    def tag(self, tag, message=None, commit=None, options=None):
        """Tag a commit

        Parameters
        ----------
        tag : str
          Custom tag label. Must be a valid tag name.
        message : str, optional
          If provided, adds ['-m', <message>] to the list of `git tag`
          arguments.
        commit : str, optional
          If provided, will be appended as last argument to the `git tag` call,
          and can be used to identify the commit that shall be tagged, if
          not HEAD.
        options : list, optional
          Additional command options, inserted prior a potential `commit`
          argument.
        """
        # TODO: call in save.py complains about extensive logging. When does it
        # happen in what way? Figure out, whether to just silence it or raise or
        # whatever else.
        args = ['tag']
        if message:
            args += ['-m', message]
        if options is not None:
            args.extend(options)
        args.append(tag)
        if commit:
            args.append(commit)
        self.call_git(args)

    def get_tags(self, output=None):
        """Get list of tags

        Parameters
        ----------
        output : str, optional
          If given, limit the return value to a list of values matching that
          particular key of the tag properties.

        Returns
        -------
        list
          Each item is a dictionary with information on a tag. At present
          this includes 'hexsha', and 'name', where the latter is the string
          label of the tag, and the former the hexsha of the object the tag
          is attached to. The list is sorted by the creator date (committer
          date for lightweight tags and tagger date for annotated tags), with
          the most recent commit being the last element.
        """
        tags = [
            dict(
                name=t['refname:strip=2'],
                hexsha=t['object'] if t['object'] else t['objectname'],
            )
            for t in self.for_each_ref_(
                fields=['refname:strip=2', 'objectname', 'object'],
                pattern='refs/tags',
                sort='creatordate')
        ]
        if output:
            return [t[output] for t in tags]
        else:
            return tags

    def describe(self, commitish=None, **kwargs):
        """ Quick and dirty implementation to call git-describe

        Parameters
        ----------
        kwargs:
            transformed to cmdline options for git-describe;
            see __init__ for description of the transformation
        """
        # TODO: be more precise what failure to expect when and raise actual
        # errors
        cmd = ['describe'] + to_options(**kwargs)
        if commitish is not None:
            cmd.append(commitish)
        try:
            describe = self.call_git(cmd, expect_fail=True)
            return describe.strip()
        # TODO: WTF "catch everything"?
        except:
            return None

    def get_tracking_branch(self, branch=None, remote_only=False):
        """Get the tracking branch for `branch` if there is any.

        Parameters
        ----------
        branch: str
            local branch to look up. If none is given, active branch is used.
        remote_only : bool
            Don't return a value if the upstream remote is set to "." (meaning
            this repository).

        Returns
        -------
        tuple
            (remote or None, refspec or None) of the tracking branch
        """
        if branch is None:
            branch = self.get_corresponding_branch() or self.get_active_branch()
            if branch is None:
                return None, None

        track_remote = self.config.get('branch.{0}.remote'.format(branch), None)
        if remote_only and track_remote == ".":
            return None, None
        track_branch = self.config.get('branch.{0}.merge'.format(branch), None)
        return track_remote, track_branch

    @property
    def count_objects(self):
        """return dictionary with count, size(in KiB) information of git objects
        """

        count_cmd = ['count-objects', '-v']
        count_str = self.call_git(count_cmd)
        count = {key: int(value)
                 for key, value in [item.split(': ')
                                    for item in count_str.split('\n')
                                    if len(item.split(': ')) == 2]}
        return count

    def get_git_attributes(self):
        """Query gitattributes which apply to top level directory

        It is a thin compatibility/shortcut wrapper around more versatile
        get_gitattributes which operates on a list of paths and returns
        a dictionary per each path

        Returns
        -------
        dict:
          a dictionary with attribute name and value items relevant for the
          top ('.') directory of the repository, and thus most likely the
          default ones (if not overwritten with more rules) for all files within
          repo.
        """
        return self.get_gitattributes('.')['.']

    def get_gitattributes(self, path, index_only=False):
        """Query gitattributes for one or more paths

        Parameters
        ----------
        path: path or list
          Path(s) to query. Paths may be relative or absolute.
        index_only: bool
          Flag whether to consider only gitattribute setting that are reflected
          in the repository index, not just in the work tree content.

        Returns
        -------
        dict:
          Each key is a queried path (always relative to the repository root),
          each value is a dictionary with attribute
          name and value items. Attribute values are either True or False,
          for set and unset attributes, or are the literal attribute value.
        """
        path = ensure_list(path)
        cmd = ["check-attr", "-z", "--all"]
        if index_only:
            cmd.append('--cached')
        # make sure we have one entry for each query path to
        # simplify work with the result
        attributes = {p: {} for p in path}
        attr = []
        for item in self.call_git_items_(cmd, files=path, sep='\0',
                                         read_only=True):
            attr.append(item)
            if len(attr) < 3:
                continue
            # we have a full record
            p, name, value = attr
            attrs = attributes[p]
            attrs[name] = \
                True if value == 'set' else False if value == 'unset' else value
            # done, reset item
            attr = []
        return {relpath(k, self.path) if isabs(k) else k: v
                for k, v in attributes.items()}

    def set_gitattributes(self, attrs, attrfile='.gitattributes', mode='a'):
        """Set gitattributes

        By default appends additional lines to `attrfile`. Note, that later
        lines in `attrfile` overrule earlier ones, which may or may not be
        what you want. Set `mode` to 'w' to replace the entire file by
        what you provided in `attrs`.

        Parameters
        ----------
        attrs : list
          Each item is a 2-tuple, where the first element is a path pattern,
          and the second element is a dictionary with attribute key/value
          pairs. The attribute dictionary must use the same semantics as those
          returned by `get_gitattributes()`. Path patterns can use absolute paths,
          in which case they will be normalized relative to the directory
          that contains the target .gitattributes file (see `attrfile`).
        attrfile: path
          Path relative to the repository root of the .gitattributes file the
          attributes shall be set in.
        mode: str
          'a' to append .gitattributes, 'w' to replace it
        """

        git_attributes_file = op.join(self.path, attrfile)
        attrdir = op.dirname(git_attributes_file)
        if not op.exists(attrdir):
            os.makedirs(attrdir)

        with open(git_attributes_file, mode + '+') as f:
            # for append, fix existing files that do not end with \n
            if mode == 'a' and f.tell():
                f.seek(max(0, f.tell() - len(os.linesep)))
                if not f.read().endswith('\n'):
                    f.write('\n')

            for pattern, attr in sorted(attrs, key=lambda x: x[0]):
                # normalize the pattern relative to the target .gitattributes file
                npath = _normalize_path(
                    op.join(self.path, op.dirname(attrfile)), pattern)
                # paths in gitattributes always have to be POSIX
                npath = Path(npath).as_posix()
                attrline = u''
                if npath.count(' '):
                    # quote patterns with spaces
                    attrline += u'"{}"'.format(npath.replace('"', '\\"'))
                else:
                    attrline += npath
                for a in sorted(attr):
                    val = attr[a]
                    if val is True:
                        attrline += ' {}'.format(a)
                    elif val is False:
                        attrline += ' -{}'.format(a)
                    else:
                        attrline += ' {}={}'.format(a, val)
                f.write('{}\n'.format(attrline))

    def get_content_info(self, paths=None, ref=None, untracked='all'):
        """Get identifier and type information from repository content.

        This is simplified front-end for `git ls-files/tree`.

        Both commands differ in their behavior when queried about subdataset
        paths. ls-files will not report anything, ls-tree will report on the
        subdataset record. This function uniformly follows the behavior of
        ls-tree (report on the respective subdataset mount).

        Parameters
        ----------
        paths : list(pathlib.PurePath) or None
          Specific paths, relative to the resolved repository root, to query
          info for. Paths must be normed to match the reporting done by Git,
          i.e. no parent dir components (ala "some/../this").
          If `None`, info is reported for all content.
        ref : gitref or None
          If given, content information is retrieved for this Git reference
          (via ls-tree), otherwise content information is produced for the
          present work tree (via ls-files). With a given reference, the
          reported content properties also contain a 'bytesize' record,
          stating the size of a file in bytes.
        untracked : {'no', 'normal', 'all'}
          If and how untracked content is reported when no `ref` was given:
          'no': no untracked files are reported; 'normal': untracked files
          and entire untracked directories are reported as such; 'all': report
          individual files even in fully untracked directories.

        Returns
        -------
        dict
          Each content item has an entry under a pathlib `Path` object instance
          pointing to its absolute path inside the repository (this path is
          guaranteed to be underneath `Repo.path`).
          Each value is a dictionary with properties:

          `type`
            Can be 'file', 'symlink', 'dataset', 'directory'

          `gitshasum`
            SHASUM of the item as tracked by Git, or None, if not
            tracked. This could be different from the SHASUM of the file
            in the worktree, if it was modified.

        Raises
        ------
        ValueError
          In case of an invalid Git reference (e.g. 'HEAD' in an empty
          repository)
        """
        lgr.debug('%s.get_content_info(...)', self)
        # TODO limit by file type to replace code in subdatasets command
        info = dict()

        if paths:  # is not None separate after
            # path matching will happen against what Git reports
            # and Git always reports POSIX paths
            # any incoming path has to be relative already, so we can simply
            # convert unconditionally
            # note: will be list-ified below
            posix_paths = [ut.PurePath(p).as_posix() for p in paths]
        elif paths is not None:
            return info
        else:
            posix_paths = None

        if posix_paths and (not ref or external_versions["cmd:git"] >= "2.29.0"):
            # If a path points within a submodule, we need to map it to the
            # containing submodule before feeding it to ls-files or ls-tree.
            #
            # Before Git 2.29.0, ls-tree and ls-files differed in how they
            # reported paths within submodules: ls-files provided no output,
            # and ls-tree listed the submodule. Now they both return no output.
            submodules = [s["path"].relative_to(self.pathobj).as_posix()
                          for s in self.get_submodules_()]
            # `paths` get normalized into PurePosixPath above, submodules are POSIX as well
            posix_paths = get_parent_paths(posix_paths, submodules)

        # this will not work in direct mode, but everything else should be
        # just fine
        if not ref:
            # make sure no operations are pending before we figure things
            # out in the worktree
            self.precommit()

            # --exclude-standard will make sure to honor and standard way
            # git can be instructed to ignore content, and will prevent
            # crap from contaminating untracked file reports
            cmd = ['ls-files', '--stage', '-z']
            # untracked report mode, using labels from `git diff` option style
            if untracked == 'all':
                cmd += ['--exclude-standard', '-o']
            elif untracked == 'normal':
                cmd += ['--exclude-standard', '-o', '--directory', '--no-empty-directory']
            elif untracked == 'no':
                pass
            else:
                raise ValueError(
                    'unknown value for `untracked`: {}'.format(untracked))
            props_re = re.compile(
                r'(?P<type>[0-9]+) (?P<sha>.*) (.*)\t(?P<fname>.*)$')
        else:
            cmd = ['ls-tree', ref, '-z', '-r', '--full-tree', '-l']
            props_re = re.compile(
                r'(?P<type>[0-9]+) ([a-z]*) (?P<sha>[^ ]*) [\s]*(?P<size>[0-9-]+)\t(?P<fname>.*)$')

        lgr.debug('Query repo: %s', cmd)
        try:
            stdout = self.call_git(
                cmd,
                files=posix_paths,
                expect_fail=True,
                read_only=True)
        except CommandError as exc:
            if "fatal: Not a valid object name" in exc.stderr:
                raise InvalidGitReferenceError(ref)
            raise
        lgr.debug('Done query repo: %s', cmd)

        self._get_content_info_line_helper(
            ref,
            info,
            stdout.split('\0'),
            props_re)

        lgr.debug('Done %s.get_content_info(...)', self)
        return info

    def _get_content_info_line_helper(self, ref, info, lines, props_re):
        """Internal helper of get_content_info() to parse Git output"""
        mode_type_map = {
            '100644': 'file',
            '100755': 'file',
            '120000': 'symlink',
            '160000': 'dataset',
        }
        for line in lines:
            if not line:
                continue
            inf = {}
            props = props_re.match(line)
            if not props:
                # Kludge: Filter out paths starting with .git/ to work around
                # an `ls-files -o` bug that was fixed in Git 2.25.
                #
                # TODO: Drop this condition when GIT_MIN_VERSION is at least
                # 2.25.
                if line.startswith(".git/"):
                    lgr.debug("Filtering out .git/ file: %s", line)
                    continue
                # not known to Git, but Git always reports POSIX
                path = ut.PurePosixPath(line)
                inf['gitshasum'] = None
            else:
                # again Git reports always in POSIX
                path = ut.PurePosixPath(props.group('fname'))

            # revisit the file props after this path has not been rejected
            if props:
                inf['gitshasum'] = props.group('sha')
                inf['type'] = mode_type_map.get(
                    props.group('type'), props.group('type'))

                if ref and inf['type'] == 'file':
                    inf['bytesize'] = int(props.group('size'))

            # join item path with repo path to get a universally useful
            # path representation with auto-conversion and tons of other
            # stuff
            path = self.pathobj.joinpath(path)
            if 'type' not in inf:
                # be nice and assign types for untracked content
                inf['type'] = 'symlink' if path.is_symlink() \
                    else 'directory' if path.is_dir() else 'file'
            info[path] = inf

    def status(self, paths=None, untracked='all', eval_submodule_state='full'):
        """Simplified `git status` equivalent.

        Parameters
        ----------
        paths : list or None
          If given, limits the query to the specified paths. To query all
          paths specify `None`, not an empty list. If a query path points
          into a subdataset, a report is made on the subdataset record
          within the queried dataset only (no recursion).
        untracked : {'no', 'normal', 'all'}
          If and how untracked content is reported:
          'no': no untracked files are reported; 'normal': untracked files
          and entire untracked directories are reported as such; 'all': report
          individual files even in fully untracked directories.
        eval_submodule_state : {'full', 'commit', 'no'}
          If 'full' (the default), the state of a submodule is evaluated by
          considering all modifications, with the treatment of untracked files
          determined by `untracked`. If 'commit', the modification check is
          restricted to comparing the submodule's HEAD commit to the one
          recorded in the superdataset. If 'no', the state of the subdataset is
          not evaluated.

        Returns
        -------
        dict
          Each content item has an entry under a pathlib `Path` object instance
          pointing to its absolute path inside the repository (this path is
          guaranteed to be underneath `Repo.path`).
          Each value is a dictionary with properties:

          `type`
            Can be 'file', 'symlink', 'dataset', 'directory'
          `state`
            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.
        """
        lgr.debug('Query status of %r for %s paths',
                  self, len(paths) if paths is not None else 'all')
        return self.diffstatus(
            fr='HEAD' if self.get_hexsha() else None,
            to=None,
            paths=paths,
            untracked=untracked,
            eval_submodule_state=eval_submodule_state)

    def diff(self, fr, to, paths=None, untracked='all',
             eval_submodule_state='full'):
        """Like status(), but reports changes between to arbitrary revisions

        Parameters
        ----------
        fr : str or None
          Revision specification (anything that Git understands). Passing
          `None` considers anything in the target state as new.
        to : str or None
          Revision specification (anything that Git understands), or None
          to compare to the state of the work tree.
        paths : list or None
          If given, limits the query to the specified paths. To query all
          paths specify `None`, not an empty list.
        untracked : {'no', 'normal', 'all'}
          If and how untracked content is reported when `to` is None:
          'no': no untracked files are reported; 'normal': untracked files
          and entire untracked directories are reported as such; 'all': report
          individual files even in fully untracked directories.
        eval_submodule_state : {'full', 'commit', 'no'}
          If 'full' (the default), the state of a submodule is evaluated by
          considering all modifications, with the treatment of untracked files
          determined by `untracked`. If 'commit', the modification check is
          restricted to comparing the submodule's HEAD commit to the one
          recorded in the superdataset. If 'no', the state of the subdataset is
          not evaluated.

        Returns
        -------
        dict
          Each content item has an entry under a pathlib `Path` object instance
          pointing to its absolute path inside the repository (this path is
          guaranteed to be underneath `Repo.path`).
          Each value is a dictionary with properties:

          `type`
            Can be 'file', 'symlink', 'dataset', 'directory'
          `state`
            Can be 'added', 'untracked', 'clean', 'deleted', 'modified'.
        """
        return {k: v for k, v in self.diffstatus(
            fr=fr, to=to, paths=paths,
            untracked=untracked,
            eval_submodule_state=eval_submodule_state).items()
            if v.get('state', None) != 'clean'}

    def diffstatus(self, fr, to, paths=None, untracked='all',
                   eval_submodule_state='full', _cache=None):
        """Like diff(), but reports the status of 'clean' content too.

        It supports an additional submodule evaluation state 'global'.
        If given, it will return a single 'modified'
        (vs. 'clean') state label for the entire repository, as soon as
        it can.
        """
        def _get_cache_key(label, paths, ref, untracked=None):
            return self.path, label, tuple(paths) if paths else None, \
                ref, untracked

        if _cache is None:
            _cache = {}

        if paths is not None:
            # at this point we must normalize paths to the form that
            # Git would report them, to easy matching later on
            paths = map(ut.Path, paths)
            paths = [
                p.relative_to(self.pathobj) if p.is_absolute() else p
                for p in paths
            ]

        # TODO report more info from get_content_info() calls in return
        # value, those are cheap and possibly useful to a consumer
        # we need (at most) three calls to git
        if to is None:
            # everything we know about the worktree, including os.stat
            # for each file
            key = _get_cache_key('ci', paths, None, untracked)
            if key in _cache:
                to_state = _cache[key]
            else:
                to_state = self.get_content_info(
                    paths=paths, ref=None, untracked=untracked)
                _cache[key] = to_state
            # we want Git to tell us what it considers modified and avoid
            # reimplementing logic ourselves
            key = _get_cache_key('mod', paths, None)
            if key in _cache:
                modified = _cache[key]
            else:
                # from Git 2.31.0 onwards ls-files has --deduplicate
                # by for backward compatibility keep doing deduplication here
                modified = set(
                    self.pathobj.joinpath(ut.PurePosixPath(p))
                    for p in self.call_git_items_(
                        # we must also look for deleted files, for the logic
                        # below to work. Only from Git 2.31.0 would they be
                        # included with `-m` alone
                        ['ls-files', '-z', '-m', '-d'],
                        # low-level code cannot handle pathobjs
                        files=[str(p) for p in paths] if paths is not None else None,
                        sep='\0',
                        read_only=True)
                    if p)
                _cache[key] = modified
        else:
            key = _get_cache_key('ci', paths, to)
            if key in _cache:
                to_state = _cache[key]
            else:
                to_state = self.get_content_info(paths=paths, ref=to)
                _cache[key] = to_state
            # we do not need worktree modification detection in this case
            modified = None
        # origin state
        key = _get_cache_key('ci', paths, fr)
        if key in _cache:
            from_state = _cache[key]
        else:
            if fr:
                from_state = self.get_content_info(paths=paths, ref=fr)
            else:
                # no ref means from nothing
                from_state = {}
            _cache[key] = from_state

        status = dict()
        for f, to_state_r in to_state.items():
            props = self._diffstatus_get_state_props(
                f,
                from_state.get(f, None),
                to_state_r,
                # are we comparing against a recorded commit or the worktree
                to is not None,
                # if we have worktree modification info, report if
                # path is reported as modified in it
                modified and f in modified,
                eval_submodule_state)
            # potential early exit in "global" eval mode
            if eval_submodule_state == 'global' and \
                    props.get('state', None) not in ('clean', None):
                # any modification means globally 'modified'
                return 'modified'
            status[f] = props

        for f, from_state_r in from_state.items():
            if f not in to_state:
                # we new this, but now it is gone and Git is not complaining
                # about it being missing -> properly deleted and deletion
                # stages
                status[f] = dict(
                    state='deleted',
                    type=from_state_r['type'],
                    # report the shasum to distinguish from a plainly vanished
                    # file
                    gitshasum=from_state_r['gitshasum'],
                )
                if eval_submodule_state == 'global':
                    return 'modified'

        if to is not None or eval_submodule_state == 'no':
            # if we have `to` we are specifically comparing against
            # a recorded state, and this function only attempts
            # to label the state of a subdataset, not investigate
            # specifically what the changes in subdatasets are
            # this is done by a high-level command like rev-diff
            # so the comparison within this repo and the present
            # `state` label are all we need, and they are done already
            if eval_submodule_state == 'global':
                return 'clean'
            else:
                return status

        # loop over all subdatasets and look for additional modifications
        for f, st in status.items():
            f = str(f)
            if 'state' in st or not st['type'] == 'dataset':
                # no business here
                continue
            if not GitRepo.is_valid_repo(f):
                # submodule is not present, no chance for a conflict
                st['state'] = 'clean'
                continue
            # we have to recurse into the dataset and get its status
            subrepo = repo_from_path(f)
            # get the HEAD commit, or the one of the corresponding branch
            # only that one counts re super-sub relationship
            # save() syncs the corresponding branch each time
            subrepo_commit = subrepo.get_hexsha(subrepo.get_corresponding_branch())
            st['gitshasum'] = subrepo_commit
            # subdataset records must be labeled clean up to this point
            # test if current commit in subdataset deviates from what is
            # recorded in the dataset
            st['state'] = 'modified' \
                if st['prev_gitshasum'] != subrepo_commit \
                else 'clean'
            if eval_submodule_state == 'global' and st['state'] == 'modified':
                return 'modified'
            if eval_submodule_state == 'commit':
                continue
            # the recorded commit did not change, so we need to make
            # a more expensive traversal
            st['state'] = subrepo.diffstatus(
                # we can use 'HEAD' because we know that the commit
                # did not change. using 'HEAD' will facilitate
                # caching the result
                fr='HEAD',
                to=None,
                paths=None,
                untracked=untracked,
                eval_submodule_state='global',
                _cache=_cache) if st['state'] == 'clean' else 'modified'
            if eval_submodule_state == 'global' and st['state'] == 'modified':
                return 'modified'

        if eval_submodule_state == 'global':
            return 'clean'
        else:
            return status

    def _diffstatus_get_state_props(self, f, from_state, to_state,
                                    against_commit,
                                    modified_in_worktree,
                                    eval_submodule_state):
        """Helper to determine diff properties for a single path

        Parameters
        ----------
        f : Path
        from_state : dict
        to_state : dict
        against_commit : bool
          Flag whether `to_state` reflects a commit or the worktree.
        modified_in_worktree : bool
          Flag whether a worktree modification is reported. This is ignored
          when `against_commit` is True.
        eval_submodule_state : {'commit', 'no', ...}
        """
        if against_commit:
            # we can ignore any worktree modification reported when
            # comparing against a commit
            modified_in_worktree = False

        props = {}
        if 'type' in to_state:
            props['type'] = to_state['type']

        to_sha = to_state['gitshasum']
        from_sha = from_state['gitshasum'] if from_state else None

        # determine the state of `f` from from_state and to_state records, if
        # it can be determined conclusively from it. If not, it will
        # stay None for now
        state = None
        if not from_state:
            # this is new, or rather not known to the previous state
            state = 'added' if to_sha else 'untracked'
        elif to_sha == from_sha and not modified_in_worktree:
            # something that is seemingly unmodified, based on the info
            # gathered so far
            if to_state['type'] == 'dataset':
                if against_commit or eval_submodule_state == 'commit':
                    # we compare against a recorded state, just based on
                    # the shas we can be confident, otherwise the state
                    # of a subdataset isn't fully known yet, because
                    # `modified_in_worktree` will only reflect changes
                    # in the commit of a subdataset without looking into
                    # it for uncommitted changes. Such tests are done
                    # later and based on further conditionals for
                    # performance reasons
                    state = 'clean'
            else:
                # no change in git record, and no change on disk
                # at this point we know that the reported object ids
                # for this file are identical in the to and from
                # records.  If to is None, we're comparing to the
                # working tree and a deleted file will still have an
                # identical id, so we need to check whether the file is
                # gone before declaring it clean. This working tree
                # check is irrelevant and wrong if to is a ref.
                state = 'clean' \
                    if against_commit or (f.exists() or f.is_symlink()) \
                    else 'deleted'
        else:
            # change in git record, or on disk
            # for subdatasets leave the 'modified' judgement to the caller
            # for supporting corner cases, such as adjusted branch
            # which require inspection of a subdataset
            # TODO we could have a new file that is already staged
            # but had subsequent modifications done to it that are
            # unstaged. Such file would presently show up as 'added'
            # ATM I think this is OK, but worth stating...
            state = ('modified'
                     if against_commit or to_state['type'] != 'dataset'
                     else None
                    ) if f.exists() or f.is_symlink() else 'deleted'
            # TODO record before and after state for diff-like use
            # cases

        if state in ('clean', 'added', 'modified', None):
            # assign present gitsha to any record
            # state==None can only happen for subdatasets that
            # already existed, so also assign a sha for them
            props['gitshasum'] = to_sha
            if 'bytesize' in to_state:
                # if we got this cheap, report it
                props['bytesize'] = to_state['bytesize']
            elif state == 'clean' and 'bytesize' in from_state:
                # no change, we can take this old size info
                props['bytesize'] = from_state['bytesize']
        if state in ('clean', 'modified', 'deleted', None):
            # assign previous gitsha to any record
            # state==None can only happen for subdatasets that
            # already existed, so also assign a sha for them
            props['prev_gitshasum'] = from_sha
        if state:
            # only report a state if we could determine any
            # outside code tests for existence of the property
            # and not (always) for the value
            props['state'] = state
        return props

    def _save_pre(self, paths, _status, **kwargs):
        # helper to get an actionable status report
        if paths is not None and not paths and not _status:
            return
        if _status is None:
            if 'untracked' not in kwargs:
                kwargs['untracked'] = 'normal'
            status = self.status(
                paths=paths,
                **{k: kwargs[k] for k in kwargs
                   if k in ('untracked', 'eval_submodule_state')})
        else:
            # we want to be able to add items down the line
            # make sure to detach from prev. owner
            status = _status.copy()
        return status

    def get_staged_paths(self):
        """Returns a list of any stage repository path(s)

        This is a rather fast call, as it will not depend on what is going on
        in the worktree.
        """
        try:
            return list(self.call_git_items_(
                ['diff', '--name-only', '--staged'],
                expect_stderr=True))
        except CommandError as e:
            lgr.debug(CapturedException(e))
            return []

    def _save_post(self, message, files, partial_commit, amend=False,
                   allow_empty=False):
        # helper to commit changes reported in status

        # TODO remove pathobj stringification when commit() can
        # handle it
        to_commit = [str(f.relative_to(self.pathobj))
                     for f in files] \
                    if partial_commit else None
        if not partial_commit or to_commit or allow_empty or \
                (amend and message):
            # we directly call GitRepo.commit() to avoid a whole slew
            # if direct-mode safeguards and workarounds in the AnnexRepo
            # implementation (which also run an additional dry-run commit
            GitRepo.commit(
                self,
                files=to_commit,
                msg=message,
                options=to_options(amend=amend, allow_empty=allow_empty),
                # do not raise on empty commit
                # it could be that the `add` in this save-cycle has already
                # brought back a 'modified' file into a clean state
                careless=True,
            )

    def save(self, message=None, paths=None, _status=None, **kwargs):
        """Save dataset content.

        Parameters
        ----------
        message : str or None
          A message to accompany the changeset in the log. If None,
          a default message is used.
        paths : list or None
          Any content with path matching any of the paths given in this
          list will be saved. Matching will be performed against the
          dataset status (GitRepo.status()), or a custom status provided
          via `_status`. If no paths are provided, ALL non-clean paths
          present in the repo status or `_status` will be saved.
        _status : dict or None
          If None, Repo.status() will be queried for the given `ds`. If
          a dict is given, its content will be used as a constraint.
          For example, to save only modified content, but no untracked
          content, set `paths` to None and provide a `_status` that has
          no entries for untracked content.
        **kwargs :
          Additional arguments that are passed to underlying Repo methods.
          Supported:

          - git : bool (passed to Repo.add()
          - eval_submodule_state : {'full', 'commit', 'no'}
            passed to Repo.status()
          - untracked : {'no', 'normal', 'all'} - passed to Repo.status()
          - amend : bool (passed to GitRepo.commit)
        """
        return list(
            self.save_(
                message=message,
                paths=paths,
                _status=_status,
                **kwargs
            )
        )

    def save_(self, message=None, paths=None, _status=None, **kwargs):
        """Like `save()` but working as a generator."""
        from datalad.interface.results import get_status_dict

        status_state = _get_save_status_state(
            self._save_pre(paths, _status, **kwargs) or {}
        )
        amend = kwargs.get('amend', False)

        # TODO: check on those None's -- may be those are also "nothing to worry about"
        # and we could just return?
        if not any(status_state.values()) and not (message and amend):
            # all clean, nothing todo
            lgr.debug('Nothing to save in %r, exiting early', self)
            return

        # three things are to be done:
        # - remove (deleted if not already staged)
        # - add (modified/untracked)
        # - commit (with all paths that have been touched, to bypass
        #   potential pre-staged bits)

        staged_paths = self.get_staged_paths()
        need_partial_commit = bool(staged_paths)
        if need_partial_commit and hasattr(self, "call_annex"):
            # so we have some staged content. let's check which ones
            # are symlinks -- those could be annex key links that
            # are broken after a `git-mv` operation
            # https://github.com/datalad/datalad/issues/4967
            # call `git-annex pre-commit` on them to rectify this before
            # saving the wrong symlinks
            added = status_state['added']
            tofix = [
                sp for sp in staged_paths
                if added.get(self.pathobj / sp, {}).get("type") == "symlink"
            ]
            if tofix:
                self.call_annex(['pre-commit'], files=tofix)

        submodule_change = False

        if status_state['deleted']:
            vanished_subds = [
                str(f.relative_to(self.pathobj))
                for f, props in status_state['deleted'].items()
                if props.get('type') == 'dataset'
            ]
            if vanished_subds:
                # we submodule removal we use `git-rm`, because the clean-up
                # is more complex than just an index update -- make no
                # sense to have a duplicate implementation.
                # we do not yield here, but only altogether below -- we are just
                # processing gone components, should always be quick.
                self._call_git(['rm', '-q'], files=vanished_subds)
                submodule_change = True
            # remove anything from the index that was found to be gone
            self._call_git(
                ['update-index', '--remove'],
                files=[
                    str(f.relative_to(self.pathobj))
                    for f, props in status_state['deleted'].items()
                    # do not update the index, if there is already
                    # something staged for this path (e.g.,
                    # a directory was removed and a file staged
                    # in its place)
                    if not props.get('gitshasum')
                    # we already did the submodules
                    and props.get('type') != 'dataset'
                ]
            )
            # now yield all deletions
            for p, props in status_state['deleted'].items():
                yield get_status_dict(
                    action='delete',
                    refds=self.pathobj,
                    type=props.get('type'),
                    path=p,
                    status='ok',
                    logger=lgr)

        # TODO this additional query should not be, based on status as given
        # if anyhow possible, however, when paths are given, status may
        # not contain all required information. In case of path=None AND
        # _status=None, we should be able to avoid this, because
        # status should have the full info already
        # looks for contained repositories
        untracked_dirs = [
            f.relative_to(self.pathobj)
            for f, props in status_state['untracked'].items()
            if props.get('type', None) == 'directory']
        to_add_submodules = []
        if untracked_dirs:
            to_add_submodules = [
                sm for sm, sm_props in
                self.get_content_info(
                    untracked_dirs,
                    ref=None,
                    # request exhaustive list, so that everything that is
                    # still reported as a directory must be its own repository
                    untracked='all').items()
                if sm_props.get('type', None) == 'directory']
            to_add_submodules = _prune_deeper_repos(to_add_submodules)

        to_stage_submodules = {
            f: props
            for f, props in status_state['modified_or_untracked'].items()
            if props.get('type', None) == 'dataset'}
        if to_stage_submodules:
            lgr.debug(
                '%i submodule path(s) to stage in %r %s',
                len(to_stage_submodules), self,
                to_stage_submodules
                if len(to_stage_submodules) < 10 else '')
            to_add_submodules += list(to_stage_submodules)

        if to_add_submodules:
            for r in self._save_add_submodules(to_add_submodules):
                if r.get('status', None) == 'ok':
                    submodule_change = True
                yield r

        if submodule_change:
            # this will alter the config, reload
            self.config.reload()
            # need to include .gitmodules in what needs committing
            f = self.pathobj.joinpath('.gitmodules')
            status_state['modified_or_untracked'][f] = \
                status_state['modified'][f] = \
                dict(type='file', state='modified')
            # now stage .gitmodules
            self._call_git(['update-index', '--add'], files=['.gitmodules'])
            # and report on it
            yield get_status_dict(
                action='add',
                refds=self.pathobj,
                type='file',
                path=f,
                status='ok',
                logger=lgr)

        to_add = {
            # TODO remove pathobj stringification when add() can
            # handle it
            str(f.relative_to(self.pathobj)): props
            for f, props in status_state['modified_or_untracked'].items()
            if not (f in to_add_submodules or f in to_stage_submodules)}
        if to_add:
            compat_config = \
                self.config.obtain("datalad.save.windows-compat-warning")
            to_add, problems = self._check_for_win_compat(to_add, compat_config)
            lgr.debug(
                '%i path(s) to add to %s %s',
                len(to_add), self, to_add if len(to_add) < 10 else '')

            if to_add:
                yield from self._save_add(
                        to_add,
                        git_opts=None,
                        **{k: kwargs[k] for k in kwargs
                           if k in (('git',) if hasattr(self, 'uuid')
                                    else tuple())})
            if problems:
                from datalad.interface.results import get_status_dict
                msg = \
                    'Incompatible name for Windows systems; disable with ' \
                    'datalad.save.windows-compat-warning.',
                for path in problems:
                    yield get_status_dict(
                        action='save',
                        refds=self.pathobj,
                        type='file',
                        path=(self.pathobj / ut.PurePosixPath(path)),
                        status='impossible',
                        message=msg,
                        logger=lgr)


        # https://github.com/datalad/datalad/issues/6558
        # file could have become a directory. Unfortunately git
        # would then mistakenly refuse to commit if that old path is also
        # given to commit, so we better filter it out
        if status_state['deleted'] and status_state['added']:
            # check if any "deleted" is a directory now. Then for those
            # there should be some other path under that directory in 'added'
            for f in [_ for _ in status_state['deleted'] if _.is_dir()]:
                # this could potentially be expensive if lots of files become
                # directories, but it is unlikely to happen often
                # Note: PurePath.is_relative_to was added in 3.9 and seems slowish
                # path_is_subpath faster, also if comparing to "in f.parents"
                f_str = str(f)
                if any(path_is_subpath(str(f2), f_str) for f2 in status_state['added']):
                    status_state['deleted'].pop(f)  # do not bother giving it to commit below in _save_post

        # Note, that allow_empty is always ok when we amend. Required when we
        # amend an empty commit while the amendment is empty, too (though
        # possibly different message). If an empty commit was okay before, it's
        # okay now.
        status_state.pop('modified_or_untracked')  # pop the hybrid state
        self._save_post(message, chain(*status_state.values()), need_partial_commit, amend=amend,
                        allow_empty=amend)
        # TODO yield result for commit, prev helper checked hexsha pre
        # and post...

    def _check_for_win_compat(self, files, config):
        """Check file names for illegal characters or reserved names on Windows

        In the case that a non-Windows-compatible file is detected, warn users
        about potential interoperability issues.

        Parameters
        ----------
        files
          list of files to add
        config
          value of self.config.obtain("datalad.save.windows-compat-warning"),
          used to choose appropriate behavior. "none" performs no check,
          "warning" warns in case of incompatibilities, and "error" results in
          an error result in case of incompatibilities
        """
        # don't perform any check when the configuration is set to 'none'
        if config == 'none':
            return files, None

        from collections import defaultdict
        problems = defaultdict(list)
        for file in files:
            for part in Path(file).parts:
                # check every component of the path for incompatibilities
                if Path(part).stem.upper() in RESERVED_NAMES_WIN:
                    problems['Elements using a reserved filename:'].append(part)
                    problems['paths'].append(file)
                if re.search(ILLEGAL_CHARS_WIN, part):
                    problems['Elements with illegal characters:'].append(part)
                    problems['paths'].append(file)
                if part.endswith('.'):
                    problems['Elements ending with a dot:'].append(part)
                    problems['paths'].append(file)
                if part.endswith(' '):
                    problems['Elements ending with a space:'].append(part)
                    problems['paths'].append(file)
        if not problems:
            return files, None
        msg = \
            "Some elements of your dataset are not compatible with " \
            "Windows systems. Disable this check by changing " \
            "datalad.save.windows-compat-warning or consider renaming " \
            "the following elements: "
        for k, v in problems.items():
            # use the key as an explanation, and report filenames only once
            msg += f"\n{k} {[*{*v}]}" if k != 'paths' else ''
        if config == 'warning':
            lgr.warning(msg)
            return files, None

        elif config == 'error':
            # take the problematic files out of to_add
            for path in [*{*problems['paths']}]:
                files.pop(path)
            return files, [*{*problems['paths']}]

    def _save_add(self, files, git_opts=None):
        """Simple helper to add files in save()"""
        from datalad.interface.results import get_status_dict
        try:
            # without --verbose git 2.9.3  add does not return anything
            add_out = self._call_git(
                # Set annex.largefiles to prevent storing files in
                # annex with a v6+ annex repo.
                ['-c', 'annex.largefiles=nothing', 'add'] +
                ensure_list(git_opts) + ['--verbose'],
                files=list(files.keys()),
                pathspec_from_file=True,
            )
            # get all the entries
            for r in self._process_git_get_output(*add_out):
                yield get_status_dict(
                    action=r.get('command', 'add'),
                    refds=self.pathobj,
                    type='file',
                    path=(self.pathobj / ut.PurePosixPath(r['file']))
                    if 'file' in r else None,
                    status='ok' if r.get('success', None) else 'error',
                    key=r.get('key', None),
                    # while there is no git-annex underneath here, we
                    # tend to fake its behavior, so we can also support
                    # this type of messaging
                    message='\n'.join(r['error-messages'])
                    if 'error-messages' in r else None,
                    logger=lgr)
        except OSError as e:
            lgr.error("add: %s", e)
            raise

    def _save_add_submodules(self, paths):
        """Add new submodules, or updates records of existing ones

        This method does not use `git submodule add`, but aims to be more
        efficient by limiting the scope to mere in-place registration of
        multiple already present repositories.

        Parameters
        ----------
        paths : list(Path)

        Yields
        ------
        dict
          Result records
        """
        from datalad.interface.results import get_status_dict

        # first gather info from all datasets in read-only fashion, and then
        # update index, .gitmodules and .git/config at once
        info = []
        # To avoid adding already known: https://github.com/datalad/datalad/issues/6843
        # We must not add already known submodules explicitly since "untracked"
        # can be assigned even for known ones (TODO: add issue, might have been closed)?
        # Not sure if operating on relative paths would provide any speed up so use full
        known_sub_paths = {s['path'] for s in self.get_submodules_()}
        for path in paths:
            already_known = path in known_sub_paths
            rpath = str(path.relative_to(self.pathobj).as_posix())
            subm = repo_from_path(path)
            # if there is a corresponding branch, we want to record it's state.
            # we rely on the corresponding branch being synced already.
            # `save` should do that each time it runs.
            subm_commit = subm.get_hexsha(subm.get_corresponding_branch())
            if not subm_commit:
                yield get_status_dict(
                    action='add_submodule',
                    ds=self,
                    path=path,
                    status='error',
                    message=('cannot add subdataset %s with no commits', subm),
                    logger=lgr)
                continue
            # make an attempt to configure a submodule source URL based on the
            # discovered remote configuration
            remote, branch = subm.get_tracking_branch()
            url = subm.get_remote_url(remote) if remote else None
            if url is None:
                url = './{}'.format(rpath)
            subm_id = subm.config.get('datalad.dataset.id', None)
            info.append(
                dict(
                     # if we have additional information on this path, pass it on.
                     # if not, treat it as an untracked directory
                     paths[path] if isinstance(paths, dict)
                     else dict(type='directory', state='untracked'),
                     path=path, rpath=rpath, commit=subm_commit, id=subm_id,
                     url=url, known=already_known))

        # bypass any convenience or safe-manipulator for speed reasons
        # use case: saving many new subdatasets in a single run
        with (self.pathobj / '.gitmodules').open('a') as gmf, \
             (self.pathobj / '.git' / 'config').open('a') as gcf:
            for i in info:
                # we update the subproject commit unconditionally
                self.call_git([
                    'update-index', '--add', '--replace', '--cacheinfo', '160000',
                    i['commit'], i['rpath']
                ])
                # only write the .gitmodules/.config changes when this is not yet
                # a subdataset and not yet already known
                # TODO: we could update the URL, and branch info at this point,
                # even for previously registered subdatasets
                if not i['known'] and (i['type'] != 'dataset' or (
                        i['type'] == 'dataset' and i['state'] == 'untracked')):
                    gmprops = dict(path=i['rpath'], url=i['url'])
                    if i['id']:
                        gmprops['datalad-id'] = i['id']
                    write_config_section(
                        gmf, 'submodule', i['rpath'], gmprops)
                    write_config_section(
                        gcf, 'submodule', i['rpath'], dict(active='true', url=i['url']))

                # This mirrors the result structure yielded for
                # to_stage_submodules below.
                yield get_status_dict(
                    action='add',
                    refds=self.pathobj,
                    type='dataset',
                    key=None,
                    path=i['path'],
                    status='ok',
                    logger=lgr)


def _get_save_status_state(status):
    """
    Returns
    -------
    dict
      By status category by file path, mapped to status properties.
    """
    # Sort status into status by state with explicit list of states
    # (excluding clean we do not care about) we expect to be present
    # and which we know of (unless None), and modified_or_untracked hybrid
    # since it is used below
    status_state = {
        k: {}
        for k in (None,  # not cared of explicitly here
                  'added',  # not cared of explicitly here
                  # 'clean'  # not even wanted since nothing to do about those
                  'deleted',
                  'modified',
                  'untracked',
                  'modified_or_untracked',  # hybrid group created here
                  )}
    for f, props in status.items():
        state = props.get('state', None)
        if state == 'clean':
            # we don't care about clean
            continue
        if state == 'modified' and props.get('gitshasum') \
                and props.get('gitshasum') == props.get('prev_gitshasum'):
            # reported as modified, but with identical shasums -> typechange
            # a subdataset maybe? do increasingly expensive tests for
            # speed reasons
            if props.get('type') != 'dataset' and f.is_dir() \
                    and GitRepo.is_valid_repo(f):
                # it was not a dataset, but now there is one.
                # we declare it untracked to engage the discovery tooling.
                state = 'untracked'
                props = dict(type='dataset', state='untracked')
        status_state[state][f] = props
        # The hybrid one to retain the same order as in original status
        if state in ('modified', 'untracked'):
            status_state['modified_or_untracked'][f] = props
    return status_state


# used in in the get command and GitRepo.add_submodule(), the
# latter is not used outside the tests
def _fixup_submodule_dotgit_setup(ds, relativepath):
    """Implementation of our current of .git in a subdataset

    Each subdataset/module has its own .git directory where a standalone
    repository would have it. No gitdir files, no symlinks.
    """
    # move .git to superrepo's .git/modules, remove .git, create
    # .git-file
    path = opj(ds.path, relativepath)
    subds_dotgit = opj(path, ".git")

    repo = GitRepo(path, create=False)
    if repo.dot_git.parent == repo.pathobj:
        # this is what we want
        return

    # first we want to remove any conflicting worktree setup
    # done by git to find the checkout at the mountpoint of the
    # submodule, if we keep that, any git command will fail
    # after we move .git
    # Ben: Shouldn't we re-setup a possible worktree afterwards?
    repo.config.unset('core.worktree', scope='local')
    # what we have here is some kind of reference, remove and
    # replace by the target
    os.remove(subds_dotgit)
    # make absolute
    src_dotgit = str(repo.dot_git)
    # move .git
    from os import (
        listdir,
        rename,
        rmdir,
    )
    ensure_dir(subds_dotgit)
    for dot_git_entry in listdir(src_dotgit):
        rename(opj(src_dotgit, dot_git_entry),
               opj(subds_dotgit, dot_git_entry))
    assert not listdir(src_dotgit)
    rmdir(src_dotgit)


# try retro-fitting GitRepo with deprecated functionality
# must be done last in this file
try:
    from datalad_deprecated.gitrepo import DeprecatedGitRepoMethods
    for symbol in dir(DeprecatedGitRepoMethods):
        if symbol.startswith('__'):
            # ignore Python internals
            continue
        if hasattr(GitRepo, symbol):
            lgr.debug(
                'Not retro-fitted GitRepo with deprecated %s, '
                'name-space conflict', symbol)
            # do not override existing symbols
            continue
        # assign deprecated symbol to GitRepo
        setattr(GitRepo, symbol, getattr(DeprecatedGitRepoMethods, symbol))
        lgr.debug('Retro-fitted GitRepo with deprecated %s', symbol)
except ImportError as e:
    ce = CapturedException(e)
    lgr.debug(
        'Not retro-fitting GitRepo with deprecated symbols, '
        'datalad-deprecated package not found')

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user