{
    "file_diffs": [
        {
            "old_file_content": "",
            "new_file_content": "### 🐛 Bug Fixes\n\n- Fix save to not force committing into git if reference dataset is pure git (not git-annex).  Fixes [#7351](https://github.com/datalad/datalad/issues/7351) via [PR #7355](https://github.com/datalad/datalad/pull/7355) (by [@yarikoptic](https://github.com/yarikoptic))\n",
            "header": {
                "file": {
                    "path": "changelog.d/pr-7355.md"
                },
                "misc_line": "new file mode 100644"
            },
            "index_line": {
                "old_commit_hash": "000000000",
                "new_commit_hash": "3dcfae15f",
                "mode": ""
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "/dev/null"
            },
            "plus_file": {
                "path": "b/changelog.d/pr-7355.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": "- Fix save to not force committing into git if reference dataset is pure git (not git-annex).  Fixes [#7351](https://github.com/datalad/datalad/issues/7351) via [PR #7355](https://github.com/datalad/datalad/pull/7355) (by [@yarikoptic](https://github.com/yarikoptic))",
                                "type": "added"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [],
                    "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\"\"\"Interface to add content, and save modifications to a dataset\n\n\"\"\"\n\n__docformat__ = 'restructuredtext'\n\nimport logging\n\nfrom functools import partial\n\nfrom datalad.interface.base import (\n    Interface,\n    build_doc,\n    eval_results,\n)\nfrom datalad.interface.common_opts import (\n    jobs_opt,\n    recursion_limit,\n    recursion_flag,\n    save_message_opt,\n)\nfrom datalad.interface.utils import (\n    get_tree_roots,\n    discover_dataset_trace_to_targets,\n)\nfrom datalad.support.param import Parameter\nfrom datalad.support.constraints import (\n    EnsureStr,\n    EnsureNone,\n)\nfrom datalad.support.exceptions import CommandError\nfrom datalad.support.parallel import (\n    no_subds_in_futures,\n    ProducerConsumerProgressLog,\n)\nfrom datalad.utils import (\n    ensure_list,\n)\nimport datalad.utils as ut\n\nfrom datalad.distribution.dataset import (\n    Dataset,\n    EnsureDataset,\n    datasetmethod,\n    require_dataset,\n)\nfrom .status import (\n    Status,\n)\n\nlgr = logging.getLogger('datalad.core.local.save')\n\n\n@build_doc\nclass Save(Interface):\n    \"\"\"Save the current state of a dataset\n\n    Saving the state of a dataset records changes that have been made to it.\n    This change record is annotated with a user-provided description.\n    Optionally, an additional tag, such as a version, can be assigned to the\n    saved state. Such tag enables straightforward retrieval of past versions at\n    a later point in time.\n\n    .. note::\n      Before Git v2.22, any Git repository without an initial commit located\n      inside a Dataset is ignored, and content underneath it will be saved to\n      the respective superdataset. DataLad datasets always have an initial\n      commit, hence are not affected by this behavior.\n    \"\"\"\n    # note above documents that out behavior is like that of `git add`, but\n    # does not explicitly mention the connection to keep it simple.\n\n    _examples_ = [\n        dict(text=\"\"\"Save any content underneath the current directory, without\n             altering any potential subdataset\"\"\",\n             code_py=\"save(path='.')\",\n             code_cmd=\"datalad save .\"),\n        dict(text=\"\"\"Save specific content in the dataset\"\"\",\n             code_py=\"save(path='myfile.txt')\",\n             code_cmd=\"datalad save myfile.txt\"),\n        dict(text=\"\"\"Attach a commit message to save\"\"\",\n             code_py=\"save(path='myfile.txt', message='add file')\",\n             code_cmd=\"datalad save -m 'add file' myfile.txt\"),\n        dict(text=\"\"\"Save any content underneath the current directory, and\n             recurse into any potential subdatasets\"\"\",\n             code_py=\"save(path='.', recursive=True)\",\n             code_cmd=\"datalad save . -r\"),\n        dict(text=\"Save any modification of known dataset content in the \"\n                  \"current directory, but leave untracked files (e.g. temporary files) \"\n                  \"untouched\",\n             code_py=\"\"\"save(path='.', updated=True)\"\"\",\n             code_cmd=\"\"\"datalad save -u .\"\"\"),\n        dict(text=\"Tag the most recent saved state of a dataset\",\n             code_py=\"save(version_tag='bestyet')\",\n             code_cmd=\"datalad save --version-tag 'bestyet'\"),\n        dict(text=\"Save a specific change but integrate into last commit keeping \"\n                  \"the already recorded commit message\",\n             code_py=\"save(path='myfile.txt', amend=True)\",\n             code_cmd=\"datalad save myfile.txt --amend\")\n    ]\n\n    _params_ = dict(\n        dataset=Parameter(\n            args=(\"-d\", \"--dataset\"),\n            doc=\"\"\"\"specify the dataset to save\"\"\",\n            constraints=EnsureDataset() | EnsureNone()),\n        path=Parameter(\n            args=(\"path\",),\n            metavar='PATH',\n            doc=\"\"\"path/name of the dataset component to save. If given, only\n            changes made to those components are recorded in the new state.\"\"\",\n            nargs='*',\n            constraints=EnsureStr() | EnsureNone()),\n        message=save_message_opt,\n        message_file=Parameter(\n            args=(\"-F\", \"--message-file\"),\n            doc=\"\"\"take the commit message from this file. This flag is\n            mutually exclusive with -m.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        version_tag=Parameter(\n            args=(\"-t\", \"--version-tag\",),\n            metavar='ID',\n            doc=\"\"\"an additional marker for that state. Every dataset that\n            is touched will receive the tag.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        recursive=recursion_flag,\n        recursion_limit=recursion_limit,\n        updated=Parameter(\n            args=('-u', '--updated',),\n            action='store_true',\n            doc=\"\"\"if given, only saves previously tracked paths.\"\"\"),\n        to_git=Parameter(\n            args=(\"--to-git\",),\n            action='store_true',\n            doc=\"\"\"flag whether to add data directly to Git, instead of\n            tracking data identity only.  Use with caution, there is no\n            guarantee that a file put directly into Git like this will\n            not be annexed in a subsequent save operation.\n            If not specified, it will be up to git-annex to decide how\n            a file is tracked, based on a dataset's configuration\n            to track particular paths,\n            file types, or file sizes with either Git or git-annex.\n            (see https://git-annex.branchable.com/tips/largefiles).\n            \"\"\"),\n        jobs=jobs_opt,\n        amend=Parameter(\n            args=('--amend',),\n            action='store_true',\n            doc=\"\"\"if set, changes are not recorded in a new, separate\n            commit, but are integrated with the changeset of the previous\n            commit, and both together are recorded by replacing that\n            previous commit. This is mutually exclusive with recursive\n            operation.\n            \"\"\"),\n    )\n\n    @staticmethod\n    @datasetmethod(name='save')\n    @eval_results\n    def __call__(path=None,\n                 *,\n                 message=None, dataset=None,\n                 version_tag=None,\n                 recursive=False, recursion_limit=None,\n                 updated=False,\n                 message_file=None,\n                 to_git=None,\n                 jobs=None,\n                 amend=False,\n                 ):\n        if message and message_file:\n            raise ValueError(\n                \"Both a message and message file were specified for save()\")\n\n        if amend and recursive:\n            raise ValueError(\"Cannot amend a commit recursively.\")\n\n        path = ensure_list(path)\n\n        if message_file:\n            with open(message_file) as mfh:\n                message = mfh.read()\n\n        # we want 'normal' to achieve the most compact argument list\n        # for git calls\n        # untracked_mode = 'no' if updated else 'normal'\n        # TODO however, Repo.add() would refuse to add any dotfiles\n        # in a directory that is itself untracked, hence the only\n        # choice is to go with potentially crazy long lists\n        # until https://github.com/datalad/datalad/issues/1454\n        # has a resolution\n        untracked_mode = 'no' if updated else 'all'\n\n        # there are three basic scenarios:\n        # 1. save modifications to any already tracked content\n        # 2. save any content (including removal of deleted content)\n        #    to bring things to a clean state\n        # 3. like (2), but only operate on a given subset of content\n        #    identified by paths\n        # - all three have to work in conjunction with --recursive\n        # - the difference between (1) and (2) should be no more\n        #   that a switch from --untracked=no to --untracked=all\n        #   in Repo.save()\n\n        # we do not support\n        # - simultaneous operations on multiple datasets from disjoint\n        #   dataset hierarchies, hence a single reference dataset must be\n        #   identifiable from the either\n        #   - curdir or\n        #   - the `dataset` argument.\n        #   This avoids complex annotation loops and hierarchy tracking.\n        # - any modification upwards from the root dataset\n\n        ds = require_dataset(dataset, check_installed=True, purpose='save')\n\n        # use status() to do all discovery and annotation of paths\n        paths_by_ds = {}\n        for s in Status()(\n                # ATTN: it is vital to pass the `dataset` argument as it,\n                # and not a dataset instance in order to maintain the path\n                # semantics between here and the status() call\n                dataset=dataset,\n                path=path,\n                untracked=untracked_mode,\n                recursive=recursive,\n                recursion_limit=recursion_limit,\n                on_failure='ignore',\n                # for save without recursion only commit matters\n                eval_subdataset_state='full' if recursive else 'commit',\n                return_type='generator',\n                # this could be, but for now only 'error' results are handled\n                # below\n                #on_failure='ignore',\n                result_renderer='disabled'):\n            if s['status'] == 'error':\n                # Downstream code can't do anything with these. Let the caller\n                # decide their fate.\n                yield s\n                continue\n\n            # fish out status dict for this parent dataset\n            ds_status = paths_by_ds.get(s['parentds'], {})\n            # reassemble path status info as repo.status() would have made it\n            ds_status[ut.Path(s['path'])] = \\\n                {k: v for k, v in s.items()\n                 if k not in (\n                     'path', 'parentds', 'refds', 'status', 'action',\n                     'logger')}\n            paths_by_ds[s['parentds']] = ds_status\n\n        lgr.debug('Determined %i datasets for saving from input arguments',\n                  len(paths_by_ds))\n        # figure out what datasets to process, start with the ones containing\n        # the paths that were given as arguments\n        discovered_datasets = list(paths_by_ds.keys())\n        if dataset:\n            # if a reference dataset was given we want to save all the way up\n            # to it, so let's throw it into the mix\n            discovered_datasets.append(ds.path)\n        # sort the datasets into (potentially) disjoint hierarchies,\n        # or a single one, if a reference dataset was given\n        dataset_hierarchies = get_tree_roots(discovered_datasets)\n        for rootds, children in dataset_hierarchies.items():\n            edges = {}\n            discover_dataset_trace_to_targets(\n                rootds, children, [], edges, includeds=children)\n            for superds, subdss in edges.items():\n                superds_status = paths_by_ds.get(superds, {})\n                for subds in subdss:\n                    subds_path = ut.Path(subds)\n                    sub_status = superds_status.get(subds_path, {})\n                    if not (sub_status.get(\"state\") == \"clean\" and\n                            sub_status.get(\"type\") == \"dataset\"):\n                        # start from an entry that may already exist in the\n                        # status record\n                        superds_status[subds_path] = superds_status.get(\n                            subds_path,\n                            # if we got nothing yet:\n                            # shot from the hip, some status config\n                            # to trigger this specific super/sub\n                            # relation to be saved\n                            dict(state='untracked', type='dataset')\n                        )\n                paths_by_ds[superds] = superds_status\n\n        def save_ds(args, version_tag=None):\n            pdspath, paths = args\n\n            pds = Dataset(pdspath)\n            pds_repo = pds.repo\n            # pop status for this dataset, we are not coming back to it\n            pds_status = {\n                # for handing over to the low-level code, we recode any\n                # path relative to the real repo location, this avoid\n                # cumbersome symlink handling without context in the\n                # lower levels\n                pds_repo.pathobj / p.relative_to(pdspath): props\n                for p, props in paths.items()}\n            start_commit = pds_repo.get_hexsha()\n            if not all(p['state'] == 'clean' for p in pds_status.values()) or \\\n                    (amend and message):\n                for res in pds_repo.save_(\n                        message=message,\n                        # make sure to have the `path` arg be None, as we want\n                        # to prevent and bypass any additional repo.status()\n                        # calls\n                        paths=None,\n                        # prevent whining of GitRepo\n                        git=True if not hasattr(ds.repo, 'uuid')\n                        else to_git,\n                        # we are supplying the full status already, do not\n                        # detect anything else\n                        untracked='no',\n                        _status=pds_status,\n                        amend=amend):\n                    # TODO remove stringification when datalad-core can handle\n                    # path objects, or when PY3.6 is the lowest supported\n                    # version\n                    for k in ('path', 'refds'):\n                        if k in res:\n                            res[k] = str(\n                                # recode path back to dataset path anchor\n                                pds.pathobj / res[k].relative_to(\n                                    pds_repo.pathobj)\n                            )\n                    yield res\n            # report on the dataset itself\n            dsres = dict(\n                action='save',\n                type='dataset',\n                path=pds.path,\n                refds=ds.path,\n                status='ok'\n                if start_commit != pds_repo.get_hexsha()\n                else 'notneeded',\n                logger=lgr,\n            )\n            if not version_tag:\n                yield dsres\n                return\n            try:\n                # method requires str\n                version_tag = str(version_tag)\n                pds_repo.tag(version_tag)\n                dsres.update(\n                    status='ok',\n                    version_tag=version_tag)\n                yield dsres\n            except CommandError as e:\n                if dsres['status'] == 'ok':\n                    # first we yield the result for the actual save\n                    # TODO: we will get duplicate dataset/save record obscuring\n                    # progress reporting.  yoh thought to decouple \"tag\" from \"save\"\n                    # messages but was worrying that original authors would disagree\n                    yield dsres.copy()\n                # and now complain that tagging didn't work\n                dsres.update(\n                    status='error',\n                    message=('cannot tag this version: %s', e.stderr.strip()))\n                yield dsres\n\n        if not paths_by_ds:\n            # Special case: empty repo. There's either an empty commit only or\n            # none at all. An empty one we can amend otherwise there's nothing\n            # to do.\n            if amend and ds.repo.get_hexsha():\n                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)\n\n            else:\n                yield dict(action='save',\n                           type='dataset',\n                           path=ds.path,\n                           refds=ds.path,\n                           status='notneeded',\n                           logger=lgr)\n            return\n\n        # TODO: in principle logging could be improved to go not by a dataset\n        # but by path(s) within subdatasets. That should provide a bit better ETA\n        # and more \"dynamic\" feedback than jumpy datasets count.\n        # See addurls where it is implemented that way by providing agg and another\n        # log_filter\n        yield from ProducerConsumerProgressLog(\n            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),\n            partial(save_ds, version_tag=version_tag),\n            safe_to_consume=no_subds_in_futures,\n            producer_future_key=lambda ds_items: ds_items[0],\n            jobs=jobs,\n            log_filter=_log_filter_save_dataset,\n            unit=\"datasets\",\n            lgr=lgr,\n        )\n\n\ndef _log_filter_save_dataset(res):\n    return res.get('type') == 'dataset' and res.get('action') == 'save'\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\"\"\"Interface to add content, and save modifications to a dataset\n\n\"\"\"\n\n__docformat__ = 'restructuredtext'\n\nimport logging\n\nfrom functools import partial\n\nfrom datalad.interface.base import (\n    Interface,\n    build_doc,\n    eval_results,\n)\nfrom datalad.interface.common_opts import (\n    jobs_opt,\n    recursion_limit,\n    recursion_flag,\n    save_message_opt,\n)\nfrom datalad.interface.utils import (\n    get_tree_roots,\n    discover_dataset_trace_to_targets,\n)\nfrom datalad.support.param import Parameter\nfrom datalad.support.constraints import (\n    EnsureStr,\n    EnsureNone,\n)\nfrom datalad.support.exceptions import CommandError\nfrom datalad.support.parallel import (\n    no_subds_in_futures,\n    ProducerConsumerProgressLog,\n)\nfrom datalad.utils import (\n    ensure_list,\n)\nimport datalad.utils as ut\n\nfrom datalad.distribution.dataset import (\n    Dataset,\n    EnsureDataset,\n    datasetmethod,\n    require_dataset,\n)\nfrom .status import (\n    Status,\n)\n\nlgr = logging.getLogger('datalad.core.local.save')\n\n\n@build_doc\nclass Save(Interface):\n    \"\"\"Save the current state of a dataset\n\n    Saving the state of a dataset records changes that have been made to it.\n    This change record is annotated with a user-provided description.\n    Optionally, an additional tag, such as a version, can be assigned to the\n    saved state. Such tag enables straightforward retrieval of past versions at\n    a later point in time.\n\n    .. note::\n      Before Git v2.22, any Git repository without an initial commit located\n      inside a Dataset is ignored, and content underneath it will be saved to\n      the respective superdataset. DataLad datasets always have an initial\n      commit, hence are not affected by this behavior.\n    \"\"\"\n    # note above documents that out behavior is like that of `git add`, but\n    # does not explicitly mention the connection to keep it simple.\n\n    _examples_ = [\n        dict(text=\"\"\"Save any content underneath the current directory, without\n             altering any potential subdataset\"\"\",\n             code_py=\"save(path='.')\",\n             code_cmd=\"datalad save .\"),\n        dict(text=\"\"\"Save specific content in the dataset\"\"\",\n             code_py=\"save(path='myfile.txt')\",\n             code_cmd=\"datalad save myfile.txt\"),\n        dict(text=\"\"\"Attach a commit message to save\"\"\",\n             code_py=\"save(path='myfile.txt', message='add file')\",\n             code_cmd=\"datalad save -m 'add file' myfile.txt\"),\n        dict(text=\"\"\"Save any content underneath the current directory, and\n             recurse into any potential subdatasets\"\"\",\n             code_py=\"save(path='.', recursive=True)\",\n             code_cmd=\"datalad save . -r\"),\n        dict(text=\"Save any modification of known dataset content in the \"\n                  \"current directory, but leave untracked files (e.g. temporary files) \"\n                  \"untouched\",\n             code_py=\"\"\"save(path='.', updated=True)\"\"\",\n             code_cmd=\"\"\"datalad save -u .\"\"\"),\n        dict(text=\"Tag the most recent saved state of a dataset\",\n             code_py=\"save(version_tag='bestyet')\",\n             code_cmd=\"datalad save --version-tag 'bestyet'\"),\n        dict(text=\"Save a specific change but integrate into last commit keeping \"\n                  \"the already recorded commit message\",\n             code_py=\"save(path='myfile.txt', amend=True)\",\n             code_cmd=\"datalad save myfile.txt --amend\")\n    ]\n\n    _params_ = dict(\n        dataset=Parameter(\n            args=(\"-d\", \"--dataset\"),\n            doc=\"\"\"\"specify the dataset to save\"\"\",\n            constraints=EnsureDataset() | EnsureNone()),\n        path=Parameter(\n            args=(\"path\",),\n            metavar='PATH',\n            doc=\"\"\"path/name of the dataset component to save. If given, only\n            changes made to those components are recorded in the new state.\"\"\",\n            nargs='*',\n            constraints=EnsureStr() | EnsureNone()),\n        message=save_message_opt,\n        message_file=Parameter(\n            args=(\"-F\", \"--message-file\"),\n            doc=\"\"\"take the commit message from this file. This flag is\n            mutually exclusive with -m.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        version_tag=Parameter(\n            args=(\"-t\", \"--version-tag\",),\n            metavar='ID',\n            doc=\"\"\"an additional marker for that state. Every dataset that\n            is touched will receive the tag.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        recursive=recursion_flag,\n        recursion_limit=recursion_limit,\n        updated=Parameter(\n            args=('-u', '--updated',),\n            action='store_true',\n            doc=\"\"\"if given, only saves previously tracked paths.\"\"\"),\n        to_git=Parameter(\n            args=(\"--to-git\",),\n            action='store_true',\n            doc=\"\"\"flag whether to add data directly to Git, instead of\n            tracking data identity only.  Use with caution, there is no\n            guarantee that a file put directly into Git like this will\n            not be annexed in a subsequent save operation.\n            If not specified, it will be up to git-annex to decide how\n            a file is tracked, based on a dataset's configuration\n            to track particular paths,\n            file types, or file sizes with either Git or git-annex.\n            (see https://git-annex.branchable.com/tips/largefiles).\n            \"\"\"),\n        jobs=jobs_opt,\n        amend=Parameter(\n            args=('--amend',),\n            action='store_true',\n            doc=\"\"\"if set, changes are not recorded in a new, separate\n            commit, but are integrated with the changeset of the previous\n            commit, and both together are recorded by replacing that\n            previous commit. This is mutually exclusive with recursive\n            operation.\n            \"\"\"),\n    )\n\n    @staticmethod\n    @datasetmethod(name='save')\n    @eval_results\n    def __call__(path=None,\n                 *,\n                 message=None, dataset=None,\n                 version_tag=None,\n                 recursive=False, recursion_limit=None,\n                 updated=False,\n                 message_file=None,\n                 to_git=None,\n                 jobs=None,\n                 amend=False,\n                 ):\n        if message and message_file:\n            raise ValueError(\n                \"Both a message and message file were specified for save()\")\n\n        if amend and recursive:\n            raise ValueError(\"Cannot amend a commit recursively.\")\n\n        path = ensure_list(path)\n\n        if message_file:\n            with open(message_file) as mfh:\n                message = mfh.read()\n\n        # we want 'normal' to achieve the most compact argument list\n        # for git calls\n        # untracked_mode = 'no' if updated else 'normal'\n        # TODO however, Repo.add() would refuse to add any dotfiles\n        # in a directory that is itself untracked, hence the only\n        # choice is to go with potentially crazy long lists\n        # until https://github.com/datalad/datalad/issues/1454\n        # has a resolution\n        untracked_mode = 'no' if updated else 'all'\n\n        # there are three basic scenarios:\n        # 1. save modifications to any already tracked content\n        # 2. save any content (including removal of deleted content)\n        #    to bring things to a clean state\n        # 3. like (2), but only operate on a given subset of content\n        #    identified by paths\n        # - all three have to work in conjunction with --recursive\n        # - the difference between (1) and (2) should be no more\n        #   that a switch from --untracked=no to --untracked=all\n        #   in Repo.save()\n\n        # we do not support\n        # - simultaneous operations on multiple datasets from disjoint\n        #   dataset hierarchies, hence a single reference dataset must be\n        #   identifiable from the either\n        #   - curdir or\n        #   - the `dataset` argument.\n        #   This avoids complex annotation loops and hierarchy tracking.\n        # - any modification upwards from the root dataset\n\n        ds = require_dataset(dataset, check_installed=True, purpose='save')\n\n        # use status() to do all discovery and annotation of paths\n        paths_by_ds = {}\n        for s in Status()(\n                # ATTN: it is vital to pass the `dataset` argument as it,\n                # and not a dataset instance in order to maintain the path\n                # semantics between here and the status() call\n                dataset=dataset,\n                path=path,\n                untracked=untracked_mode,\n                recursive=recursive,\n                recursion_limit=recursion_limit,\n                on_failure='ignore',\n                # for save without recursion only commit matters\n                eval_subdataset_state='full' if recursive else 'commit',\n                return_type='generator',\n                # this could be, but for now only 'error' results are handled\n                # below\n                #on_failure='ignore',\n                result_renderer='disabled'):\n            if s['status'] == 'error':\n                # Downstream code can't do anything with these. Let the caller\n                # decide their fate.\n                yield s\n                continue\n\n            # fish out status dict for this parent dataset\n            ds_status = paths_by_ds.get(s['parentds'], {})\n            # reassemble path status info as repo.status() would have made it\n            ds_status[ut.Path(s['path'])] = \\\n                {k: v for k, v in s.items()\n                 if k not in (\n                     'path', 'parentds', 'refds', 'status', 'action',\n                     'logger')}\n            paths_by_ds[s['parentds']] = ds_status\n\n        lgr.debug('Determined %i datasets for saving from input arguments',\n                  len(paths_by_ds))\n        # figure out what datasets to process, start with the ones containing\n        # the paths that were given as arguments\n        discovered_datasets = list(paths_by_ds.keys())\n        if dataset:\n            # if a reference dataset was given we want to save all the way up\n            # to it, so let's throw it into the mix\n            discovered_datasets.append(ds.path)\n        # sort the datasets into (potentially) disjoint hierarchies,\n        # or a single one, if a reference dataset was given\n        dataset_hierarchies = get_tree_roots(discovered_datasets)\n        for rootds, children in dataset_hierarchies.items():\n            edges = {}\n            discover_dataset_trace_to_targets(\n                rootds, children, [], edges, includeds=children)\n            for superds, subdss in edges.items():\n                superds_status = paths_by_ds.get(superds, {})\n                for subds in subdss:\n                    subds_path = ut.Path(subds)\n                    sub_status = superds_status.get(subds_path, {})\n                    if not (sub_status.get(\"state\") == \"clean\" and\n                            sub_status.get(\"type\") == \"dataset\"):\n                        # start from an entry that may already exist in the\n                        # status record\n                        superds_status[subds_path] = superds_status.get(\n                            subds_path,\n                            # if we got nothing yet:\n                            # shot from the hip, some status config\n                            # to trigger this specific super/sub\n                            # relation to be saved\n                            dict(state='untracked', type='dataset')\n                        )\n                paths_by_ds[superds] = superds_status\n\n        def save_ds(args, version_tag=None):\n            pdspath, paths = args\n\n            pds = Dataset(pdspath)\n            pds_repo = pds.repo\n            # pop status for this dataset, we are not coming back to it\n            pds_status = {\n                # for handing over to the low-level code, we recode any\n                # path relative to the real repo location, this avoid\n                # cumbersome symlink handling without context in the\n                # lower levels\n                pds_repo.pathobj / p.relative_to(pdspath): props\n                for p, props in paths.items()}\n            start_commit = pds_repo.get_hexsha()\n            if not all(p['state'] == 'clean' for p in pds_status.values()) or \\\n                    (amend and message):\n                for res in pds_repo.save_(\n                        message=message,\n                        # make sure to have the `path` arg be None, as we want\n                        # to prevent and bypass any additional repo.status()\n                        # calls\n                        paths=None,\n                        # prevent whining of GitRepo\n                        git=True if not hasattr(pds_repo, 'uuid')\n                        else to_git,\n                        # we are supplying the full status already, do not\n                        # detect anything else\n                        untracked='no',\n                        _status=pds_status,\n                        amend=amend):\n                    # TODO remove stringification when datalad-core can handle\n                    # path objects, or when PY3.6 is the lowest supported\n                    # version\n                    for k in ('path', 'refds'):\n                        if k in res:\n                            res[k] = str(\n                                # recode path back to dataset path anchor\n                                pds.pathobj / res[k].relative_to(\n                                    pds_repo.pathobj)\n                            )\n                    yield res\n            # report on the dataset itself\n            dsres = dict(\n                action='save',\n                type='dataset',\n                path=pds.path,\n                refds=ds.path,\n                status='ok'\n                if start_commit != pds_repo.get_hexsha()\n                else 'notneeded',\n                logger=lgr,\n            )\n            if not version_tag:\n                yield dsres\n                return\n            try:\n                # method requires str\n                version_tag = str(version_tag)\n                pds_repo.tag(version_tag)\n                dsres.update(\n                    status='ok',\n                    version_tag=version_tag)\n                yield dsres\n            except CommandError as e:\n                if dsres['status'] == 'ok':\n                    # first we yield the result for the actual save\n                    # TODO: we will get duplicate dataset/save record obscuring\n                    # progress reporting.  yoh thought to decouple \"tag\" from \"save\"\n                    # messages but was worrying that original authors would disagree\n                    yield dsres.copy()\n                # and now complain that tagging didn't work\n                dsres.update(\n                    status='error',\n                    message=('cannot tag this version: %s', e.stderr.strip()))\n                yield dsres\n\n        if not paths_by_ds:\n            # Special case: empty repo. There's either an empty commit only or\n            # none at all. An empty one we can amend otherwise there's nothing\n            # to do.\n            if amend and ds.repo.get_hexsha():\n                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)\n\n            else:\n                yield dict(action='save',\n                           type='dataset',\n                           path=ds.path,\n                           refds=ds.path,\n                           status='notneeded',\n                           logger=lgr)\n            return\n\n        # TODO: in principle logging could be improved to go not by a dataset\n        # but by path(s) within subdatasets. That should provide a bit better ETA\n        # and more \"dynamic\" feedback than jumpy datasets count.\n        # See addurls where it is implemented that way by providing agg and another\n        # log_filter\n        yield from ProducerConsumerProgressLog(\n            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),\n            partial(save_ds, version_tag=version_tag),\n            safe_to_consume=no_subds_in_futures,\n            producer_future_key=lambda ds_items: ds_items[0],\n            jobs=jobs,\n            log_filter=_log_filter_save_dataset,\n            unit=\"datasets\",\n            lgr=lgr,\n        )\n\n\ndef _log_filter_save_dataset(res):\n    return res.get('type') == 'dataset' and res.get('action') == 'save'\n",
            "header": {
                "file": {
                    "path": "datalad/core/local/save.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "fc61a792d",
                "new_commit_hash": "903a0ce5b",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/datalad/core/local/save.py"
            },
            "plus_file": {
                "path": "b/datalad/core/local/save.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 314,
                            "length": 7
                        },
                        "new_range": {
                            "start": 314,
                            "length": 7
                        },
                        "section": "class Save(Interface):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                        # calls",
                                "type": "context"
                            },
                            {
                                "content": "                        paths=None,",
                                "type": "context"
                            },
                            {
                                "content": "                        # prevent whining of GitRepo",
                                "type": "context"
                            },
                            {
                                "content": "                        git=True if not hasattr(ds.repo, 'uuid')",
                                "type": "deleted"
                            },
                            {
                                "content": "                        git=True if not hasattr(pds_repo, 'uuid')",
                                "type": "added"
                            },
                            {
                                "content": "                        else to_git,",
                                "type": "context"
                            },
                            {
                                "content": "                        # we are supplying the full status already, do not",
                                "type": "context"
                            },
                            {
                                "content": "                        # detect anything else",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "datalad/core/local/save.py",
                            "type": "method",
                            "name": "Save.__call__",
                            "content": "class Save(Interface):\n    \"\"\"Save the current state of a dataset\n\n    Saving the state of a dataset records changes that have been made to it.\n    This change record is annotated with a user-provided description.\n    Optionally, an additional tag, such as a version, can be assigned to the\n    saved state. Such tag enables straightforward retrieval of past versions at\n    a later point in time.\n\n    .. note::\n      Before Git v2.22, any Git repository without an initial commit located\n      inside a Dataset is ignored, and content underneath it will be saved to\n      the respective superdataset. DataLad datasets always have an initial\n      commit, hence are not affected by this behavior.\n    \"\"\"\n    # note above documents that out behavior is like that of `git add`, but\n    # does not explicitly mention the connection to keep it simple.\n\n    _examples_ = [\n        dict(text=\"\"\"Save any content underneath the current directory, without\n             altering any potential subdataset\"\"\",\n             code_py=\"save(path='.')\",\n             code_cmd=\"datalad save .\"),\n        dict(text=\"\"\"Save specific content in the dataset\"\"\",\n             code_py=\"save(path='myfile.txt')\",\n             code_cmd=\"datalad save myfile.txt\"),\n        dict(text=\"\"\"Attach a commit message to save\"\"\",\n             code_py=\"save(path='myfile.txt', message='add file')\",\n             code_cmd=\"datalad save -m 'add file' myfile.txt\"),\n        dict(text=\"\"\"Save any content underneath the current directory, and\n             recurse into any potential subdatasets\"\"\",\n             code_py=\"save(path='.', recursive=True)\",\n             code_cmd=\"datalad save . -r\"),\n        dict(text=\"Save any modification of known dataset content in the \"\n                  \"current directory, but leave untracked files (e.g. temporary files) \"\n                  \"untouched\",\n             code_py=\"\"\"save(path='.', updated=True)\"\"\",\n             code_cmd=\"\"\"datalad save -u .\"\"\"),\n        dict(text=\"Tag the most recent saved state of a dataset\",\n             code_py=\"save(version_tag='bestyet')\",\n             code_cmd=\"datalad save --version-tag 'bestyet'\"),\n        dict(text=\"Save a specific change but integrate into last commit keeping \"\n                  \"the already recorded commit message\",\n             code_py=\"save(path='myfile.txt', amend=True)\",\n             code_cmd=\"datalad save myfile.txt --amend\")\n    ]\n\n    _params_ = dict(\n        dataset=Parameter(\n            args=(\"-d\", \"--dataset\"),\n            doc=\"\"\"\"specify the dataset to save\"\"\",\n            constraints=EnsureDataset() | EnsureNone()),\n        path=Parameter(\n            args=(\"path\",),\n            metavar='PATH',\n            doc=\"\"\"path/name of the dataset component to save. If given, only\n            changes made to those components are recorded in the new state.\"\"\",\n            nargs='*',\n            constraints=EnsureStr() | EnsureNone()),\n        message=save_message_opt,\n        message_file=Parameter(\n            args=(\"-F\", \"--message-file\"),\n            doc=\"\"\"take the commit message from this file. This flag is\n            mutually exclusive with -m.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        version_tag=Parameter(\n            args=(\"-t\", \"--version-tag\",),\n            metavar='ID',\n            doc=\"\"\"an additional marker for that state. Every dataset that\n            is touched will receive the tag.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        recursive=recursion_flag,\n        recursion_limit=recursion_limit,\n        updated=Parameter(\n            args=('-u', '--updated',),\n            action='store_true',\n            doc=\"\"\"if given, only saves previously tracked paths.\"\"\"),\n        to_git=Parameter(\n            args=(\"--to-git\",),\n            action='store_true',\n            doc=\"\"\"flag whether to add data directly to Git, instead of\n            tracking data identity only.  Use with caution, there is no\n            guarantee that a file put directly into Git like this will\n            not be annexed in a subsequent save operation.\n            If not specified, it will be up to git-annex to decide how\n            a file is tracked, based on a dataset's configuration\n            to track particular paths,\n            file types, or file sizes with either Git or git-annex.\n            (see https://git-annex.branchable.com/tips/largefiles).\n            \"\"\"),\n        jobs=jobs_opt,\n        amend=Parameter(\n            args=('--amend',),\n            action='store_true',\n            doc=\"\"\"if set, changes are not recorded in a new, separate\n            commit, but are integrated with the changeset of the previous\n            commit, and both together are recorded by replacing that\n            previous commit. This is mutually exclusive with recursive\n            operation.\n            \"\"\"),\n    )\n\n    @staticmethod\n    @datasetmethod(name='save')\n    @eval_results\n    def __call__(path=None,\n                 *,\n                 message=None, dataset=None,\n                 version_tag=None,\n                 recursive=False, recursion_limit=None,\n                 updated=False,\n                 message_file=None,\n                 to_git=None,\n                 jobs=None,\n                 amend=False,\n                 ):\n        if message and message_file:\n            raise ValueError(\n                \"Both a message and message file were specified for save()\")\n\n        if amend and recursive:\n            raise ValueError(\"Cannot amend a commit recursively.\")\n\n        path = ensure_list(path)\n\n        if message_file:\n            with open(message_file) as mfh:\n                message = mfh.read()\n\n        # we want 'normal' to achieve the most compact argument list\n        # for git calls\n        # untracked_mode = 'no' if updated else 'normal'\n        # TODO however, Repo.add() would refuse to add any dotfiles\n        # in a directory that is itself untracked, hence the only\n        # choice is to go with potentially crazy long lists\n        # until https://github.com/datalad/datalad/issues/1454\n        # has a resolution\n        untracked_mode = 'no' if updated else 'all'\n\n        # there are three basic scenarios:\n        # 1. save modifications to any already tracked content\n        # 2. save any content (including removal of deleted content)\n        #    to bring things to a clean state\n        # 3. like (2), but only operate on a given subset of content\n        #    identified by paths\n        # - all three have to work in conjunction with --recursive\n        # - the difference between (1) and (2) should be no more\n        #   that a switch from --untracked=no to --untracked=all\n        #   in Repo.save()\n\n        # we do not support\n        # - simultaneous operations on multiple datasets from disjoint\n        #   dataset hierarchies, hence a single reference dataset must be\n        #   identifiable from the either\n        #   - curdir or\n        #   - the `dataset` argument.\n        #   This avoids complex annotation loops and hierarchy tracking.\n        # - any modification upwards from the root dataset\n\n        ds = require_dataset(dataset, check_installed=True, purpose='save')\n\n        # use status() to do all discovery and annotation of paths\n        paths_by_ds = {}\n        for s in Status()(\n                # ATTN: it is vital to pass the `dataset` argument as it,\n                # and not a dataset instance in order to maintain the path\n                # semantics between here and the status() call\n                dataset=dataset,\n                path=path,\n                untracked=untracked_mode,\n                recursive=recursive,\n                recursion_limit=recursion_limit,\n                on_failure='ignore',\n                # for save without recursion only commit matters\n                eval_subdataset_state='full' if recursive else 'commit',\n                return_type='generator',\n                # this could be, but for now only 'error' results are handled\n                # below\n                #on_failure='ignore',\n                result_renderer='disabled'):\n            if s['status'] == 'error':\n                # Downstream code can't do anything with these. Let the caller\n                # decide their fate.\n                yield s\n                continue\n\n            # fish out status dict for this parent dataset\n            ds_status = paths_by_ds.get(s['parentds'], {})\n            # reassemble path status info as repo.status() would have made it\n            ds_status[ut.Path(s['path'])] = \\\n                {k: v for k, v in s.items()\n                 if k not in (\n                     'path', 'parentds', 'refds', 'status', 'action',\n                     'logger')}\n            paths_by_ds[s['parentds']] = ds_status\n\n        lgr.debug('Determined %i datasets for saving from input arguments',\n                  len(paths_by_ds))\n        # figure out what datasets to process, start with the ones containing\n        # the paths that were given as arguments\n        discovered_datasets = list(paths_by_ds.keys())\n        if dataset:\n            # if a reference dataset was given we want to save all the way up\n            # to it, so let's throw it into the mix\n            discovered_datasets.append(ds.path)\n        # sort the datasets into (potentially) disjoint hierarchies,\n        # or a single one, if a reference dataset was given\n        dataset_hierarchies = get_tree_roots(discovered_datasets)\n        for rootds, children in dataset_hierarchies.items():\n            edges = {}\n            discover_dataset_trace_to_targets(\n                rootds, children, [], edges, includeds=children)\n            for superds, subdss in edges.items():\n                superds_status = paths_by_ds.get(superds, {})\n                for subds in subdss:\n                    subds_path = ut.Path(subds)\n                    sub_status = superds_status.get(subds_path, {})\n                    if not (sub_status.get(\"state\") == \"clean\" and\n                            sub_status.get(\"type\") == \"dataset\"):\n                        # start from an entry that may already exist in the\n                        # status record\n                        superds_status[subds_path] = superds_status.get(\n                            subds_path,\n                            # if we got nothing yet:\n                            # shot from the hip, some status config\n                            # to trigger this specific super/sub\n                            # relation to be saved\n                            dict(state='untracked', type='dataset')\n                        )\n                paths_by_ds[superds] = superds_status\n\n        def save_ds(args, version_tag=None):\n            pdspath, paths = args\n\n            pds = Dataset(pdspath)\n            pds_repo = pds.repo\n            # pop status for this dataset, we are not coming back to it\n            pds_status = {\n                # for handing over to the low-level code, we recode any\n                # path relative to the real repo location, this avoid\n                # cumbersome symlink handling without context in the\n                # lower levels\n                pds_repo.pathobj / p.relative_to(pdspath): props\n                for p, props in paths.items()}\n            start_commit = pds_repo.get_hexsha()\n            if not all(p['state'] == 'clean' for p in pds_status.values()) or \\\n                    (amend and message):\n                for res in pds_repo.save_(\n                        message=message,\n                        # make sure to have the `path` arg be None, as we want\n                        # to prevent and bypass any additional repo.status()\n                        # calls\n                        paths=None,\n                        # prevent whining of GitRepo\n                        git=True if not hasattr(ds.repo, 'uuid')\n                        else to_git,\n                        # we are supplying the full status already, do not\n                        # detect anything else\n                        untracked='no',\n                        _status=pds_status,\n                        amend=amend):\n                    # TODO remove stringification when datalad-core can handle\n                    # path objects, or when PY3.6 is the lowest supported\n                    # version\n                    for k in ('path', 'refds'):\n                        if k in res:\n                            res[k] = str(\n                                # recode path back to dataset path anchor\n                                pds.pathobj / res[k].relative_to(\n                                    pds_repo.pathobj)\n                            )\n                    yield res\n            # report on the dataset itself\n            dsres = dict(\n                action='save',\n                type='dataset',\n                path=pds.path,\n                refds=ds.path,\n                status='ok'\n                if start_commit != pds_repo.get_hexsha()\n                else 'notneeded',\n                logger=lgr,\n            )\n            if not version_tag:\n                yield dsres\n                return\n            try:\n                # method requires str\n                version_tag = str(version_tag)\n                pds_repo.tag(version_tag)\n                dsres.update(\n                    status='ok',\n                    version_tag=version_tag)\n                yield dsres\n            except CommandError as e:\n                if dsres['status'] == 'ok':\n                    # first we yield the result for the actual save\n                    # TODO: we will get duplicate dataset/save record obscuring\n                    # progress reporting.  yoh thought to decouple \"tag\" from \"save\"\n                    # messages but was worrying that original authors would disagree\n                    yield dsres.copy()\n                # and now complain that tagging didn't work\n                dsres.update(\n                    status='error',\n                    message=('cannot tag this version: %s', e.stderr.strip()))\n                yield dsres\n\n        if not paths_by_ds:\n            # Special case: empty repo. There's either an empty commit only or\n            # none at all. An empty one we can amend otherwise there's nothing\n            # to do.\n            if amend and ds.repo.get_hexsha():\n                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)\n\n            else:\n                yield dict(action='save',\n                           type='dataset',\n                           path=ds.path,\n                           refds=ds.path,\n                           status='notneeded',\n                           logger=lgr)\n            return\n\n        # TODO: in principle logging could be improved to go not by a dataset\n        # but by path(s) within subdatasets. That should provide a bit better ETA\n        # and more \"dynamic\" feedback than jumpy datasets count.\n        # See addurls where it is implemented that way by providing agg and another\n        # log_filter\n        yield from ProducerConsumerProgressLog(\n            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),\n            partial(save_ds, version_tag=version_tag),\n            safe_to_consume=no_subds_in_futures,\n            producer_future_key=lambda ds_items: ds_items[0],\n            jobs=jobs,\n            log_filter=_log_filter_save_dataset,\n            unit=\"datasets\",\n            lgr=lgr,\n        )",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 168,
                            "end_lineno": 400,
                            "parent": {
                                "file_name": "datalad/core/local/save.py",
                                "type": "class",
                                "name": "Save",
                                "content": "class Save(Interface):\n    \"\"\"Save the current state of a dataset\n\n    Saving the state of a dataset records changes that have been made to it.\n    This change record is annotated with a user-provided description.\n    Optionally, an additional tag, such as a version, can be assigned to the\n    saved state. Such tag enables straightforward retrieval of past versions at\n    a later point in time.\n\n    .. note::\n      Before Git v2.22, any Git repository without an initial commit located\n      inside a Dataset is ignored, and content underneath it will be saved to\n      the respective superdataset. DataLad datasets always have an initial\n      commit, hence are not affected by this behavior.\n    \"\"\"\n    # note above documents that out behavior is like that of `git add`, but\n    # does not explicitly mention the connection to keep it simple.\n\n    _examples_ = [\n        dict(text=\"\"\"Save any content underneath the current directory, without\n             altering any potential subdataset\"\"\",\n             code_py=\"save(path='.')\",\n             code_cmd=\"datalad save .\"),\n        dict(text=\"\"\"Save specific content in the dataset\"\"\",\n             code_py=\"save(path='myfile.txt')\",\n             code_cmd=\"datalad save myfile.txt\"),\n        dict(text=\"\"\"Attach a commit message to save\"\"\",\n             code_py=\"save(path='myfile.txt', message='add file')\",\n             code_cmd=\"datalad save -m 'add file' myfile.txt\"),\n        dict(text=\"\"\"Save any content underneath the current directory, and\n             recurse into any potential subdatasets\"\"\",\n             code_py=\"save(path='.', recursive=True)\",\n             code_cmd=\"datalad save . -r\"),\n        dict(text=\"Save any modification of known dataset content in the \"\n                  \"current directory, but leave untracked files (e.g. temporary files) \"\n                  \"untouched\",\n             code_py=\"\"\"save(path='.', updated=True)\"\"\",\n             code_cmd=\"\"\"datalad save -u .\"\"\"),\n        dict(text=\"Tag the most recent saved state of a dataset\",\n             code_py=\"save(version_tag='bestyet')\",\n             code_cmd=\"datalad save --version-tag 'bestyet'\"),\n        dict(text=\"Save a specific change but integrate into last commit keeping \"\n                  \"the already recorded commit message\",\n             code_py=\"save(path='myfile.txt', amend=True)\",\n             code_cmd=\"datalad save myfile.txt --amend\")\n    ]\n\n    _params_ = dict(\n        dataset=Parameter(\n            args=(\"-d\", \"--dataset\"),\n            doc=\"\"\"\"specify the dataset to save\"\"\",\n            constraints=EnsureDataset() | EnsureNone()),\n        path=Parameter(\n            args=(\"path\",),\n            metavar='PATH',\n            doc=\"\"\"path/name of the dataset component to save. If given, only\n            changes made to those components are recorded in the new state.\"\"\",\n            nargs='*',\n            constraints=EnsureStr() | EnsureNone()),\n        message=save_message_opt,\n        message_file=Parameter(\n            args=(\"-F\", \"--message-file\"),\n            doc=\"\"\"take the commit message from this file. This flag is\n            mutually exclusive with -m.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        version_tag=Parameter(\n            args=(\"-t\", \"--version-tag\",),\n            metavar='ID',\n            doc=\"\"\"an additional marker for that state. Every dataset that\n            is touched will receive the tag.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        recursive=recursion_flag,\n        recursion_limit=recursion_limit,\n        updated=Parameter(\n            args=('-u', '--updated',),\n            action='store_true',\n            doc=\"\"\"if given, only saves previously tracked paths.\"\"\"),\n        to_git=Parameter(\n            args=(\"--to-git\",),\n            action='store_true',\n            doc=\"\"\"flag whether to add data directly to Git, instead of\n            tracking data identity only.  Use with caution, there is no\n            guarantee that a file put directly into Git like this will\n            not be annexed in a subsequent save operation.\n            If not specified, it will be up to git-annex to decide how\n            a file is tracked, based on a dataset's configuration\n            to track particular paths,\n            file types, or file sizes with either Git or git-annex.\n            (see https://git-annex.branchable.com/tips/largefiles).\n            \"\"\"),\n        jobs=jobs_opt,\n        amend=Parameter(\n            args=('--amend',),\n            action='store_true',\n            doc=\"\"\"if set, changes are not recorded in a new, separate\n            commit, but are integrated with the changeset of the previous\n            commit, and both together are recorded by replacing that\n            previous commit. This is mutually exclusive with recursive\n            operation.\n            \"\"\"),\n    )\n\n    @staticmethod\n    @datasetmethod(name='save')\n    @eval_results\n    def __call__(path=None,\n                 *,\n                 message=None, dataset=None,\n                 version_tag=None,\n                 recursive=False, recursion_limit=None,\n                 updated=False,\n                 message_file=None,\n                 to_git=None,\n                 jobs=None,\n                 amend=False,\n                 ):\n        if message and message_file:\n            raise ValueError(\n                \"Both a message and message file were specified for save()\")\n\n        if amend and recursive:\n            raise ValueError(\"Cannot amend a commit recursively.\")\n\n        path = ensure_list(path)\n\n        if message_file:\n            with open(message_file) as mfh:\n                message = mfh.read()\n\n        # we want 'normal' to achieve the most compact argument list\n        # for git calls\n        # untracked_mode = 'no' if updated else 'normal'\n        # TODO however, Repo.add() would refuse to add any dotfiles\n        # in a directory that is itself untracked, hence the only\n        # choice is to go with potentially crazy long lists\n        # until https://github.com/datalad/datalad/issues/1454\n        # has a resolution\n        untracked_mode = 'no' if updated else 'all'\n\n        # there are three basic scenarios:\n        # 1. save modifications to any already tracked content\n        # 2. save any content (including removal of deleted content)\n        #    to bring things to a clean state\n        # 3. like (2), but only operate on a given subset of content\n        #    identified by paths\n        # - all three have to work in conjunction with --recursive\n        # - the difference between (1) and (2) should be no more\n        #   that a switch from --untracked=no to --untracked=all\n        #   in Repo.save()\n\n        # we do not support\n        # - simultaneous operations on multiple datasets from disjoint\n        #   dataset hierarchies, hence a single reference dataset must be\n        #   identifiable from the either\n        #   - curdir or\n        #   - the `dataset` argument.\n        #   This avoids complex annotation loops and hierarchy tracking.\n        # - any modification upwards from the root dataset\n\n        ds = require_dataset(dataset, check_installed=True, purpose='save')\n\n        # use status() to do all discovery and annotation of paths\n        paths_by_ds = {}\n        for s in Status()(\n                # ATTN: it is vital to pass the `dataset` argument as it,\n                # and not a dataset instance in order to maintain the path\n                # semantics between here and the status() call\n                dataset=dataset,\n                path=path,\n                untracked=untracked_mode,\n                recursive=recursive,\n                recursion_limit=recursion_limit,\n                on_failure='ignore',\n                # for save without recursion only commit matters\n                eval_subdataset_state='full' if recursive else 'commit',\n                return_type='generator',\n                # this could be, but for now only 'error' results are handled\n                # below\n                #on_failure='ignore',\n                result_renderer='disabled'):\n            if s['status'] == 'error':\n                # Downstream code can't do anything with these. Let the caller\n                # decide their fate.\n                yield s\n                continue\n\n            # fish out status dict for this parent dataset\n            ds_status = paths_by_ds.get(s['parentds'], {})\n            # reassemble path status info as repo.status() would have made it\n            ds_status[ut.Path(s['path'])] = \\\n                {k: v for k, v in s.items()\n                 if k not in (\n                     'path', 'parentds', 'refds', 'status', 'action',\n                     'logger')}\n            paths_by_ds[s['parentds']] = ds_status\n\n        lgr.debug('Determined %i datasets for saving from input arguments',\n                  len(paths_by_ds))\n        # figure out what datasets to process, start with the ones containing\n        # the paths that were given as arguments\n        discovered_datasets = list(paths_by_ds.keys())\n        if dataset:\n            # if a reference dataset was given we want to save all the way up\n            # to it, so let's throw it into the mix\n            discovered_datasets.append(ds.path)\n        # sort the datasets into (potentially) disjoint hierarchies,\n        # or a single one, if a reference dataset was given\n        dataset_hierarchies = get_tree_roots(discovered_datasets)\n        for rootds, children in dataset_hierarchies.items():\n            edges = {}\n            discover_dataset_trace_to_targets(\n                rootds, children, [], edges, includeds=children)\n            for superds, subdss in edges.items():\n                superds_status = paths_by_ds.get(superds, {})\n                for subds in subdss:\n                    subds_path = ut.Path(subds)\n                    sub_status = superds_status.get(subds_path, {})\n                    if not (sub_status.get(\"state\") == \"clean\" and\n                            sub_status.get(\"type\") == \"dataset\"):\n                        # start from an entry that may already exist in the\n                        # status record\n                        superds_status[subds_path] = superds_status.get(\n                            subds_path,\n                            # if we got nothing yet:\n                            # shot from the hip, some status config\n                            # to trigger this specific super/sub\n                            # relation to be saved\n                            dict(state='untracked', type='dataset')\n                        )\n                paths_by_ds[superds] = superds_status\n\n        def save_ds(args, version_tag=None):\n            pdspath, paths = args\n\n            pds = Dataset(pdspath)\n            pds_repo = pds.repo\n            # pop status for this dataset, we are not coming back to it\n            pds_status = {\n                # for handing over to the low-level code, we recode any\n                # path relative to the real repo location, this avoid\n                # cumbersome symlink handling without context in the\n                # lower levels\n                pds_repo.pathobj / p.relative_to(pdspath): props\n                for p, props in paths.items()}\n            start_commit = pds_repo.get_hexsha()\n            if not all(p['state'] == 'clean' for p in pds_status.values()) or \\\n                    (amend and message):\n                for res in pds_repo.save_(\n                        message=message,\n                        # make sure to have the `path` arg be None, as we want\n                        # to prevent and bypass any additional repo.status()\n                        # calls\n                        paths=None,\n                        # prevent whining of GitRepo\n                        git=True if not hasattr(ds.repo, 'uuid')\n                        else to_git,\n                        # we are supplying the full status already, do not\n                        # detect anything else\n                        untracked='no',\n                        _status=pds_status,\n                        amend=amend):\n                    # TODO remove stringification when datalad-core can handle\n                    # path objects, or when PY3.6 is the lowest supported\n                    # version\n                    for k in ('path', 'refds'):\n                        if k in res:\n                            res[k] = str(\n                                # recode path back to dataset path anchor\n                                pds.pathobj / res[k].relative_to(\n                                    pds_repo.pathobj)\n                            )\n                    yield res\n            # report on the dataset itself\n            dsres = dict(\n                action='save',\n                type='dataset',\n                path=pds.path,\n                refds=ds.path,\n                status='ok'\n                if start_commit != pds_repo.get_hexsha()\n                else 'notneeded',\n                logger=lgr,\n            )\n            if not version_tag:\n                yield dsres\n                return\n            try:\n                # method requires str\n                version_tag = str(version_tag)\n                pds_repo.tag(version_tag)\n                dsres.update(\n                    status='ok',\n                    version_tag=version_tag)\n                yield dsres\n            except CommandError as e:\n                if dsres['status'] == 'ok':\n                    # first we yield the result for the actual save\n                    # TODO: we will get duplicate dataset/save record obscuring\n                    # progress reporting.  yoh thought to decouple \"tag\" from \"save\"\n                    # messages but was worrying that original authors would disagree\n                    yield dsres.copy()\n                # and now complain that tagging didn't work\n                dsres.update(\n                    status='error',\n                    message=('cannot tag this version: %s', e.stderr.strip()))\n                yield dsres\n\n        if not paths_by_ds:\n            # Special case: empty repo. There's either an empty commit only or\n            # none at all. An empty one we can amend otherwise there's nothing\n            # to do.\n            if amend and ds.repo.get_hexsha():\n                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)\n\n            else:\n                yield dict(action='save',\n                           type='dataset',\n                           path=ds.path,\n                           refds=ds.path,\n                           status='notneeded',\n                           logger=lgr)\n            return\n\n        # TODO: in principle logging could be improved to go not by a dataset\n        # but by path(s) within subdatasets. That should provide a bit better ETA\n        # and more \"dynamic\" feedback than jumpy datasets count.\n        # See addurls where it is implemented that way by providing agg and another\n        # log_filter\n        yield from ProducerConsumerProgressLog(\n            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),\n            partial(save_ds, version_tag=version_tag),\n            safe_to_consume=no_subds_in_futures,\n            producer_future_key=lambda ds_items: ds_items[0],\n            jobs=jobs,\n            log_filter=_log_filter_save_dataset,\n            unit=\"datasets\",\n            lgr=lgr,\n        )",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 63,
                                "end_lineno": 400,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "datalad/core/local/save.py",
                            "type": "class",
                            "name": "Save",
                            "content": "class Save(Interface):\n    \"\"\"Save the current state of a dataset\n\n    Saving the state of a dataset records changes that have been made to it.\n    This change record is annotated with a user-provided description.\n    Optionally, an additional tag, such as a version, can be assigned to the\n    saved state. Such tag enables straightforward retrieval of past versions at\n    a later point in time.\n\n    .. note::\n      Before Git v2.22, any Git repository without an initial commit located\n      inside a Dataset is ignored, and content underneath it will be saved to\n      the respective superdataset. DataLad datasets always have an initial\n      commit, hence are not affected by this behavior.\n    \"\"\"\n    # note above documents that out behavior is like that of `git add`, but\n    # does not explicitly mention the connection to keep it simple.\n\n    _examples_ = [\n        dict(text=\"\"\"Save any content underneath the current directory, without\n             altering any potential subdataset\"\"\",\n             code_py=\"save(path='.')\",\n             code_cmd=\"datalad save .\"),\n        dict(text=\"\"\"Save specific content in the dataset\"\"\",\n             code_py=\"save(path='myfile.txt')\",\n             code_cmd=\"datalad save myfile.txt\"),\n        dict(text=\"\"\"Attach a commit message to save\"\"\",\n             code_py=\"save(path='myfile.txt', message='add file')\",\n             code_cmd=\"datalad save -m 'add file' myfile.txt\"),\n        dict(text=\"\"\"Save any content underneath the current directory, and\n             recurse into any potential subdatasets\"\"\",\n             code_py=\"save(path='.', recursive=True)\",\n             code_cmd=\"datalad save . -r\"),\n        dict(text=\"Save any modification of known dataset content in the \"\n                  \"current directory, but leave untracked files (e.g. temporary files) \"\n                  \"untouched\",\n             code_py=\"\"\"save(path='.', updated=True)\"\"\",\n             code_cmd=\"\"\"datalad save -u .\"\"\"),\n        dict(text=\"Tag the most recent saved state of a dataset\",\n             code_py=\"save(version_tag='bestyet')\",\n             code_cmd=\"datalad save --version-tag 'bestyet'\"),\n        dict(text=\"Save a specific change but integrate into last commit keeping \"\n                  \"the already recorded commit message\",\n             code_py=\"save(path='myfile.txt', amend=True)\",\n             code_cmd=\"datalad save myfile.txt --amend\")\n    ]\n\n    _params_ = dict(\n        dataset=Parameter(\n            args=(\"-d\", \"--dataset\"),\n            doc=\"\"\"\"specify the dataset to save\"\"\",\n            constraints=EnsureDataset() | EnsureNone()),\n        path=Parameter(\n            args=(\"path\",),\n            metavar='PATH',\n            doc=\"\"\"path/name of the dataset component to save. If given, only\n            changes made to those components are recorded in the new state.\"\"\",\n            nargs='*',\n            constraints=EnsureStr() | EnsureNone()),\n        message=save_message_opt,\n        message_file=Parameter(\n            args=(\"-F\", \"--message-file\"),\n            doc=\"\"\"take the commit message from this file. This flag is\n            mutually exclusive with -m.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        version_tag=Parameter(\n            args=(\"-t\", \"--version-tag\",),\n            metavar='ID',\n            doc=\"\"\"an additional marker for that state. Every dataset that\n            is touched will receive the tag.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        recursive=recursion_flag,\n        recursion_limit=recursion_limit,\n        updated=Parameter(\n            args=('-u', '--updated',),\n            action='store_true',\n            doc=\"\"\"if given, only saves previously tracked paths.\"\"\"),\n        to_git=Parameter(\n            args=(\"--to-git\",),\n            action='store_true',\n            doc=\"\"\"flag whether to add data directly to Git, instead of\n            tracking data identity only.  Use with caution, there is no\n            guarantee that a file put directly into Git like this will\n            not be annexed in a subsequent save operation.\n            If not specified, it will be up to git-annex to decide how\n            a file is tracked, based on a dataset's configuration\n            to track particular paths,\n            file types, or file sizes with either Git or git-annex.\n            (see https://git-annex.branchable.com/tips/largefiles).\n            \"\"\"),\n        jobs=jobs_opt,\n        amend=Parameter(\n            args=('--amend',),\n            action='store_true',\n            doc=\"\"\"if set, changes are not recorded in a new, separate\n            commit, but are integrated with the changeset of the previous\n            commit, and both together are recorded by replacing that\n            previous commit. This is mutually exclusive with recursive\n            operation.\n            \"\"\"),\n    )\n\n    @staticmethod\n    @datasetmethod(name='save')\n    @eval_results\n    def __call__(path=None,\n                 *,\n                 message=None, dataset=None,\n                 version_tag=None,\n                 recursive=False, recursion_limit=None,\n                 updated=False,\n                 message_file=None,\n                 to_git=None,\n                 jobs=None,\n                 amend=False,\n                 ):\n        if message and message_file:\n            raise ValueError(\n                \"Both a message and message file were specified for save()\")\n\n        if amend and recursive:\n            raise ValueError(\"Cannot amend a commit recursively.\")\n\n        path = ensure_list(path)\n\n        if message_file:\n            with open(message_file) as mfh:\n                message = mfh.read()\n\n        # we want 'normal' to achieve the most compact argument list\n        # for git calls\n        # untracked_mode = 'no' if updated else 'normal'\n        # TODO however, Repo.add() would refuse to add any dotfiles\n        # in a directory that is itself untracked, hence the only\n        # choice is to go with potentially crazy long lists\n        # until https://github.com/datalad/datalad/issues/1454\n        # has a resolution\n        untracked_mode = 'no' if updated else 'all'\n\n        # there are three basic scenarios:\n        # 1. save modifications to any already tracked content\n        # 2. save any content (including removal of deleted content)\n        #    to bring things to a clean state\n        # 3. like (2), but only operate on a given subset of content\n        #    identified by paths\n        # - all three have to work in conjunction with --recursive\n        # - the difference between (1) and (2) should be no more\n        #   that a switch from --untracked=no to --untracked=all\n        #   in Repo.save()\n\n        # we do not support\n        # - simultaneous operations on multiple datasets from disjoint\n        #   dataset hierarchies, hence a single reference dataset must be\n        #   identifiable from the either\n        #   - curdir or\n        #   - the `dataset` argument.\n        #   This avoids complex annotation loops and hierarchy tracking.\n        # - any modification upwards from the root dataset\n\n        ds = require_dataset(dataset, check_installed=True, purpose='save')\n\n        # use status() to do all discovery and annotation of paths\n        paths_by_ds = {}\n        for s in Status()(\n                # ATTN: it is vital to pass the `dataset` argument as it,\n                # and not a dataset instance in order to maintain the path\n                # semantics between here and the status() call\n                dataset=dataset,\n                path=path,\n                untracked=untracked_mode,\n                recursive=recursive,\n                recursion_limit=recursion_limit,\n                on_failure='ignore',\n                # for save without recursion only commit matters\n                eval_subdataset_state='full' if recursive else 'commit',\n                return_type='generator',\n                # this could be, but for now only 'error' results are handled\n                # below\n                #on_failure='ignore',\n                result_renderer='disabled'):\n            if s['status'] == 'error':\n                # Downstream code can't do anything with these. Let the caller\n                # decide their fate.\n                yield s\n                continue\n\n            # fish out status dict for this parent dataset\n            ds_status = paths_by_ds.get(s['parentds'], {})\n            # reassemble path status info as repo.status() would have made it\n            ds_status[ut.Path(s['path'])] = \\\n                {k: v for k, v in s.items()\n                 if k not in (\n                     'path', 'parentds', 'refds', 'status', 'action',\n                     'logger')}\n            paths_by_ds[s['parentds']] = ds_status\n\n        lgr.debug('Determined %i datasets for saving from input arguments',\n                  len(paths_by_ds))\n        # figure out what datasets to process, start with the ones containing\n        # the paths that were given as arguments\n        discovered_datasets = list(paths_by_ds.keys())\n        if dataset:\n            # if a reference dataset was given we want to save all the way up\n            # to it, so let's throw it into the mix\n            discovered_datasets.append(ds.path)\n        # sort the datasets into (potentially) disjoint hierarchies,\n        # or a single one, if a reference dataset was given\n        dataset_hierarchies = get_tree_roots(discovered_datasets)\n        for rootds, children in dataset_hierarchies.items():\n            edges = {}\n            discover_dataset_trace_to_targets(\n                rootds, children, [], edges, includeds=children)\n            for superds, subdss in edges.items():\n                superds_status = paths_by_ds.get(superds, {})\n                for subds in subdss:\n                    subds_path = ut.Path(subds)\n                    sub_status = superds_status.get(subds_path, {})\n                    if not (sub_status.get(\"state\") == \"clean\" and\n                            sub_status.get(\"type\") == \"dataset\"):\n                        # start from an entry that may already exist in the\n                        # status record\n                        superds_status[subds_path] = superds_status.get(\n                            subds_path,\n                            # if we got nothing yet:\n                            # shot from the hip, some status config\n                            # to trigger this specific super/sub\n                            # relation to be saved\n                            dict(state='untracked', type='dataset')\n                        )\n                paths_by_ds[superds] = superds_status\n\n        def save_ds(args, version_tag=None):\n            pdspath, paths = args\n\n            pds = Dataset(pdspath)\n            pds_repo = pds.repo\n            # pop status for this dataset, we are not coming back to it\n            pds_status = {\n                # for handing over to the low-level code, we recode any\n                # path relative to the real repo location, this avoid\n                # cumbersome symlink handling without context in the\n                # lower levels\n                pds_repo.pathobj / p.relative_to(pdspath): props\n                for p, props in paths.items()}\n            start_commit = pds_repo.get_hexsha()\n            if not all(p['state'] == 'clean' for p in pds_status.values()) or \\\n                    (amend and message):\n                for res in pds_repo.save_(\n                        message=message,\n                        # make sure to have the `path` arg be None, as we want\n                        # to prevent and bypass any additional repo.status()\n                        # calls\n                        paths=None,\n                        # prevent whining of GitRepo\n                        git=True if not hasattr(ds.repo, 'uuid')\n                        else to_git,\n                        # we are supplying the full status already, do not\n                        # detect anything else\n                        untracked='no',\n                        _status=pds_status,\n                        amend=amend):\n                    # TODO remove stringification when datalad-core can handle\n                    # path objects, or when PY3.6 is the lowest supported\n                    # version\n                    for k in ('path', 'refds'):\n                        if k in res:\n                            res[k] = str(\n                                # recode path back to dataset path anchor\n                                pds.pathobj / res[k].relative_to(\n                                    pds_repo.pathobj)\n                            )\n                    yield res\n            # report on the dataset itself\n            dsres = dict(\n                action='save',\n                type='dataset',\n                path=pds.path,\n                refds=ds.path,\n                status='ok'\n                if start_commit != pds_repo.get_hexsha()\n                else 'notneeded',\n                logger=lgr,\n            )\n            if not version_tag:\n                yield dsres\n                return\n            try:\n                # method requires str\n                version_tag = str(version_tag)\n                pds_repo.tag(version_tag)\n                dsres.update(\n                    status='ok',\n                    version_tag=version_tag)\n                yield dsres\n            except CommandError as e:\n                if dsres['status'] == 'ok':\n                    # first we yield the result for the actual save\n                    # TODO: we will get duplicate dataset/save record obscuring\n                    # progress reporting.  yoh thought to decouple \"tag\" from \"save\"\n                    # messages but was worrying that original authors would disagree\n                    yield dsres.copy()\n                # and now complain that tagging didn't work\n                dsres.update(\n                    status='error',\n                    message=('cannot tag this version: %s', e.stderr.strip()))\n                yield dsres\n\n        if not paths_by_ds:\n            # Special case: empty repo. There's either an empty commit only or\n            # none at all. An empty one we can amend otherwise there's nothing\n            # to do.\n            if amend and ds.repo.get_hexsha():\n                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)\n\n            else:\n                yield dict(action='save',\n                           type='dataset',\n                           path=ds.path,\n                           refds=ds.path,\n                           status='notneeded',\n                           logger=lgr)\n            return\n\n        # TODO: in principle logging could be improved to go not by a dataset\n        # but by path(s) within subdatasets. That should provide a bit better ETA\n        # and more \"dynamic\" feedback than jumpy datasets count.\n        # See addurls where it is implemented that way by providing agg and another\n        # log_filter\n        yield from ProducerConsumerProgressLog(\n            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),\n            partial(save_ds, version_tag=version_tag),\n            safe_to_consume=no_subds_in_futures,\n            producer_future_key=lambda ds_items: ds_items[0],\n            jobs=jobs,\n            log_filter=_log_filter_save_dataset,\n            unit=\"datasets\",\n            lgr=lgr,\n        )",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 63,
                            "end_lineno": 400,
                            "parent": null
                        }
                    ],
                    "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 saveds function\"\"\"\n\nimport shutil\n\nfrom datalad.api import create\nfrom datalad.distribution.dataset import Dataset\nfrom datalad.support.annexrepo import AnnexRepo\nfrom datalad.support.gitrepo import GitRepo\nfrom datalad.tests.utils_pytest import (\n    assert_in,\n    assert_in_results,\n    assert_not_in,\n    assert_repo_status,\n    create_tree,\n    eq_,\n    get_annexstatus,\n    get_convoluted_situation,\n    known_failure_windows,\n    slow,\n    with_tempfile,\n)\nfrom datalad.utils import (\n    on_windows,\n    rmtree,\n)\n\n\n@with_tempfile\ndef test_save_basics(path=None):\n    ds = Dataset(path).create(result_renderer='disabled')\n    # nothing happens\n    eq_(list(ds.repo.save(paths=[], _status={})),\n        [])\n\n    # dataset is clean, so nothing happens with all on default\n    eq_(list(ds.repo.save()),\n        [])\n\n\ndef _test_save_all(path, repocls):\n    ds = get_convoluted_situation(path, repocls)\n    orig_status = ds.repo.status(untracked='all')\n    # TODO test the results when the are crafted\n    res = ds.repo.save()\n    # make sure we get a 'delete' result for each deleted file\n    eq_(\n        set(r['path'] for r in res if r['action'] == 'delete'),\n        {k for k, v in orig_status.items()\n         if k.name in ('file_deleted', 'file_staged_deleted')}\n    )\n    saved_status = ds.repo.status(untracked='all')\n    # we still have an entry for everything that did not get deleted\n    # intentionally\n    eq_(\n        len([f for f, p in orig_status.items()\n             if not f.match('*_deleted')]),\n        len(saved_status))\n    # everything but subdataset entries that contain untracked content,\n    # or modified subsubdatasets is now clean, a repo simply doesn touch\n    # other repos' private parts\n    for f, p in saved_status.items():\n        if p.get('state', None) != 'clean':\n            assert f.match('subds_modified'), f\n    return ds\n\n\n@slow  # 11sec on travis\n@known_failure_windows  # see gh-5462\n@with_tempfile\ndef test_gitrepo_save_all(path=None):\n    _test_save_all(path, GitRepo)\n\n\n@slow  # 11sec on travis\n@known_failure_windows  # see gh-5462\n@with_tempfile\ndef test_annexrepo_save_all(path=None):\n    _test_save_all(path, AnnexRepo)\n\n\n@with_tempfile\ndef test_save_typechange(path=None):\n    ckwa = dict(result_renderer='disabled')\n    ds = Dataset(path).create(**ckwa)\n    foo = ds.pathobj / 'foo'\n    # save a file\n    foo.write_text('some')\n    ds.save(**ckwa)\n    # now delete the file and replace with a directory and a file in it\n    foo.unlink()\n    foo.mkdir()\n    bar = foo / 'bar'\n    bar.write_text('foobar')\n    res = ds.save(**ckwa)\n    assert_in_results(res, path=str(bar), action='add', status='ok')\n    assert_repo_status(ds.repo)\n    if not on_windows:\n        # now replace file with subdataset\n        # (this is https://github.com/datalad/datalad/issues/5418)\n        bar.unlink()\n        Dataset(ds.pathobj / 'tmp').create(**ckwa)\n        shutil.move(ds.pathobj / 'tmp', bar)\n        res = ds.save(**ckwa)\n        assert_repo_status(ds.repo)\n        assert len(ds.subdatasets(**ckwa)) == 1\n    # now replace directory with subdataset\n    rmtree(foo)\n    Dataset(ds.pathobj / 'tmp').create(**ckwa)\n    shutil.move(ds.pathobj / 'tmp', foo)\n    # right now a first save() will save the subdataset removal only\n    ds.save(**ckwa)\n    # subdataset is gone\n    assert len(ds.subdatasets(**ckwa)) == 0\n    # but it takes a second save() run to get a valid status report\n    # to understand that there is a new subdataset on a higher level\n    ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n    assert len(ds.subdatasets(**ckwa)) == 1\n    # now replace subdataset with a file\n    rmtree(foo)\n    foo.write_text('some')\n    ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n\n\n@with_tempfile\ndef test_save_to_git(path=None):\n    ds = Dataset(path).create(result_renderer='disabled')\n    create_tree(\n        ds.path,\n        {\n            'file_ingit': 'file_ingit',\n            'file_inannex': 'file_inannex',\n        }\n    )\n    ds.repo.save(paths=['file_ingit'], git=True)\n    ds.repo.save(paths=['file_inannex'])\n    assert_repo_status(ds.repo)\n    for f, p in get_annexstatus(ds.repo).items():\n        eq_(p['state'], 'clean')\n        if f.match('*ingit'):\n            assert_not_in('key', p, f)\n        elif f.match('*inannex'):\n            assert_in('key', p, f)\n\n\n@with_tempfile\ndef test_save_subds_change(path=None):\n    ckwa = dict(result_renderer='disabled')\n    ds = Dataset(path).create(**ckwa)\n    subds = ds.create('sub', **ckwa)\n    assert_repo_status(ds.repo)\n    rmtree(subds.path)\n    res = ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n    # updated .gitmodules, deleted subds, saved superds\n    assert len(res) == 3\n    assert_in_results(\n        res, type='dataset', path=ds.path, action='save')\n    assert_in_results(\n        res, type='dataset', path=subds.path, action='delete')\n    assert_in_results(\n        res, type='file', path=str(ds.pathobj / '.gitmodules'), action='add')\n    # now add one via save\n    subds2 = create(ds.pathobj / 'sub2', **ckwa)\n    res = ds.save(**ckwa)\n    # updated .gitmodules, added subds, saved superds\n    assert len(res) == 3\n    assert_repo_status(ds.repo)\n    assert_in_results(\n        res, type='dataset', path=ds.path, action='save')\n    assert_in_results(\n        res, type='dataset', path=subds2.path, action='add')\n    assert_in_results(\n        res, type='file', path=str(ds.pathobj / '.gitmodules'), action='add')\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 saveds function\"\"\"\n\nimport shutil\n\nfrom datalad.api import create\nfrom datalad.distribution.dataset import Dataset\nfrom datalad.support.annexrepo import AnnexRepo\nfrom datalad.support.gitrepo import GitRepo\nfrom datalad.tests.utils_pytest import (\n    assert_in,\n    assert_in_results,\n    assert_not_in,\n    assert_repo_status,\n    create_tree,\n    eq_,\n    get_annexstatus,\n    get_convoluted_situation,\n    known_failure_windows,\n    slow,\n    with_tempfile,\n)\nfrom datalad.utils import (\n    on_windows,\n    rmtree,\n)\n\n\n@with_tempfile\ndef test_save_basics(path=None):\n    ds = Dataset(path).create(result_renderer='disabled')\n    # nothing happens\n    eq_(list(ds.repo.save(paths=[], _status={})),\n        [])\n\n    # dataset is clean, so nothing happens with all on default\n    eq_(list(ds.repo.save()),\n        [])\n\n\ndef _test_save_all(path, repocls):\n    ds = get_convoluted_situation(path, repocls)\n    orig_status = ds.repo.status(untracked='all')\n    # TODO test the results when the are crafted\n    res = ds.repo.save()\n    # make sure we get a 'delete' result for each deleted file\n    eq_(\n        set(r['path'] for r in res if r['action'] == 'delete'),\n        {k for k, v in orig_status.items()\n         if k.name in ('file_deleted', 'file_staged_deleted')}\n    )\n    saved_status = ds.repo.status(untracked='all')\n    # we still have an entry for everything that did not get deleted\n    # intentionally\n    eq_(\n        len([f for f, p in orig_status.items()\n             if not f.match('*_deleted')]),\n        len(saved_status))\n    # everything but subdataset entries that contain untracked content,\n    # or modified subsubdatasets is now clean, a repo simply doesn touch\n    # other repos' private parts\n    for f, p in saved_status.items():\n        if p.get('state', None) != 'clean':\n            assert f.match('subds_modified'), f\n\n    # Since we already have rich filetree, now save at dataset level\n    # recursively and introspect some known gotchas\n    resr = ds.save(recursive=True)\n\n    # File within subdataset got committed to git-annex, which was not the\n    # case for GitRepo parent https://github.com/datalad/datalad/issues/7351\n    assert_in_results(\n        resr,\n        status='ok',\n        path=str(ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'),\n        # if key is None -- was committed to git which should have not happened!\n        key=\"MD5E-s14--2c320e0c56ed653384a926292647f226\")\n\n    return ds\n\n\n@slow  # 11sec on travis\n@known_failure_windows  # see gh-5462\n@with_tempfile\ndef test_gitrepo_save_all(path=None):\n    _test_save_all(path, GitRepo)\n\n\n@slow  # 11sec on travis\n@known_failure_windows  # see gh-5462\n@with_tempfile\ndef test_annexrepo_save_all(path=None):\n    _test_save_all(path, AnnexRepo)\n\n\n@with_tempfile\ndef test_save_typechange(path=None):\n    ckwa = dict(result_renderer='disabled')\n    ds = Dataset(path).create(**ckwa)\n    foo = ds.pathobj / 'foo'\n    # save a file\n    foo.write_text('some')\n    ds.save(**ckwa)\n    # now delete the file and replace with a directory and a file in it\n    foo.unlink()\n    foo.mkdir()\n    bar = foo / 'bar'\n    bar.write_text('foobar')\n    res = ds.save(**ckwa)\n    assert_in_results(res, path=str(bar), action='add', status='ok')\n    assert_repo_status(ds.repo)\n    if not on_windows:\n        # now replace file with subdataset\n        # (this is https://github.com/datalad/datalad/issues/5418)\n        bar.unlink()\n        Dataset(ds.pathobj / 'tmp').create(**ckwa)\n        shutil.move(ds.pathobj / 'tmp', bar)\n        res = ds.save(**ckwa)\n        assert_repo_status(ds.repo)\n        assert len(ds.subdatasets(**ckwa)) == 1\n    # now replace directory with subdataset\n    rmtree(foo)\n    Dataset(ds.pathobj / 'tmp').create(**ckwa)\n    shutil.move(ds.pathobj / 'tmp', foo)\n    # right now a first save() will save the subdataset removal only\n    ds.save(**ckwa)\n    # subdataset is gone\n    assert len(ds.subdatasets(**ckwa)) == 0\n    # but it takes a second save() run to get a valid status report\n    # to understand that there is a new subdataset on a higher level\n    ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n    assert len(ds.subdatasets(**ckwa)) == 1\n    # now replace subdataset with a file\n    rmtree(foo)\n    foo.write_text('some')\n    ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n\n\n@with_tempfile\ndef test_save_to_git(path=None):\n    ds = Dataset(path).create(result_renderer='disabled')\n    create_tree(\n        ds.path,\n        {\n            'file_ingit': 'file_ingit',\n            'file_inannex': 'file_inannex',\n        }\n    )\n    ds.repo.save(paths=['file_ingit'], git=True)\n    ds.repo.save(paths=['file_inannex'])\n    assert_repo_status(ds.repo)\n    for f, p in get_annexstatus(ds.repo).items():\n        eq_(p['state'], 'clean')\n        if f.match('*ingit'):\n            assert_not_in('key', p, f)\n        elif f.match('*inannex'):\n            assert_in('key', p, f)\n\n\n@with_tempfile\ndef test_save_subds_change(path=None):\n    ckwa = dict(result_renderer='disabled')\n    ds = Dataset(path).create(**ckwa)\n    subds = ds.create('sub', **ckwa)\n    assert_repo_status(ds.repo)\n    rmtree(subds.path)\n    res = ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n    # updated .gitmodules, deleted subds, saved superds\n    assert len(res) == 3\n    assert_in_results(\n        res, type='dataset', path=ds.path, action='save')\n    assert_in_results(\n        res, type='dataset', path=subds.path, action='delete')\n    assert_in_results(\n        res, type='file', path=str(ds.pathobj / '.gitmodules'), action='add')\n    # now add one via save\n    subds2 = create(ds.pathobj / 'sub2', **ckwa)\n    res = ds.save(**ckwa)\n    # updated .gitmodules, added subds, saved superds\n    assert len(res) == 3\n    assert_repo_status(ds.repo)\n    assert_in_results(\n        res, type='dataset', path=ds.path, action='save')\n    assert_in_results(\n        res, type='dataset', path=subds2.path, action='add')\n    assert_in_results(\n        res, type='file', path=str(ds.pathobj / '.gitmodules'), action='add')\n",
            "header": {
                "file": {
                    "path": "datalad/support/tests/test_repo_save.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "0d91fb0db",
                "new_commit_hash": "692c67e52",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/datalad/support/tests/test_repo_save.py"
            },
            "plus_file": {
                "path": "b/datalad/support/tests/test_repo_save.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 68,
                            "length": 6
                        },
                        "new_range": {
                            "start": 68,
                            "length": 20
                        },
                        "section": "def _test_save_all(path, repocls):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    for f, p in saved_status.items():",
                                "type": "context"
                            },
                            {
                                "content": "        if p.get('state', None) != 'clean':",
                                "type": "context"
                            },
                            {
                                "content": "            assert f.match('subds_modified'), f",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    # Since we already have rich filetree, now save at dataset level",
                                "type": "added"
                            },
                            {
                                "content": "    # recursively and introspect some known gotchas",
                                "type": "added"
                            },
                            {
                                "content": "    resr = ds.save(recursive=True)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    # File within subdataset got committed to git-annex, which was not the",
                                "type": "added"
                            },
                            {
                                "content": "    # case for GitRepo parent https://github.com/datalad/datalad/issues/7351",
                                "type": "added"
                            },
                            {
                                "content": "    assert_in_results(",
                                "type": "added"
                            },
                            {
                                "content": "        resr,",
                                "type": "added"
                            },
                            {
                                "content": "        status='ok',",
                                "type": "added"
                            },
                            {
                                "content": "        path=str(ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'),",
                                "type": "added"
                            },
                            {
                                "content": "        # if key is None -- was committed to git which should have not happened!",
                                "type": "added"
                            },
                            {
                                "content": "        key=\"MD5E-s14--2c320e0c56ed653384a926292647f226\")",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    return ds",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "datalad/support/tests/test_repo_save.py",
                            "type": "function",
                            "name": "_test_save_all",
                            "content": "def _test_save_all(path, repocls):\n    ds = get_convoluted_situation(path, repocls)\n    orig_status = ds.repo.status(untracked='all')\n    # TODO test the results when the are crafted\n    res = ds.repo.save()\n    # make sure we get a 'delete' result for each deleted file\n    eq_(\n        set(r['path'] for r in res if r['action'] == 'delete'),\n        {k for k, v in orig_status.items()\n         if k.name in ('file_deleted', 'file_staged_deleted')}\n    )\n    saved_status = ds.repo.status(untracked='all')\n    # we still have an entry for everything that did not get deleted\n    # intentionally\n    eq_(\n        len([f for f, p in orig_status.items()\n             if not f.match('*_deleted')]),\n        len(saved_status))\n    # everything but subdataset entries that contain untracked content,\n    # or modified subsubdatasets is now clean, a repo simply doesn touch\n    # other repos' private parts\n    for f, p in saved_status.items():\n        if p.get('state', None) != 'clean':\n            assert f.match('subds_modified'), f\n\n    # Since we already have rich filetree, now save at dataset level\n    # recursively and introspect some known gotchas\n    resr = ds.save(recursive=True)\n\n    # File within subdataset got committed to git-annex, which was not the\n    # case for GitRepo parent https://github.com/datalad/datalad/issues/7351\n    assert_in_results(\n        resr,\n        status='ok',\n        path=str(ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'),\n        # if key is None -- was committed to git which should have not happened!\n        key=\"MD5E-s14--2c320e0c56ed653384a926292647f226\")\n\n    return ds",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 47,
                            "end_lineno": 85,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "a8d7c63b763aacfbca15925bb1562a62b4448ea6^",
    "new_commit_hash": "a8d7c63b763aacfbca15925bb1562a62b4448ea6",
    "commit_message": "Merge pull request #7355 from yarikoptic/bf-save\n\nFix save to not force committing into git if reference dataset is pure git (not git-annex)",
    "commit_date": "2023-03-25T12:48:06-04:00",
    "metadata": {}
}
{
    "repo_name": "datalad",
    "new_commit_hash": "a8d7c63b763aacfbca15925bb1562a62b4448ea6",
    "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 saveds function\"\"\"\n\nimport shutil\n\nfrom datalad.api import create\nfrom datalad.distribution.dataset import Dataset\nfrom datalad.support.annexrepo import AnnexRepo\nfrom datalad.support.gitrepo import GitRepo\nfrom datalad.tests.utils_pytest import (\n    assert_in,\n    assert_in_results,\n    assert_not_in,\n    assert_repo_status,\n    create_tree,\n    eq_,\n    get_annexstatus,\n    get_convoluted_situation,\n    known_failure_windows,\n    slow,\n    with_tempfile,\n)\nfrom datalad.utils import (\n    on_windows,\n    rmtree,\n)\n\n\n@with_tempfile\ndef test_save_basics(path=None):\n    ds = Dataset(path).create(result_renderer='disabled')\n    # nothing happens\n    eq_(list(ds.repo.save(paths=[], _status={})),\n        [])\n\n    # dataset is clean, so nothing happens with all on default\n    eq_(list(ds.repo.save()),\n        [])\n\n\ndef _test_save_all(path, repocls):\n    ds = get_convoluted_situation(path, repocls)\n    orig_status = ds.repo.status(untracked='all')\n    # TODO test the results when the are crafted\n    res = ds.repo.save()\n    # make sure we get a 'delete' result for each deleted file\n    eq_(\n        set(r['path'] for r in res if r['action'] == 'delete'),\n        {k for k, v in orig_status.items()\n         if k.name in ('file_deleted', 'file_staged_deleted')}\n    )\n    saved_status = ds.repo.status(untracked='all')\n    # we still have an entry for everything that did not get deleted\n    # intentionally\n    eq_(\n        len([f for f, p in orig_status.items()\n             if not f.match('*_deleted')]),\n        len(saved_status))\n    # everything but subdataset entries that contain untracked content,\n    # or modified subsubdatasets is now clean, a repo simply doesn touch\n    # other repos' private parts\n    for f, p in saved_status.items():\n        if p.get('state', None) != 'clean':\n            assert f.match('subds_modified'), f\n\n    # Since we already have rich filetree, now save at dataset level\n    # recursively and introspect some known gotchas\n    resr = ds.save(recursive=True)\n\n    # File within subdataset got committed to git-annex, which was not the\n    # case for GitRepo parent https://github.com/datalad/datalad/issues/7351\n    assert_in_results(\n        resr,\n        status='ok',\n        path=str(ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'),\n        # if key is None -- was committed to git which should have not happened!\n        key=\"MD5E-s14--2c320e0c56ed653384a926292647f226\")\n\n    return ds\n\n\n@slow  # 11sec on travis\n@known_failure_windows  # see gh-5462\n@with_tempfile\ndef test_gitrepo_save_all(path=None):\n    _test_save_all(path, GitRepo)\n\n\n@slow  # 11sec on travis\n@known_failure_windows  # see gh-5462\n@with_tempfile\ndef test_annexrepo_save_all(path=None):\n    _test_save_all(path, AnnexRepo)\n\n\n@with_tempfile\ndef test_save_typechange(path=None):\n    ckwa = dict(result_renderer='disabled')\n    ds = Dataset(path).create(**ckwa)\n    foo = ds.pathobj / 'foo'\n    # save a file\n    foo.write_text('some')\n    ds.save(**ckwa)\n    # now delete the file and replace with a directory and a file in it\n    foo.unlink()\n    foo.mkdir()\n    bar = foo / 'bar'\n    bar.write_text('foobar')\n    res = ds.save(**ckwa)\n    assert_in_results(res, path=str(bar), action='add', status='ok')\n    assert_repo_status(ds.repo)\n    if not on_windows:\n        # now replace file with subdataset\n        # (this is https://github.com/datalad/datalad/issues/5418)\n        bar.unlink()\n        Dataset(ds.pathobj / 'tmp').create(**ckwa)\n        shutil.move(ds.pathobj / 'tmp', bar)\n        res = ds.save(**ckwa)\n        assert_repo_status(ds.repo)\n        assert len(ds.subdatasets(**ckwa)) == 1\n    # now replace directory with subdataset\n    rmtree(foo)\n    Dataset(ds.pathobj / 'tmp').create(**ckwa)\n    shutil.move(ds.pathobj / 'tmp', foo)\n    # right now a first save() will save the subdataset removal only\n    ds.save(**ckwa)\n    # subdataset is gone\n    assert len(ds.subdatasets(**ckwa)) == 0\n    # but it takes a second save() run to get a valid status report\n    # to understand that there is a new subdataset on a higher level\n    ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n    assert len(ds.subdatasets(**ckwa)) == 1\n    # now replace subdataset with a file\n    rmtree(foo)\n    foo.write_text('some')\n    ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n\n\n@with_tempfile\ndef test_save_to_git(path=None):\n    ds = Dataset(path).create(result_renderer='disabled')\n    create_tree(\n        ds.path,\n        {\n            'file_ingit': 'file_ingit',\n            'file_inannex': 'file_inannex',\n        }\n    )\n    ds.repo.save(paths=['file_ingit'], git=True)\n    ds.repo.save(paths=['file_inannex'])\n    assert_repo_status(ds.repo)\n    for f, p in get_annexstatus(ds.repo).items():\n        eq_(p['state'], 'clean')\n        if f.match('*ingit'):\n            assert_not_in('key', p, f)\n        elif f.match('*inannex'):\n            assert_in('key', p, f)\n\n\n@with_tempfile\ndef test_save_subds_change(path=None):\n    ckwa = dict(result_renderer='disabled')\n    ds = Dataset(path).create(**ckwa)\n    subds = ds.create('sub', **ckwa)\n    assert_repo_status(ds.repo)\n    rmtree(subds.path)\n    res = ds.save(**ckwa)\n    assert_repo_status(ds.repo)\n    # updated .gitmodules, deleted subds, saved superds\n    assert len(res) == 3\n    assert_in_results(\n        res, type='dataset', path=ds.path, action='save')\n    assert_in_results(\n        res, type='dataset', path=subds.path, action='delete')\n    assert_in_results(\n        res, type='file', path=str(ds.pathobj / '.gitmodules'), action='add')\n    # now add one via save\n    subds2 = create(ds.pathobj / 'sub2', **ckwa)\n    res = ds.save(**ckwa)\n    # updated .gitmodules, added subds, saved superds\n    assert len(res) == 3\n    assert_repo_status(ds.repo)\n    assert_in_results(\n        res, type='dataset', path=ds.path, action='save')\n    assert_in_results(\n        res, type='dataset', path=subds2.path, action='add')\n    assert_in_results(\n        res, type='file', path=str(ds.pathobj / '.gitmodules'), action='add')\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+85.ga8d7c63b7\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 131ms\nInstalled 25 packages in 4.84s\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.29s\nPrepared 1 package in 4.67s\nInstalled 53 packages in 375ms\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+85.ga8d7c63b7 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/datalad_a8d7c63b763aacfbca15925bb1562a62b4448ea6)\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 375ms\nInstalled 38 packages in 344ms\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": 0,
    "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_a8d7c63b763aacfbca15925bb1562a62b4448ea6\nconfigfile: tox.ini\nplugins: fail-slow-0.6.0, xdist-3.6.1, cov-6.0.0\ncollected 6 items\n\nr2e_tests/test_1.py ......                                               [100%]\n\n==================================== PASSES ====================================\n_______________________________ test_save_basics _______________________________\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\n____________________________ test_gitrepo_save_all _____________________________\n----------------------------- Captured stdout call -----------------------------\nadd(ok): file_untracked (file)\nsave(ok): subds_modified/someds/dirtyds (dataset)\nadd(ok): dirtyds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified/someds (dataset)\nadd(ok): someds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified (dataset)\nadd(ok): progressedds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subdir/subds_modified (dataset)\nadd(ok): subdir/subds_modified (dataset)\nadd(ok): subds_modified (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 10)\n  save (notneeded: 7, ok: 5)\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] 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] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7/subds_modified/someds\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7/subdir/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7/subdir/subds_modified/progressedds\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_alltvdyl4l7/subds_untracked\nINFO     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_gitrepo_save_alltvdyl4l7/subds_modified/someds/dirtyds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_alltvdyl4l7/subds_modified/someds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_alltvdyl4l7/subds_modified\nINFO     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_gitrepo_save_alltvdyl4l7/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7/subds_added\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7/subdir/subds_untracked\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7/subdir/subds_modified/progressedds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_alltvdyl4l7/subdir/subds_modified\nINFO     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_gitrepo_save_alltvdyl4l7/subdir/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_alltvdyl4l7/subdir/subds_added\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_alltvdyl4l7\nINFO     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_annexrepo_save_all ____________________________\n----------------------------- Captured stdout call -----------------------------\nadd(ok): file_untracked (file)\nsave(ok): subds_modified/someds/dirtyds (dataset)\nadd(ok): dirtyds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified/someds (dataset)\nadd(ok): someds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified (dataset)\nadd(ok): progressedds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subdir/subds_modified (dataset)\nadd(ok): subdir/subds_modified (dataset)\nadd(ok): subds_modified (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 10)\n  save (notneeded: 7, ok: 5)\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] 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] 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:3922 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_annexrepo_save_all043lds07\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07/subds_modified/someds\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07/subdir/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07/subdir/subds_modified/progressedds\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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.annex:log.py:432 Finished annex add\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_all043lds07/subds_untracked\nINFO     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_annexrepo_save_all043lds07/subds_modified/someds/dirtyds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_all043lds07/subds_modified/someds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_all043lds07/subds_modified\nINFO     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_annexrepo_save_all043lds07/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07/subds_added\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07/subdir/subds_untracked\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07/subdir/subds_modified/progressedds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_all043lds07/subdir/subds_modified\nINFO     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_annexrepo_save_all043lds07/subdir/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_all043lds07/subdir/subds_added\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_all043lds07\nINFO     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_save_typechange _____________________________\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------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 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_save_typechange3dd5va_t\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_typechange3dd5va_t\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_save_typechange3dd5va_t\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_save_typechange3dd5va_t\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_typechange3dd5va_t\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_typechange3dd5va_t\nINFO     datalad.core.local.save:log.py:432 Total: done\n_______________________________ test_save_to_git _______________________________\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\n____________________________ test_save_subds_change ____________________________\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------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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_save_subds_change0mdwldj3\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_subds_change0mdwldj3\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_save_subds_change0mdwldj3\nINFO     datalad.core.local.save:log.py:432 Total: done\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::test_save_basics\nPASSED r2e_tests/test_1.py::test_gitrepo_save_all\nPASSED r2e_tests/test_1.py::test_annexrepo_save_all\nPASSED r2e_tests/test_1.py::test_save_typechange\nPASSED r2e_tests/test_1.py::test_save_to_git\nPASSED r2e_tests/test_1.py::test_save_subds_change\n============================== 6 passed in 31.88s ==============================\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_a8d7c63b763aacfbca15925bb1562a62b4448ea6\nconfigfile: tox.ini\nplugins: fail-slow-0.6.0, xdist-3.6.1, cov-6.0.0\ncollected 6 items\n\nr2e_tests/test_1.py .F....                                               [100%]\n\n=================================== FAILURES ===================================\n____________________________ test_gitrepo_save_all _____________________________\n\npath = '/tmp/datalad_temp_test_gitrepo_save_all03nj3qni'\n\n    @slow  # 11sec on travis\n    @known_failure_windows  # see gh-5462\n    @with_tempfile\n    def test_gitrepo_save_all(path=None):\n>       _test_save_all(path, GitRepo)\n\nr2e_tests/test_1.py:92: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:78: in _test_save_all\n    assert_in_results(\ndatalad/tests/utils_pytest.py:1394: in assert_in_results\n    _check_results_in(True, results, **kwargs)\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nshould_contain = True\nresults = [{'action': 'save', 'path': '/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_untracked', 'refds': '/tmp/datalad_...03nj3qni/subds_modified/someds', 'refds': '/tmp/datalad_temp_test_gitrepo_save_all03nj3qni', 'status': 'ok', ...}, ...]\nkwargs = {'key': 'MD5E-s14--2c320e0c56ed653384a926292647f226', 'path': '/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds/file_untracked', 'status': 'ok'}\nfound = False, msg = 'Desired result\\n{}\\nnot found among\\n{}'\n\n    def _check_results_in(should_contain, results, **kwargs):\n        results = ensure_result_list(results)\n        found = False\n        for r in results:\n            if all(k in r and r[k] == v for k, v in kwargs.items()):\n                found = True\n                break\n        if found ^ should_contain:\n            if should_contain:\n                msg = \"Desired result\\n{}\\nnot found among\\n{}\"\n            else:\n                msg = \"Result\\n{}\\nunexpectedly found among\\n{}\"\n>           raise AssertionError(msg.format(_format_res(kwargs),\n                                            _format_res(results)))\nE           AssertionError: Desired result\nE             {\nE              \"key\": \"MD5E-s14--2c320e0c56ed653384a926292647f226\",\nE              \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds/file_untracked\",\nE              \"status\": \"ok\"\nE             }\nE           not found among\nE             [\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_untracked\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"key\": null,\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds/file_untracked\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds\",\nE               \"status\": \"ok\",\nE               \"type\": \"file\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"key\": null,\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds/.gitmodules\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds\",\nE               \"status\": \"ok\",\nE               \"type\": \"file\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"key\": null,\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/someds\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified/.gitmodules\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified\",\nE               \"status\": \"ok\",\nE               \"type\": \"file\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_clean\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_added\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_untracked\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified/progressedds\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"key\": null,\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified/progressedds\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified/.gitmodules\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified\",\nE               \"status\": \"ok\",\nE               \"type\": \"file\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_clean\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_added\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"notneeded\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"key\": null,\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"key\": null,\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              },\nE              {\nE               \"action\": \"add\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni/.gitmodules\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"file\"\nE              },\nE              {\nE               \"action\": \"save\",\nE               \"path\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"refds\": \"/tmp/datalad_temp_test_gitrepo_save_all03nj3qni\",\nE               \"status\": \"ok\",\nE               \"type\": \"dataset\"\nE              }\nE             ]\n\ndatalad/tests/utils_pytest.py:1387: AssertionError\n----------------------------- Captured stdout call -----------------------------\nadd(ok): file_untracked (file)\nsave(ok): subds_modified/someds/dirtyds (dataset)\nadd(ok): dirtyds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified/someds (dataset)\nadd(ok): someds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified (dataset)\nadd(ok): progressedds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subdir/subds_modified (dataset)\nadd(ok): subdir/subds_modified (dataset)\nadd(ok): subds_modified (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 10)\n  save (notneeded: 7, ok: 5)\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] 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] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni/subds_modified/someds\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni/subdir/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified/progressedds\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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_gitrepo_save_all03nj3qni/subds_untracked\nINFO     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_gitrepo_save_all03nj3qni/subds_modified/someds/dirtyds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_all03nj3qni/subds_modified/someds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_all03nj3qni/subds_modified\nINFO     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_gitrepo_save_all03nj3qni/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subds_added\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_untracked\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_modified/progressedds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_all03nj3qni/subdir/subds_modified\nINFO     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_gitrepo_save_all03nj3qni/subdir/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_gitrepo_save_all03nj3qni/subdir/subds_added\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_gitrepo_save_all03nj3qni\nINFO     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==================================== PASSES ====================================\n_______________________________ test_save_basics _______________________________\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\n___________________________ test_annexrepo_save_all ____________________________\n----------------------------- Captured stdout call -----------------------------\nadd(ok): file_untracked (file)\nsave(ok): subds_modified/someds/dirtyds (dataset)\nadd(ok): dirtyds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified/someds (dataset)\nadd(ok): someds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subds_modified (dataset)\nadd(ok): progressedds (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): subdir/subds_modified (dataset)\nadd(ok): subdir/subds_modified (dataset)\nadd(ok): subds_modified (dataset)\nadd(ok): .gitmodules (file)\nsave(ok): . (dataset)\naction summary:\n  add (ok: 10)\n  save (notneeded: 7, ok: 5)\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] 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] 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:3922 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_annexrepo_save_alltemytvz_\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_/subds_modified/someds\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_/subdir/subds_modified\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_/subdir/subds_modified/progressedds\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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.annex:log.py:432 Finished annex add\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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_annexrepo_save_alltemytvz_/subds_untracked\nINFO     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_annexrepo_save_alltemytvz_/subds_modified/someds/dirtyds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_alltemytvz_/subds_modified/someds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_alltemytvz_/subds_modified\nINFO     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_annexrepo_save_alltemytvz_/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_/subds_added\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_/subdir/subds_untracked\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_/subdir/subds_modified/progressedds\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_alltemytvz_/subdir/subds_modified\nINFO     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_annexrepo_save_alltemytvz_/subdir/subds_clean\nINFO     datalad.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_annexrepo_save_alltemytvz_/subdir/subds_added\nINFO     datalad:log.py:432 Clear progress bars\nINFO     datalad: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_annexrepo_save_alltemytvz_\nINFO     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_save_typechange _____________________________\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------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 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_save_typechangej_uovluv\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_typechangej_uovluv\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_save_typechangej_uovluv\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_save_typechangej_uovluv\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_typechangej_uovluv\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_typechangej_uovluv\nINFO     datalad.core.local.save:log.py:432 Total: done\n_______________________________ test_save_to_git _______________________________\n----------------------------- Captured stderr call -----------------------------\n[INFO] scanning for unlocked files (this may take some time) \n------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\n____________________________ test_save_subds_change ____________________________\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------------------------------ Captured log call -------------------------------\nINFO     datalad.annex:annexrepo.py:3922 scanning for unlocked files (this may take some time)\nINFO     datalad.annex:annexrepo.py:3922 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_save_subds_changecbq809de\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.core.local.save:log.py:432 Total: processed result for /tmp/datalad_temp_test_save_subds_changecbq809de\nINFO     datalad.core.local.save:log.py:432 Total: done\nINFO     datalad.annex:annexrepo.py:3922 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_save_subds_changecbq809de\nINFO     datalad.core.local.save:log.py:432 Total: done\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::test_save_basics\nPASSED r2e_tests/test_1.py::test_annexrepo_save_all\nPASSED r2e_tests/test_1.py::test_save_typechange\nPASSED r2e_tests/test_1.py::test_save_to_git\nPASSED r2e_tests/test_1.py::test_save_subds_change\nFAILED r2e_tests/test_1.py::test_gitrepo_save_all - AssertionError: Desired r...\n========================= 1 failed, 5 passed in 28.43s =========================\n",
    "old_commit_res_stderr": ""
}
changelog.d/pr-7355.md datalad/core/local/save.py datalad/support/tests/test_repo_save.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Save Operation Forces Git Commit Instead of Using Git-Annex for Pure Git Datasets

**Description:**
When saving changes in a dataset that uses a pure Git repository (without Git-Annex), the save operation incorrectly forces files to be committed directly to Git. This behavior disregards the intended handling of files in pure Git setups and should respect the absence of Git-Annex.

**Example Buggy Code:**
```python
def test_save_with_pure_git_reference():
    # Setup dataset with pure Git reference
    ds = Dataset('/path/to/pure_git_dataset')
    
    # Modify an untracked file within a subdataset
    file_path = ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'
    file_path.touch()
    
    # Attempt to save recursively
    ds.save(recursive=True)
    
    # Check if the file was committed to Git instead of Git-Annex
    # Expected to have a Git-Annex key, but key is None indicating a Git commit
    assert_in_results(
        resr,
        status='ok',
        path=str(file_path),
        key="MD5E-s14--2c320e0c56ed653384a926292647f226"
    )
```

**Expected Behavior:**
When saving in a pure Git dataset, the save operation should handle files appropriately without forcing them into Git commits. Specifically, files should be managed based on the repository's configuration, and Git-Annex keys should be used where applicable.

**Actual Behavior:**
The save operation forces the untracked file to be committed directly to Git, resulting in the `key` being `None`. This indicates that the file was committed to Git instead of being managed by Git-Annex, which is not the desired behavior for pure Git datasets.

[/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/core/local/save.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.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Interface to add content, and save modifications to a dataset

"""

__docformat__ = 'restructuredtext'

import logging

from functools import partial

from datalad.interface.base import (
    Interface,
    build_doc,
    eval_results,
)
from datalad.interface.common_opts import (
    jobs_opt,
    recursion_limit,
    recursion_flag,
    save_message_opt,
)
from datalad.interface.utils import (
    get_tree_roots,
    discover_dataset_trace_to_targets,
)
from datalad.support.param import Parameter
from datalad.support.constraints import (
    EnsureStr,
    EnsureNone,
)
from datalad.support.exceptions import CommandError
from datalad.support.parallel import (
    no_subds_in_futures,
    ProducerConsumerProgressLog,
)
from datalad.utils import (
    ensure_list,
)
import datalad.utils as ut

from datalad.distribution.dataset import (
    Dataset,
    EnsureDataset,
    datasetmethod,
    require_dataset,
)
from .status import (
    Status,
)

lgr = logging.getLogger('datalad.core.local.save')


@build_doc
class Save(Interface):
    """Save the current state of a dataset

    Saving the state of a dataset records changes that have been made to it.
    This change record is annotated with a user-provided description.
    Optionally, an additional tag, such as a version, can be assigned to the
    saved state. Such tag enables straightforward retrieval of past versions at
    a later point in time.

    .. note::
      Before Git v2.22, any Git repository without an initial commit located
      inside a Dataset is ignored, and content underneath it will be saved to
      the respective superdataset. DataLad datasets always have an initial
      commit, hence are not affected by this behavior.
    """
    # note above documents that out behavior is like that of `git add`, but
    # does not explicitly mention the connection to keep it simple.

    _examples_ = [
        dict(text="""Save any content underneath the current directory, without
             altering any potential subdataset""",
             code_py="save(path='.')",
             code_cmd="datalad save ."),
        dict(text="""Save specific content in the dataset""",
             code_py="save(path='myfile.txt')",
             code_cmd="datalad save myfile.txt"),
        dict(text="""Attach a commit message to save""",
             code_py="save(path='myfile.txt', message='add file')",
             code_cmd="datalad save -m 'add file' myfile.txt"),
        dict(text="""Save any content underneath the current directory, and
             recurse into any potential subdatasets""",
             code_py="save(path='.', recursive=True)",
             code_cmd="datalad save . -r"),
        dict(text="Save any modification of known dataset content in the "
                  "current directory, but leave untracked files (e.g. temporary files) "
                  "untouched",
             code_py="""save(path='.', updated=True)""",
             code_cmd="""datalad save -u ."""),
        dict(text="Tag the most recent saved state of a dataset",
             code_py="save(version_tag='bestyet')",
             code_cmd="datalad save --version-tag 'bestyet'"),
        dict(text="Save a specific change but integrate into last commit keeping "
                  "the already recorded commit message",
             code_py="save(path='myfile.txt', amend=True)",
             code_cmd="datalad save myfile.txt --amend")
    ]

    _params_ = dict(
        dataset=Parameter(
            args=("-d", "--dataset"),
            doc=""""specify the dataset to save""",
            constraints=EnsureDataset() | EnsureNone()),
        path=Parameter(
            args=("path",),
            metavar='PATH',
            doc="""path/name of the dataset component to save. If given, only
            changes made to those components are recorded in the new state.""",
            nargs='*',
            constraints=EnsureStr() | EnsureNone()),
        message=save_message_opt,
        message_file=Parameter(
            args=("-F", "--message-file"),
            doc="""take the commit message from this file. This flag is
            mutually exclusive with -m.""",
            constraints=EnsureStr() | EnsureNone()),
        version_tag=Parameter(
            args=("-t", "--version-tag",),
            metavar='ID',
            doc="""an additional marker for that state. Every dataset that
            is touched will receive the tag.""",
            constraints=EnsureStr() | EnsureNone()),
        recursive=recursion_flag,
        recursion_limit=recursion_limit,
        updated=Parameter(
            args=('-u', '--updated',),
            action='store_true',
            doc="""if given, only saves previously tracked paths."""),
        to_git=Parameter(
            args=("--to-git",),
            action='store_true',
            doc="""flag whether to add data directly to Git, instead of
            tracking data identity only.  Use with caution, there is no
            guarantee that a file put directly into Git like this will
            not be annexed in a subsequent save operation.
            If not specified, it will be up to git-annex to decide how
            a file is tracked, based on a dataset's configuration
            to track particular paths,
            file types, or file sizes with either Git or git-annex.
            (see https://git-annex.branchable.com/tips/largefiles).
            """),
        jobs=jobs_opt,
        amend=Parameter(
            args=('--amend',),
            action='store_true',
            doc="""if set, changes are not recorded in a new, separate
            commit, but are integrated with the changeset of the previous
            commit, and both together are recorded by replacing that
            previous commit. This is mutually exclusive with recursive
            operation.
            """),
    )

    @staticmethod
    @datasetmethod(name='save')
    @eval_results
    def __call__(path=None,
                 *,
                 message=None, dataset=None,
                 version_tag=None,
                 recursive=False, recursion_limit=None,
                 updated=False,
                 message_file=None,
                 to_git=None,
                 jobs=None,
                 amend=False,
                 ):
        if message and message_file:
            raise ValueError(
                "Both a message and message file were specified for save()")

        if amend and recursive:
            raise ValueError("Cannot amend a commit recursively.")

        path = ensure_list(path)

        if message_file:
            with open(message_file) as mfh:
                message = mfh.read()

        # we want 'normal' to achieve the most compact argument list
        # for git calls
        # untracked_mode = 'no' if updated else 'normal'
        # TODO however, Repo.add() would refuse to add any dotfiles
        # in a directory that is itself untracked, hence the only
        # choice is to go with potentially crazy long lists
        # until https://github.com/datalad/datalad/issues/1454
        # has a resolution
        untracked_mode = 'no' if updated else 'all'

        # there are three basic scenarios:
        # 1. save modifications to any already tracked content
        # 2. save any content (including removal of deleted content)
        #    to bring things to a clean state
        # 3. like (2), but only operate on a given subset of content
        #    identified by paths
        # - all three have to work in conjunction with --recursive
        # - the difference between (1) and (2) should be no more
        #   that a switch from --untracked=no to --untracked=all
        #   in Repo.save()

        # we do not support
        # - simultaneous operations on multiple datasets from disjoint
        #   dataset hierarchies, hence a single reference dataset must be
        #   identifiable from the either
        #   - curdir or
        #   - the `dataset` argument.
        #   This avoids complex annotation loops and hierarchy tracking.
        # - any modification upwards from the root dataset

        ds = require_dataset(dataset, check_installed=True, purpose='save')

        # use status() to do all discovery and annotation of paths
        paths_by_ds = {}
        for s in Status()(
                # ATTN: it is vital to pass the `dataset` argument as it,
                # and not a dataset instance in order to maintain the path
                # semantics between here and the status() call
                dataset=dataset,
                path=path,
                untracked=untracked_mode,
                recursive=recursive,
                recursion_limit=recursion_limit,
                on_failure='ignore',
                # for save without recursion only commit matters
                eval_subdataset_state='full' if recursive else 'commit',
                return_type='generator',
                # this could be, but for now only 'error' results are handled
                # below
                #on_failure='ignore',
                result_renderer='disabled'):
            if s['status'] == 'error':
                # Downstream code can't do anything with these. Let the caller
                # decide their fate.
                yield s
                continue

            # fish out status dict for this parent dataset
            ds_status = paths_by_ds.get(s['parentds'], {})
            # reassemble path status info as repo.status() would have made it
            ds_status[ut.Path(s['path'])] = \
                {k: v for k, v in s.items()
                 if k not in (
                     'path', 'parentds', 'refds', 'status', 'action',
                     'logger')}
            paths_by_ds[s['parentds']] = ds_status

        lgr.debug('Determined %i datasets for saving from input arguments',
                  len(paths_by_ds))
        # figure out what datasets to process, start with the ones containing
        # the paths that were given as arguments
        discovered_datasets = list(paths_by_ds.keys())
        if dataset:
            # if a reference dataset was given we want to save all the way up
            # to it, so let's throw it into the mix
            discovered_datasets.append(ds.path)
        # sort the datasets into (potentially) disjoint hierarchies,
        # or a single one, if a reference dataset was given
        dataset_hierarchies = get_tree_roots(discovered_datasets)
        for rootds, children in dataset_hierarchies.items():
            edges = {}
            discover_dataset_trace_to_targets(
                rootds, children, [], edges, includeds=children)
            for superds, subdss in edges.items():
                superds_status = paths_by_ds.get(superds, {})
                for subds in subdss:
                    subds_path = ut.Path(subds)
                    sub_status = superds_status.get(subds_path, {})
                    if not (sub_status.get("state") == "clean" and
                            sub_status.get("type") == "dataset"):
                        # start from an entry that may already exist in the
                        # status record
                        superds_status[subds_path] = superds_status.get(
                            subds_path,
                            # if we got nothing yet:
                            # shot from the hip, some status config
                            # to trigger this specific super/sub
                            # relation to be saved
                            dict(state='untracked', type='dataset')
                        )
                paths_by_ds[superds] = superds_status

        def save_ds(args, version_tag=None):
            pdspath, paths = args

            pds = Dataset(pdspath)
            pds_repo = pds.repo
            # pop status for this dataset, we are not coming back to it
            pds_status = {
                # for handing over to the low-level code, we recode any
                # path relative to the real repo location, this avoid
                # cumbersome symlink handling without context in the
                # lower levels
                pds_repo.pathobj / p.relative_to(pdspath): props
                for p, props in paths.items()}
            start_commit = pds_repo.get_hexsha()
            if not all(p['state'] == 'clean' for p in pds_status.values()) or \
                    (amend and message):
                for res in pds_repo.save_(
                        message=message,
                        # make sure to have the `path` arg be None, as we want
                        # to prevent and bypass any additional repo.status()
                        # calls
                        paths=None,
                        # prevent whining of GitRepo
                        git=True if not hasattr(ds.repo, 'uuid')
                        else to_git,
                        # we are supplying the full status already, do not
                        # detect anything else
                        untracked='no',
                        _status=pds_status,
                        amend=amend):
                    # TODO remove stringification when datalad-core can handle
                    # path objects, or when PY3.6 is the lowest supported
                    # version
                    for k in ('path', 'refds'):
                        if k in res:
                            res[k] = str(
                                # recode path back to dataset path anchor
                                pds.pathobj / res[k].relative_to(
                                    pds_repo.pathobj)
                            )
                    yield res
            # report on the dataset itself
            dsres = dict(
                action='save',
                type='dataset',
                path=pds.path,
                refds=ds.path,
                status='ok'
                if start_commit != pds_repo.get_hexsha()
                else 'notneeded',
                logger=lgr,
            )
            if not version_tag:
                yield dsres
                return
            try:
                # method requires str
                version_tag = str(version_tag)
                pds_repo.tag(version_tag)
                dsres.update(
                    status='ok',
                    version_tag=version_tag)
                yield dsres
            except CommandError as e:
                if dsres['status'] == 'ok':
                    # first we yield the result for the actual save
                    # TODO: we will get duplicate dataset/save record obscuring
                    # progress reporting.  yoh thought to decouple "tag" from "save"
                    # messages but was worrying that original authors would disagree
                    yield dsres.copy()
                # and now complain that tagging didn't work
                dsres.update(
                    status='error',
                    message=('cannot tag this version: %s', e.stderr.strip()))
                yield dsres

        if not paths_by_ds:
            # Special case: empty repo. There's either an empty commit only or
            # none at all. An empty one we can amend otherwise there's nothing
            # to do.
            if amend and ds.repo.get_hexsha():
                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)

            else:
                yield dict(action='save',
                           type='dataset',
                           path=ds.path,
                           refds=ds.path,
                           status='notneeded',
                           logger=lgr)
            return

        # TODO: in principle logging could be improved to go not by a dataset
        # but by path(s) within subdatasets. That should provide a bit better ETA
        # and more "dynamic" feedback than jumpy datasets count.
        # See addurls where it is implemented that way by providing agg and another
        # log_filter
        yield from ProducerConsumerProgressLog(
            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),
            partial(save_ds, version_tag=version_tag),
            safe_to_consume=no_subds_in_futures,
            producer_future_key=lambda ds_items: ds_items[0],
            jobs=jobs,
            log_filter=_log_filter_save_dataset,
            unit="datasets",
            lgr=lgr,
        )


def _log_filter_save_dataset(res):
    return res.get('type') == 'dataset' and res.get('action') == 'save'

```
--- 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:** Save Operation Forces Git Commit Instead of Using Git-Annex for Pure Git Datasets

**Description:**
When saving changes in a dataset that uses a pure Git repository (without Git-Annex), the save operation incorrectly forces files to be committed directly to Git. This behavior disregards the intended handling of files in pure Git setups and should respect the absence of Git-Annex.

**Example Buggy Code:**
```python
def test_save_with_pure_git_reference():
    # Setup dataset with pure Git reference
    ds = Dataset('/path/to/pure_git_dataset')
    
    # Modify an untracked file within a subdataset
    file_path = ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'
    file_path.touch()
    
    # Attempt to save recursively
    ds.save(recursive=True)
    
    # Check if the file was committed to Git instead of Git-Annex
    # Expected to have a Git-Annex key, but key is None indicating a Git commit
    assert_in_results(
        resr,
        status='ok',
        path=str(file_path),
        key="MD5E-s14--2c320e0c56ed653384a926292647f226"
    )
```

**Expected Behavior:**
When saving in a pure Git dataset, the save operation should handle files appropriately without forcing them into Git commits. Specifically, files should be managed based on the repository's configuration, and Git-Annex keys should be used where applicable.

**Actual Behavior:**
The save operation forces the untracked file to be committed directly to Git, resulting in the `key` being `None`. This indicates that the file was committed to Git instead of being managed by Git-Annex, which is not the desired behavior for pure Git datasets.

[/ISSUE]
{
    "test_save_basics": "PASSED",
    "test_gitrepo_save_all": "PASSED",
    "test_annexrepo_save_all": "PASSED",
    "test_save_typechange": "PASSED",
    "test_save_to_git": "PASSED",
    "test_save_subds_change": "PASSED"
}
diff --git a/datalad/core/local/save.py b/datalad/core/local/save.py
index fc61a792d..903a0ce5b 100644
--- a/datalad/core/local/save.py
+++ b/datalad/core/local/save.py
@@ -314,7 +314,7 @@ class Save(Interface):
                         # calls
                         paths=None,
                         # prevent whining of GitRepo
-                        git=True if not hasattr(ds.repo, 'uuid')
+                        git=True if not hasattr(pds_repo, 'uuid')
                         else to_git,
                         # we are supplying the full status already, do not
                         # detect anything else

{"datalad/core/local/save.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\"\"\"Interface to add content, and save modifications to a dataset\n\n\"\"\"\n\n__docformat__ = 'restructuredtext'\n\nimport logging\n\nfrom functools import partial\n\nfrom datalad.interface.base import (\n    Interface,\n    build_doc,\n    eval_results,\n)\nfrom datalad.interface.common_opts import (\n    jobs_opt,\n    recursion_limit,\n    recursion_flag,\n    save_message_opt,\n)\nfrom datalad.interface.utils import (\n    get_tree_roots,\n    discover_dataset_trace_to_targets,\n)\nfrom datalad.support.param import Parameter\nfrom datalad.support.constraints import (\n    EnsureStr,\n    EnsureNone,\n)\nfrom datalad.support.exceptions import CommandError\nfrom datalad.support.parallel import (\n    no_subds_in_futures,\n    ProducerConsumerProgressLog,\n)\nfrom datalad.utils import (\n    ensure_list,\n)\nimport datalad.utils as ut\n\nfrom datalad.distribution.dataset import (\n    Dataset,\n    EnsureDataset,\n    datasetmethod,\n    require_dataset,\n)\nfrom .status import (\n    Status,\n)\n\nlgr = logging.getLogger('datalad.core.local.save')\n\n\n@build_doc\nclass Save(Interface):\n    \"\"\"Save the current state of a dataset\n\n    Saving the state of a dataset records changes that have been made to it.\n    This change record is annotated with a user-provided description.\n    Optionally, an additional tag, such as a version, can be assigned to the\n    saved state. Such tag enables straightforward retrieval of past versions at\n    a later point in time.\n\n    .. note::\n      Before Git v2.22, any Git repository without an initial commit located\n      inside a Dataset is ignored, and content underneath it will be saved to\n      the respective superdataset. DataLad datasets always have an initial\n      commit, hence are not affected by this behavior.\n    \"\"\"\n    # note above documents that out behavior is like that of `git add`, but\n    # does not explicitly mention the connection to keep it simple.\n\n    _examples_ = [\n        dict(text=\"\"\"Save any content underneath the current directory, without\n             altering any potential subdataset\"\"\",\n             code_py=\"save(path='.')\",\n             code_cmd=\"datalad save .\"),\n        dict(text=\"\"\"Save specific content in the dataset\"\"\",\n             code_py=\"save(path='myfile.txt')\",\n             code_cmd=\"datalad save myfile.txt\"),\n        dict(text=\"\"\"Attach a commit message to save\"\"\",\n             code_py=\"save(path='myfile.txt', message='add file')\",\n             code_cmd=\"datalad save -m 'add file' myfile.txt\"),\n        dict(text=\"\"\"Save any content underneath the current directory, and\n             recurse into any potential subdatasets\"\"\",\n             code_py=\"save(path='.', recursive=True)\",\n             code_cmd=\"datalad save . -r\"),\n        dict(text=\"Save any modification of known dataset content in the \"\n                  \"current directory, but leave untracked files (e.g. temporary files) \"\n                  \"untouched\",\n             code_py=\"\"\"save(path='.', updated=True)\"\"\",\n             code_cmd=\"\"\"datalad save -u .\"\"\"),\n        dict(text=\"Tag the most recent saved state of a dataset\",\n             code_py=\"save(version_tag='bestyet')\",\n             code_cmd=\"datalad save --version-tag 'bestyet'\"),\n        dict(text=\"Save a specific change but integrate into last commit keeping \"\n                  \"the already recorded commit message\",\n             code_py=\"save(path='myfile.txt', amend=True)\",\n             code_cmd=\"datalad save myfile.txt --amend\")\n    ]\n\n    _params_ = dict(\n        dataset=Parameter(\n            args=(\"-d\", \"--dataset\"),\n            doc=\"\"\"\"specify the dataset to save\"\"\",\n            constraints=EnsureDataset() | EnsureNone()),\n        path=Parameter(\n            args=(\"path\",),\n            metavar='PATH',\n            doc=\"\"\"path/name of the dataset component to save. If given, only\n            changes made to those components are recorded in the new state.\"\"\",\n            nargs='*',\n            constraints=EnsureStr() | EnsureNone()),\n        message=save_message_opt,\n        message_file=Parameter(\n            args=(\"-F\", \"--message-file\"),\n            doc=\"\"\"take the commit message from this file. This flag is\n            mutually exclusive with -m.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        version_tag=Parameter(\n            args=(\"-t\", \"--version-tag\",),\n            metavar='ID',\n            doc=\"\"\"an additional marker for that state. Every dataset that\n            is touched will receive the tag.\"\"\",\n            constraints=EnsureStr() | EnsureNone()),\n        recursive=recursion_flag,\n        recursion_limit=recursion_limit,\n        updated=Parameter(\n            args=('-u', '--updated',),\n            action='store_true',\n            doc=\"\"\"if given, only saves previously tracked paths.\"\"\"),\n        to_git=Parameter(\n            args=(\"--to-git\",),\n            action='store_true',\n            doc=\"\"\"flag whether to add data directly to Git, instead of\n            tracking data identity only.  Use with caution, there is no\n            guarantee that a file put directly into Git like this will\n            not be annexed in a subsequent save operation.\n            If not specified, it will be up to git-annex to decide how\n            a file is tracked, based on a dataset's configuration\n            to track particular paths,\n            file types, or file sizes with either Git or git-annex.\n            (see https://git-annex.branchable.com/tips/largefiles).\n            \"\"\"),\n        jobs=jobs_opt,\n        amend=Parameter(\n            args=('--amend',),\n            action='store_true',\n            doc=\"\"\"if set, changes are not recorded in a new, separate\n            commit, but are integrated with the changeset of the previous\n            commit, and both together are recorded by replacing that\n            previous commit. This is mutually exclusive with recursive\n            operation.\n            \"\"\"),\n    )\n\n    @staticmethod\n    @datasetmethod(name='save')\n    @eval_results\n    def __call__(path=None,\n                 *,\n                 message=None, dataset=None,\n                 version_tag=None,\n                 recursive=False, recursion_limit=None,\n                 updated=False,\n                 message_file=None,\n                 to_git=None,\n                 jobs=None,\n                 amend=False,\n                 ):\n        if message and message_file:\n            raise ValueError(\n                \"Both a message and message file were specified for save()\")\n\n        if amend and recursive:\n            raise ValueError(\"Cannot amend a commit recursively.\")\n\n        path = ensure_list(path)\n\n        if message_file:\n            with open(message_file) as mfh:\n                message = mfh.read()\n\n        # we want 'normal' to achieve the most compact argument list\n        # for git calls\n        # untracked_mode = 'no' if updated else 'normal'\n        # TODO however, Repo.add() would refuse to add any dotfiles\n        # in a directory that is itself untracked, hence the only\n        # choice is to go with potentially crazy long lists\n        # until https://github.com/datalad/datalad/issues/1454\n        # has a resolution\n        untracked_mode = 'no' if updated else 'all'\n\n        # there are three basic scenarios:\n        # 1. save modifications to any already tracked content\n        # 2. save any content (including removal of deleted content)\n        #    to bring things to a clean state\n        # 3. like (2), but only operate on a given subset of content\n        #    identified by paths\n        # - all three have to work in conjunction with --recursive\n        # - the difference between (1) and (2) should be no more\n        #   that a switch from --untracked=no to --untracked=all\n        #   in Repo.save()\n\n        # we do not support\n        # - simultaneous operations on multiple datasets from disjoint\n        #   dataset hierarchies, hence a single reference dataset must be\n        #   identifiable from the either\n        #   - curdir or\n        #   - the `dataset` argument.\n        #   This avoids complex annotation loops and hierarchy tracking.\n        # - any modification upwards from the root dataset\n\n        ds = require_dataset(dataset, check_installed=True, purpose='save')\n\n        # use status() to do all discovery and annotation of paths\n        paths_by_ds = {}\n        for s in Status()(\n                # ATTN: it is vital to pass the `dataset` argument as it,\n                # and not a dataset instance in order to maintain the path\n                # semantics between here and the status() call\n                dataset=dataset,\n                path=path,\n                untracked=untracked_mode,\n                recursive=recursive,\n                recursion_limit=recursion_limit,\n                on_failure='ignore',\n                # for save without recursion only commit matters\n                eval_subdataset_state='full' if recursive else 'commit',\n                return_type='generator',\n                # this could be, but for now only 'error' results are handled\n                # below\n                #on_failure='ignore',\n                result_renderer='disabled'):\n            if s['status'] == 'error':\n                # Downstream code can't do anything with these. Let the caller\n                # decide their fate.\n                yield s\n                continue\n\n            # fish out status dict for this parent dataset\n            ds_status = paths_by_ds.get(s['parentds'], {})\n            # reassemble path status info as repo.status() would have made it\n            ds_status[ut.Path(s['path'])] = \\\n                {k: v for k, v in s.items()\n                 if k not in (\n                     'path', 'parentds', 'refds', 'status', 'action',\n                     'logger')}\n            paths_by_ds[s['parentds']] = ds_status\n\n        lgr.debug('Determined %i datasets for saving from input arguments',\n                  len(paths_by_ds))\n        # figure out what datasets to process, start with the ones containing\n        # the paths that were given as arguments\n        discovered_datasets = list(paths_by_ds.keys())\n        if dataset:\n            # if a reference dataset was given we want to save all the way up\n            # to it, so let's throw it into the mix\n            discovered_datasets.append(ds.path)\n        # sort the datasets into (potentially) disjoint hierarchies,\n        # or a single one, if a reference dataset was given\n        dataset_hierarchies = get_tree_roots(discovered_datasets)\n        for rootds, children in dataset_hierarchies.items():\n            edges = {}\n            discover_dataset_trace_to_targets(\n                rootds, children, [], edges, includeds=children)\n            for superds, subdss in edges.items():\n                superds_status = paths_by_ds.get(superds, {})\n                for subds in subdss:\n                    subds_path = ut.Path(subds)\n                    sub_status = superds_status.get(subds_path, {})\n                    if not (sub_status.get(\"state\") == \"clean\" and\n                            sub_status.get(\"type\") == \"dataset\"):\n                        # start from an entry that may already exist in the\n                        # status record\n                        superds_status[subds_path] = superds_status.get(\n                            subds_path,\n                            # if we got nothing yet:\n                            # shot from the hip, some status config\n                            # to trigger this specific super/sub\n                            # relation to be saved\n                            dict(state='untracked', type='dataset')\n                        )\n                paths_by_ds[superds] = superds_status\n\n        def save_ds(args, version_tag=None):\n            pdspath, paths = args\n\n            pds = Dataset(pdspath)\n            pds_repo = pds.repo\n            # pop status for this dataset, we are not coming back to it\n            pds_status = {\n                # for handing over to the low-level code, we recode any\n                # path relative to the real repo location, this avoid\n                # cumbersome symlink handling without context in the\n                # lower levels\n                pds_repo.pathobj / p.relative_to(pdspath): props\n                for p, props in paths.items()}\n            start_commit = pds_repo.get_hexsha()\n            if not all(p['state'] == 'clean' for p in pds_status.values()) or \\\n                    (amend and message):\n                for res in pds_repo.save_(\n                        message=message,\n                        # make sure to have the `path` arg be None, as we want\n                        # to prevent and bypass any additional repo.status()\n                        # calls\n                        paths=None,\n                        # prevent whining of GitRepo\n                        git=True if not hasattr(ds.repo, 'uuid')\n                        else to_git,\n                        # we are supplying the full status already, do not\n                        # detect anything else\n                        untracked='no',\n                        _status=pds_status,\n                        amend=amend):\n                    # TODO remove stringification when datalad-core can handle\n                    # path objects, or when PY3.6 is the lowest supported\n                    # version\n                    for k in ('path', 'refds'):\n                        if k in res:\n                            res[k] = str(\n                                # recode path back to dataset path anchor\n                                pds.pathobj / res[k].relative_to(\n                                    pds_repo.pathobj)\n                            )\n                    yield res\n            # report on the dataset itself\n            dsres = dict(\n                action='save',\n                type='dataset',\n                path=pds.path,\n                refds=ds.path,\n                status='ok'\n                if start_commit != pds_repo.get_hexsha()\n                else 'notneeded',\n                logger=lgr,\n            )\n            if not version_tag:\n                yield dsres\n                return\n            try:\n                # method requires str\n                version_tag = str(version_tag)\n                pds_repo.tag(version_tag)\n                dsres.update(\n                    status='ok',\n                    version_tag=version_tag)\n                yield dsres\n            except CommandError as e:\n                if dsres['status'] == 'ok':\n                    # first we yield the result for the actual save\n                    # TODO: we will get duplicate dataset/save record obscuring\n                    # progress reporting.  yoh thought to decouple \"tag\" from \"save\"\n                    # messages but was worrying that original authors would disagree\n                    yield dsres.copy()\n                # and now complain that tagging didn't work\n                dsres.update(\n                    status='error',\n                    message=('cannot tag this version: %s', e.stderr.strip()))\n                yield dsres\n\n        if not paths_by_ds:\n            # Special case: empty repo. There's either an empty commit only or\n            # none at all. An empty one we can amend otherwise there's nothing\n            # to do.\n            if amend and ds.repo.get_hexsha():\n                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)\n\n            else:\n                yield dict(action='save',\n                           type='dataset',\n                           path=ds.path,\n                           refds=ds.path,\n                           status='notneeded',\n                           logger=lgr)\n            return\n\n        # TODO: in principle logging could be improved to go not by a dataset\n        # but by path(s) within subdatasets. That should provide a bit better ETA\n        # and more \"dynamic\" feedback than jumpy datasets count.\n        # See addurls where it is implemented that way by providing agg and another\n        # log_filter\n        yield from ProducerConsumerProgressLog(\n            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),\n            partial(save_ds, version_tag=version_tag),\n            safe_to_consume=no_subds_in_futures,\n            producer_future_key=lambda ds_items: ds_items[0],\n            jobs=jobs,\n            log_filter=_log_filter_save_dataset,\n            unit=\"datasets\",\n            lgr=lgr,\n        )\n\n\ndef _log_filter_save_dataset(res):\n    return res.get('type') == 'dataset' and res.get('action') == 'save'\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:** Save Operation Forces Git Commit Instead of Using Git-Annex for Pure Git Datasets

**Description:**
When saving changes in a dataset that uses a pure Git repository (without Git-Annex), the save operation incorrectly forces files to be committed directly to Git. This behavior disregards the intended handling of files in pure Git setups and should respect the absence of Git-Annex.

**Example Buggy Code:**
```python
def test_save_with_pure_git_reference():
    # Setup dataset with pure Git reference
    ds = Dataset('/path/to/pure_git_dataset')
    
    # Modify an untracked file within a subdataset
    file_path = ds.pathobj / 'subds_modified' / 'someds' / 'dirtyds' / 'file_untracked'
    file_path.touch()
    
    # Attempt to save recursively
    ds.save(recursive=True)
    
    # Check if the file was committed to Git instead of Git-Annex
    # Expected to have a Git-Annex key, but key is None indicating a Git commit
    assert_in_results(
        resr,
        status='ok',
        path=str(file_path),
        key="MD5E-s14--2c320e0c56ed653384a926292647f226"
    )
```

**Expected Behavior:**
When saving in a pure Git dataset, the save operation should handle files appropriately without forcing them into Git commits. Specifically, files should be managed based on the repository's configuration, and Git-Annex keys should be used where applicable.

**Actual Behavior:**
The save operation forces the untracked file to be committed directly to Git, resulting in the `key` being `None`. This indicates that the file was committed to Git instead of being managed by Git-Annex, which is not the desired behavior for pure Git datasets.

[/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/core/local/save.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.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Interface to add content, and save modifications to a dataset

"""

__docformat__ = 'restructuredtext'

import logging

from functools import partial

from datalad.interface.base import (
    Interface,
    build_doc,
    eval_results,
)
from datalad.interface.common_opts import (
    jobs_opt,
    recursion_limit,
    recursion_flag,
    save_message_opt,
)
from datalad.interface.utils import (
    get_tree_roots,
    discover_dataset_trace_to_targets,
)
from datalad.support.param import Parameter
from datalad.support.constraints import (
    EnsureStr,
    EnsureNone,
)
from datalad.support.exceptions import CommandError
from datalad.support.parallel import (
    no_subds_in_futures,
    ProducerConsumerProgressLog,
)
from datalad.utils import (
    ensure_list,
)
import datalad.utils as ut

from datalad.distribution.dataset import (
    Dataset,
    EnsureDataset,
    datasetmethod,
    require_dataset,
)
from .status import (
    Status,
)

lgr = logging.getLogger('datalad.core.local.save')


@build_doc
class Save(Interface):
    """Save the current state of a dataset

    Saving the state of a dataset records changes that have been made to it.
    This change record is annotated with a user-provided description.
    Optionally, an additional tag, such as a version, can be assigned to the
    saved state. Such tag enables straightforward retrieval of past versions at
    a later point in time.

    .. note::
      Before Git v2.22, any Git repository without an initial commit located
      inside a Dataset is ignored, and content underneath it will be saved to
      the respective superdataset. DataLad datasets always have an initial
      commit, hence are not affected by this behavior.
    """
    # note above documents that out behavior is like that of `git add`, but
    # does not explicitly mention the connection to keep it simple.

    _examples_ = [
        dict(text="""Save any content underneath the current directory, without
             altering any potential subdataset""",
             code_py="save(path='.')",
             code_cmd="datalad save ."),
        dict(text="""Save specific content in the dataset""",
             code_py="save(path='myfile.txt')",
             code_cmd="datalad save myfile.txt"),
        dict(text="""Attach a commit message to save""",
             code_py="save(path='myfile.txt', message='add file')",
             code_cmd="datalad save -m 'add file' myfile.txt"),
        dict(text="""Save any content underneath the current directory, and
             recurse into any potential subdatasets""",
             code_py="save(path='.', recursive=True)",
             code_cmd="datalad save . -r"),
        dict(text="Save any modification of known dataset content in the "
                  "current directory, but leave untracked files (e.g. temporary files) "
                  "untouched",
             code_py="""save(path='.', updated=True)""",
             code_cmd="""datalad save -u ."""),
        dict(text="Tag the most recent saved state of a dataset",
             code_py="save(version_tag='bestyet')",
             code_cmd="datalad save --version-tag 'bestyet'"),
        dict(text="Save a specific change but integrate into last commit keeping "
                  "the already recorded commit message",
             code_py="save(path='myfile.txt', amend=True)",
             code_cmd="datalad save myfile.txt --amend")
    ]

    _params_ = dict(
        dataset=Parameter(
            args=("-d", "--dataset"),
            doc=""""specify the dataset to save""",
            constraints=EnsureDataset() | EnsureNone()),
        path=Parameter(
            args=("path",),
            metavar='PATH',
            doc="""path/name of the dataset component to save. If given, only
            changes made to those components are recorded in the new state.""",
            nargs='*',
            constraints=EnsureStr() | EnsureNone()),
        message=save_message_opt,
        message_file=Parameter(
            args=("-F", "--message-file"),
            doc="""take the commit message from this file. This flag is
            mutually exclusive with -m.""",
            constraints=EnsureStr() | EnsureNone()),
        version_tag=Parameter(
            args=("-t", "--version-tag",),
            metavar='ID',
            doc="""an additional marker for that state. Every dataset that
            is touched will receive the tag.""",
            constraints=EnsureStr() | EnsureNone()),
        recursive=recursion_flag,
        recursion_limit=recursion_limit,
        updated=Parameter(
            args=('-u', '--updated',),
            action='store_true',
            doc="""if given, only saves previously tracked paths."""),
        to_git=Parameter(
            args=("--to-git",),
            action='store_true',
            doc="""flag whether to add data directly to Git, instead of
            tracking data identity only.  Use with caution, there is no
            guarantee that a file put directly into Git like this will
            not be annexed in a subsequent save operation.
            If not specified, it will be up to git-annex to decide how
            a file is tracked, based on a dataset's configuration
            to track particular paths,
            file types, or file sizes with either Git or git-annex.
            (see https://git-annex.branchable.com/tips/largefiles).
            """),
        jobs=jobs_opt,
        amend=Parameter(
            args=('--amend',),
            action='store_true',
            doc="""if set, changes are not recorded in a new, separate
            commit, but are integrated with the changeset of the previous
            commit, and both together are recorded by replacing that
            previous commit. This is mutually exclusive with recursive
            operation.
            """),
    )

    @staticmethod
    @datasetmethod(name='save')
    @eval_results
    def __call__(path=None,
                 *,
                 message=None, dataset=None,
                 version_tag=None,
                 recursive=False, recursion_limit=None,
                 updated=False,
                 message_file=None,
                 to_git=None,
                 jobs=None,
                 amend=False,
                 ):
        if message and message_file:
            raise ValueError(
                "Both a message and message file were specified for save()")

        if amend and recursive:
            raise ValueError("Cannot amend a commit recursively.")

        path = ensure_list(path)

        if message_file:
            with open(message_file) as mfh:
                message = mfh.read()

        # we want 'normal' to achieve the most compact argument list
        # for git calls
        # untracked_mode = 'no' if updated else 'normal'
        # TODO however, Repo.add() would refuse to add any dotfiles
        # in a directory that is itself untracked, hence the only
        # choice is to go with potentially crazy long lists
        # until https://github.com/datalad/datalad/issues/1454
        # has a resolution
        untracked_mode = 'no' if updated else 'all'

        # there are three basic scenarios:
        # 1. save modifications to any already tracked content
        # 2. save any content (including removal of deleted content)
        #    to bring things to a clean state
        # 3. like (2), but only operate on a given subset of content
        #    identified by paths
        # - all three have to work in conjunction with --recursive
        # - the difference between (1) and (2) should be no more
        #   that a switch from --untracked=no to --untracked=all
        #   in Repo.save()

        # we do not support
        # - simultaneous operations on multiple datasets from disjoint
        #   dataset hierarchies, hence a single reference dataset must be
        #   identifiable from the either
        #   - curdir or
        #   - the `dataset` argument.
        #   This avoids complex annotation loops and hierarchy tracking.
        # - any modification upwards from the root dataset

        ds = require_dataset(dataset, check_installed=True, purpose='save')

        # use status() to do all discovery and annotation of paths
        paths_by_ds = {}
        for s in Status()(
                # ATTN: it is vital to pass the `dataset` argument as it,
                # and not a dataset instance in order to maintain the path
                # semantics between here and the status() call
                dataset=dataset,
                path=path,
                untracked=untracked_mode,
                recursive=recursive,
                recursion_limit=recursion_limit,
                on_failure='ignore',
                # for save without recursion only commit matters
                eval_subdataset_state='full' if recursive else 'commit',
                return_type='generator',
                # this could be, but for now only 'error' results are handled
                # below
                #on_failure='ignore',
                result_renderer='disabled'):
            if s['status'] == 'error':
                # Downstream code can't do anything with these. Let the caller
                # decide their fate.
                yield s
                continue

            # fish out status dict for this parent dataset
            ds_status = paths_by_ds.get(s['parentds'], {})
            # reassemble path status info as repo.status() would have made it
            ds_status[ut.Path(s['path'])] = \
                {k: v for k, v in s.items()
                 if k not in (
                     'path', 'parentds', 'refds', 'status', 'action',
                     'logger')}
            paths_by_ds[s['parentds']] = ds_status

        lgr.debug('Determined %i datasets for saving from input arguments',
                  len(paths_by_ds))
        # figure out what datasets to process, start with the ones containing
        # the paths that were given as arguments
        discovered_datasets = list(paths_by_ds.keys())
        if dataset:
            # if a reference dataset was given we want to save all the way up
            # to it, so let's throw it into the mix
            discovered_datasets.append(ds.path)
        # sort the datasets into (potentially) disjoint hierarchies,
        # or a single one, if a reference dataset was given
        dataset_hierarchies = get_tree_roots(discovered_datasets)
        for rootds, children in dataset_hierarchies.items():
            edges = {}
            discover_dataset_trace_to_targets(
                rootds, children, [], edges, includeds=children)
            for superds, subdss in edges.items():
                superds_status = paths_by_ds.get(superds, {})
                for subds in subdss:
                    subds_path = ut.Path(subds)
                    sub_status = superds_status.get(subds_path, {})
                    if not (sub_status.get("state") == "clean" and
                            sub_status.get("type") == "dataset"):
                        # start from an entry that may already exist in the
                        # status record
                        superds_status[subds_path] = superds_status.get(
                            subds_path,
                            # if we got nothing yet:
                            # shot from the hip, some status config
                            # to trigger this specific super/sub
                            # relation to be saved
                            dict(state='untracked', type='dataset')
                        )
                paths_by_ds[superds] = superds_status

        def save_ds(args, version_tag=None):
            pdspath, paths = args

            pds = Dataset(pdspath)
            pds_repo = pds.repo
            # pop status for this dataset, we are not coming back to it
            pds_status = {
                # for handing over to the low-level code, we recode any
                # path relative to the real repo location, this avoid
                # cumbersome symlink handling without context in the
                # lower levels
                pds_repo.pathobj / p.relative_to(pdspath): props
                for p, props in paths.items()}
            start_commit = pds_repo.get_hexsha()
            if not all(p['state'] == 'clean' for p in pds_status.values()) or \
                    (amend and message):
                for res in pds_repo.save_(
                        message=message,
                        # make sure to have the `path` arg be None, as we want
                        # to prevent and bypass any additional repo.status()
                        # calls
                        paths=None,
                        # prevent whining of GitRepo
                        git=True if not hasattr(ds.repo, 'uuid')
                        else to_git,
                        # we are supplying the full status already, do not
                        # detect anything else
                        untracked='no',
                        _status=pds_status,
                        amend=amend):
                    # TODO remove stringification when datalad-core can handle
                    # path objects, or when PY3.6 is the lowest supported
                    # version
                    for k in ('path', 'refds'):
                        if k in res:
                            res[k] = str(
                                # recode path back to dataset path anchor
                                pds.pathobj / res[k].relative_to(
                                    pds_repo.pathobj)
                            )
                    yield res
            # report on the dataset itself
            dsres = dict(
                action='save',
                type='dataset',
                path=pds.path,
                refds=ds.path,
                status='ok'
                if start_commit != pds_repo.get_hexsha()
                else 'notneeded',
                logger=lgr,
            )
            if not version_tag:
                yield dsres
                return
            try:
                # method requires str
                version_tag = str(version_tag)
                pds_repo.tag(version_tag)
                dsres.update(
                    status='ok',
                    version_tag=version_tag)
                yield dsres
            except CommandError as e:
                if dsres['status'] == 'ok':
                    # first we yield the result for the actual save
                    # TODO: we will get duplicate dataset/save record obscuring
                    # progress reporting.  yoh thought to decouple "tag" from "save"
                    # messages but was worrying that original authors would disagree
                    yield dsres.copy()
                # and now complain that tagging didn't work
                dsres.update(
                    status='error',
                    message=('cannot tag this version: %s', e.stderr.strip()))
                yield dsres

        if not paths_by_ds:
            # Special case: empty repo. There's either an empty commit only or
            # none at all. An empty one we can amend otherwise there's nothing
            # to do.
            if amend and ds.repo.get_hexsha():
                yield from save_ds((ds.pathobj, dict()), version_tag=version_tag)

            else:
                yield dict(action='save',
                           type='dataset',
                           path=ds.path,
                           refds=ds.path,
                           status='notneeded',
                           logger=lgr)
            return

        # TODO: in principle logging could be improved to go not by a dataset
        # but by path(s) within subdatasets. That should provide a bit better ETA
        # and more "dynamic" feedback than jumpy datasets count.
        # See addurls where it is implemented that way by providing agg and another
        # log_filter
        yield from ProducerConsumerProgressLog(
            sorted(paths_by_ds.items(), key=lambda v: v[0], reverse=True),
            partial(save_ds, version_tag=version_tag),
            safe_to_consume=no_subds_in_futures,
            producer_future_key=lambda ds_items: ds_items[0],
            jobs=jobs,
            log_filter=_log_filter_save_dataset,
            unit="datasets",
            lgr=lgr,
        )


def _log_filter_save_dataset(res):
    return res.get('type') == 'dataset' and res.get('action') == 'save'

```
--- 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