# -----------------------------------------------------------------------------
# Copyright (C) 2026 Iliyas Jorio.
# This file is part of GitFourchette, distributed under the GNU GPL v3.
# For full terms, see the included LICENSE file.
# -----------------------------------------------------------------------------

import logging
import os
import shutil
from itertools import chain
from pathlib import Path

from gitfourchette import settings
from gitfourchette.exttools.mergedriver import MergeDriver
from gitfourchette.exttools.toolprocess import ToolProcess
from gitfourchette.gitdriver import *
from gitfourchette.localization import *
from gitfourchette.nav import NavLocator
from gitfourchette.porcelain import *
from gitfourchette.qt import *
from gitfourchette.tasks.repotask import AbortTask, RepoTask, TaskEffects
from gitfourchette.toolbox import *
from gitfourchette.trash import Trash
from gitfourchette.trtables import TrTables

logger = logging.getLogger(__name__)


class _BaseStagingTask(RepoTask):
    def canKill(self, task: RepoTask):
        # Jump/Refresh tasks shouldn't prevent a staging task from starting
        # when the user holds down RETURN/DELETE in a FileListView
        # to stage/unstage a series of files.
        from gitfourchette import tasks
        return isinstance(task, tasks.Jump | tasks.RefreshRepo)

    def denyConflicts(self, deltas: list[GitDelta], purpose: PatchPurpose):
        # Filter on conflicts
        conflictPaths = [d.new.path for d in deltas if d.conflict is not None]

        # Bail if no conflicts
        if not conflictPaths:
            return

        numPatches = len(deltas)
        numConflicts = len(conflictPaths)

        if numPatches == numConflicts:
            intro = _n("You have selected an unresolved merge conflict.",
                       "You have selected {n} unresolved merge conflicts.", numConflicts)
        else:
            intro = _n("There is an unresolved merge conflict among your selection.",
                       "There are {n} unresolved merge conflicts among your selection.", numConflicts)

        if purpose == PatchPurpose.Stage:
            please = _np("please fix (the merge conflicts)", "Please fix it before staging:", "Please fix them before staging:", numConflicts)
        else:
            please = _np("please fix (the merge conflicts)", "Please fix it before discarding:", "Please fix them before discarding:", numConflicts)

        message = paragraphs(intro, please)
        message += toTightUL(conflictPaths)
        raise AbortTask(message)


class StageFiles(_BaseStagingTask):
    def flow(self, deltas: list[GitDelta]):
        if not deltas:  # Nothing to stage (may happen if user keeps pressing Enter in file list view)
            QApplication.beep()
            raise AbortTask()

        self.denyConflicts(deltas, PatchPurpose.Stage)

        paths = [d.new.path for d in deltas]

        self.epilog.effects |= TaskEffects.Workdir
        yield from self.flowCallGit("add", "--", *paths)

        yield from self.debriefPostStage(deltas)

        self.epilog.status = _n("File staged.", "{n} files staged.", len(deltas))

    def debriefPostStage(self, deltas: list[GitDelta]):
        debrief = {}

        for delta in deltas:
            # Staging a tree that isn't registered as a submodule
            if delta.new.mode == FileMode.TREE:
                m = _("You’ve added another Git repo inside your current repo. "
                      "It is STRONGLY RECOMMENDED to absorb it as a submodule before committing.")

            # Staging a submodule with uncommitted changes within
            elif delta.new.mode == FileMode.COMMIT and delta.submoduleWorkdirDirty:
                m = _("Uncommitted changes in the submodule can’t be staged from the parent repository.")

            # Staging a submodule deletion, and the submodule is still in .gitmodules
            elif (delta.status == "D"
                  and delta.old.mode == FileMode.COMMIT
                  and delta.old.path in self.repo.listall_submodules_dict_at_head()):
                m = _("Don’t forget to remove the submodule from {0} to complete its deletion.", tquo(DOT_GITMODULES))

            # Otherwise, no special message
            else:
                continue

            debrief[delta.new.path] = m

        if not debrief:
            return

        # For better perceived responsivity, show message box asynchronously
        # so that RefreshRepo occurs in the background after the task completes
        yield from self.flowEnterUiThread()
        qmb = asyncMessageBox(
            self.parentWidget(),
            'information',
            self.name(),
            _n("An item requires your attention after staging:", "{n} items require your attention after staging:", len(debrief)))
        addULToMessageBox(qmb, [f"{btag(path)}: {issue}" for path, issue in debrief.items()])
        qmb.show()


class DiscardFiles(_BaseStagingTask):
    def flow(self, deltas: list[GitDelta]):
        assert all(d.source == GitDeltaSource.Dirty for d in deltas)

        textPara = []
        really = ""
        verb = _("Discard changes")

        if not deltas:  # Nothing to discard (may happen if user keeps pressing Delete in file list view)
            QApplication.beep()
            raise AbortTask()

        self.denyConflicts(deltas, PatchPurpose.Discard)

        # Filter on submodules
        submoPaths = [d.new.path for d in deltas if d.isSubtreeCommitPatch()]
        numSubmos = len(submoPaths)

        if len(deltas) == 1:
            delta = deltas[0]
            bpath = bquo(delta.new.path)
            if delta.status == "?":  # untracked
                really = _("Really delete {0}?", bpath)
                really += " " + _("Git isn’t tracking this file, so you may not be able to recover it from older commits.")
                verb = _("Delete")
            elif delta.new.mode == FileMode.COMMIT:  # modeWorktree
                really = _("Really discard changes in submodule {0}?", bpath)
            else:
                really = _("Really discard changes to {0}?", bpath)
        else:
            numFiles = len(deltas) - numSubmos
            if numSubmos and numFiles:
                really = _("Really discard changes to {nf} files and in {ns} submodules?", nf=numFiles, ns=numSubmos)
            elif numSubmos:
                really = _("Really discard changes in {n} submodules?", n=numSubmos)
            else:
                really = _("Really discard changes to {n} files?", n=numFiles)

        textPara.append(really)
        if numSubmos:
            submoPostamble = _n(
                "Any uncommitted changes in the submodule will be <b>cleared</b> and the submodule’s HEAD will be reset.",
                "Any uncommitted changes in {n} submodules will be <b>cleared</b> and the submodules’ HEAD will be reset.",
                numSubmos)
            textPara.append(submoPostamble)

        textPara.append(_("This cannot be undone!"))

        yield from self.flowConfirm(text=paragraphs(*textPara), verb=verb, buttonIcon="git-discard")

        self.epilog.effects |= TaskEffects.Workdir
        if numSubmos:
            self.epilog.effects |= TaskEffects.Refs  # We don't have TaskEffects.Submodules so .Refs is the next best thing

        # Back up discarded patches
        if Trash.enabled():
            for delta in deltas:
                try:
                    yield from self._backupDelta(delta)
                except Trash.BackupSkipped as ex:
                    logger.warning(f"Backup skipped: {ex}")

        tracked = [d.new.path for d in deltas if d.status != "?"]
        untrackedFiles = [d.new.path for d in deltas if d.status == "?" and d.new.mode != FileMode.TREE]
        untrackedTrees = [d.new.path for d in deltas if d.status == "?" and d.new.mode == FileMode.TREE]

        # Discard untracked trees. They have already been backed up above,
        # but restore_files_from_index isn't capable of removing trees.
        for untrackedTree in untrackedTrees:
            untrackedTreePath = self.repo.in_workdir(untrackedTree)
            assert os.path.isdir(untrackedTreePath)
            shutil.rmtree(untrackedTreePath)

        if untrackedFiles:
            yield from self.flowCallGit("clean", "--force", "--", *untrackedFiles)
        if tracked:
            yield from self.flowCallGit("checkout", "--", *tracked)

        if submoPaths:
            for submo in submoPaths:
                subWd = os.path.join(self.repo.workdir, submo)
                yield from self.flowCallGit("clean", "-d", "--force", workdir=subWd)
            yield from self.flowCallGit("submodule", "update", "--force", "--init", "--recursive", "--checkout", "--", *submoPaths)

        self.epilog.status = _n("File discarded.", "{n} files discarded.", len(deltas))

    def _backupDelta(self, delta: GitDelta):
        # Don't back up deletions
        if delta.status == "D":
            return

        trash = Trash.instance()
        path = delta.new.path
        workdir = self.repo.workdir

        if delta.status == "?":
            if delta.new.mode == FileMode.TREE:
                # Untracked tree
                trash.backupTree(workdir, path)
            else:
                # Untracked file
                # TODO: Also binary files?
                trash.backupFile(workdir, path)
        else:
            # TODO: Cache patch in GitDelta? So we don't have to regenerate the patch if we've already displayed it
            tokens = GitDriver.buildDiffCommand(delta)
            driver = yield from self.flowCallGit(*tokens, autoFail=False)
            patchText = driver.stdoutScrollback()
            trash.backupPatch(workdir, patchText, path)


class UnstageFiles(_BaseStagingTask):
    def flow(self, deltas: list[GitDelta]):
        if not deltas:  # Nothing to unstage (may happen if user keeps pressing Delete in file list view)
            QApplication.beep()
            raise AbortTask()

        paths = []
        for delta in deltas:
            paths.append(delta.new.path)
            if delta.status == "R":
                paths.append(delta.old.path)

        self.epilog.effects |= TaskEffects.Workdir
        # Not using 'restore --staged' because it doesn't work in an empty repo
        yield from self.flowCallGit("reset", "--", *paths)

        self.epilog.status = _n("File unstaged.", "{n} files unstaged.", len(deltas))


class DiscardModeChanges(_BaseStagingTask):
    def flow(self, deltas: list[GitDelta]):
        paths = [delta.new.path for delta in deltas]
        numFiles = len(paths)

        if numFiles == 0:  # Nothing to unstage (may happen if user keeps pressing Delete in file list view)
            QApplication.beep()
            raise AbortTask()

        text = paragraphs(
            (_("Really discard mode change in {0}?", bquo(paths[0]))
             if numFiles == 1 else
             _("Really discard mode changes in <b>{n} files</b>?", n=numFiles)),
            _("This cannot be undone!"))

        yield from self.flowConfirm(text=text, verb=_("Discard mode changes"), buttonIcon="git-discard")

        yield from self.flowEnterWorkerThread()
        self.epilog.effects |= TaskEffects.Workdir
        self.repo.discard_mode_changes(paths)


class UnstageModeChanges(_BaseStagingTask):
    def flow(self, deltas: list[GitDelta]):
        if not deltas:  # Nothing to unstage (may happen if user keeps pressing Delete in file list view)
            QApplication.beep()
            raise AbortTask()

        yield from self.flowEnterWorkerThread()
        self.epilog.effects |= TaskEffects.Workdir

        self.repo.refresh_index()
        index = self.repo.index
        for delta in deltas:
            of = delta.old
            nf = delta.new
            if (of.mode != nf.mode
                    and delta.status not in "AD?"  # ADDED, DELETED, UNTRACKED
                    and of.mode in [FileMode.BLOB, FileMode.BLOB_EXECUTABLE]):
                index.add(IndexEntry(nf.path, Oid(hex=nf.id), of.mode))
        index.write()


class ApplyPatch(RepoTask):
    def flow(self, delta: GitDelta, subPatch: str, purpose: PatchPurpose):
        if not subPatch:
            QApplication.beep()
            verb = TrTables.enum(purpose & PatchPurpose.VerbMask).lower()
            message = _("Can’t {verb} the selection because no red/green lines are selected.", verb=verb)
            raise AbortTask(message, asStatusMessage=True)

        if purpose & PatchPurpose.Discard:
            title = TrTables.enum(purpose)
            isHunk = purpose & PatchPurpose.Hunk
            text = paragraphs(
                _("Really discard this hunk?") if isHunk else _("Really discard the selected lines?"),
                _("This cannot be undone!"))
            yield from self.flowConfirm(title, text=text, verb=title, buttonIcon="git-discard-lines")

            try:
                Trash.instance().backupPatch(self.repo.workdir, subPatch, delta.new.path)
            except Trash.BackupSkipped as ex:
                logger.warning(f"Backup skipped: {ex}")

            applyLocation = ApplyLocation.WORKDIR
        else:
            applyLocation = ApplyLocation.INDEX

        yield from self.flowEnterWorkerThread()
        self.epilog.effects |= TaskEffects.Workdir

        # libgit2's index must be fresh in order to apply a partial patch to the index.
        if applyLocation == ApplyLocation.INDEX:
            self.repo.refresh_index()

        self.repo.apply(subPatch, applyLocation)

        self.epilog.status = TrTables.patchPurposePastTense(purpose)


class HardSolveConflicts(RepoTask):
    def flow(self, ours: list[str], theirs: list[str], remove: list[str]):
        # Back up affected files
        if Trash.enabled():
            for path in chain(theirs, remove):
                try:
                    Trash.instance().backupFile(self.repo.workdir, path)
                except Trash.BackupSkipped as ex:
                    logger.warning(f"Backup skipped: {ex}")

        # Restore desired sides
        self.epilog.effects |= TaskEffects.Workdir

        if ours:
            yield from self.flowCallGit("restore", "--progress", "--ours", "--", *ours)

        if theirs:
            yield from self.flowCallGit("restore", "--progress", "--theirs", "--", *theirs)

        # Stage the files we kept to resolve the conflict
        if ours or theirs:
            yield from self.flowCallGit("add", "--force", "--", *chain(ours, theirs))

        # Stage deletions to resolve the conflict
        if remove:
            yield from self.flowCallGit("rm", "--", *remove)

        # Jump to any staged file after the task
        for path in chain(ours, theirs):
            if Path(self.repo.in_workdir(path)).is_file():
                self.epilog.jumpTo = NavLocator.inStaged(path)
                break

        self.epilog.status = _n("Conflict resolved.", "{n} conflicts resolved.", len(ours) + len(theirs) + len(remove))


class OpenMergeTool(RepoTask):
    def flow(self, conflict: GitConflict, reopenWorkInProgress: bool):
        mergeDriver = MergeDriver.findOngoingMerge(conflict)

        if mergeDriver is None:
            mergeDriver = yield from self.makeMergeDriver(conflict)

        mergeDriver.startProcess(reopenWorkInProgress)
        return mergeDriver

    def makeMergeDriver(self, conflict: GitConflict):
        mergeTempDir = QTemporaryDir(str(Path(qTempDir(), "merge")))
        into = mergeTempDir.path()
        repo = self.repo

        def dump(df: GitDeltaFile, prefix: str):
            path = yield from self.flowSubtask(SaveDeltaFileAs, df, saveIntoDir=into, prefix=prefix)
            return path

        targetPath = repo.in_workdir(conflict.ours.path)
        baseName = Path(targetPath).name

        # Dump OURS and THEIRS blobs into the temporary directory
        oursPath = yield from dump(conflict.ours, prefix="[OURS]")
        theirsPath = yield from dump(conflict.theirs, prefix="[THEIRS]")

        if conflict.ancestor:
            # Dump ANCESTOR blob into the temporary directory
            ancestorPath = yield from dump(conflict.ancestor, prefix="[ANCESTOR]")
        else:
            # There's no ancestor! Some merge tools can fake a 3-way merge without
            # an ancestor (e.g. PyCharm), but others won't (e.g. VS Code).
            # To make sure we get a 3-way merge, copy our current workdir file as
            # the fake ANCESTOR file. It should contain chevron conflict markers
            # (<<<<<<< >>>>>>>) which should trigger conflicts between OURS and
            # THEIRS in the merge tool.
            ancestorPath = str(Path(into, f"[NO-ANCESTOR]{baseName}"))
            shutil.copyfile(targetPath, ancestorPath)

        # Create scratch file (merge tool output).
        # Some merge tools (such as VS Code) use the contents of this file
        # as a starting point, so copy the workdir version for this purpose.
        scratchPath = Path(into, f"[MERGED]{baseName}")
        shutil.copyfile(targetPath, scratchPath)

        paths = MergeDriver.MergeFiles(
            ours=oursPath,
            theirs=theirsPath,
            ancestor=ancestorPath,
            scratch=str(scratchPath),
            target=targetPath)

        mergeDriver = MergeDriver(self.parentWidget(), conflict, paths)

        # Keep a reference to temp dir so it doesn't vanish
        mergeDriver._keepAroundMergeTempDir = mergeTempDir  # type: ignore[attr-defined]

        return mergeDriver


class AcceptMergeConflictResolution(RepoTask):
    def canKill(self, task: RepoTask) -> bool:
        from gitfourchette.tasks import RefreshRepo, Jump
        return isinstance(task, RefreshRepo | Jump)

    def flow(self, mergeDriver: MergeDriver):
        self.epilog.effects |= TaskEffects.Workdir
        path = mergeDriver.conflict.ours.path
        mergeDriver.copyScratchToTarget()
        mergeDriver.deleteNow()
        yield from self.flowCallGit("add", "--force", "--", path)

        # Jump to staged file after confirming conflict resolution
        self.epilog.jumpTo = NavLocator.inStaged(path)
        self.epilog.status = _("Merge conflict resolved in {0}.", tquo(path))


class ApplyPatchFile(RepoTask):
    def flow(self, path: str = ""):
        yield from ApplyPatchFile.do(self, path=path)

    @staticmethod
    def do(
            task: RepoTask,
            path: str = "",
            reverse: bool = False,
            context: int = -1,
            title: str = "",
            question: str = "",
    ):
        # Fallback title
        title = title or task.name()

        # If no path, bring up file dialog
        if not path:
            patchFileCaption = _("Patch file")
            allFilesCaption = _("All files")

            qfd = PersistentFileDialog.openFile(
                task.parentWidget(), "OpenPatch", title,
                filter=f"{patchFileCaption} (*.patch);;{allFilesCaption} (*)")
            path = yield from task.flowFileDialog(qfd)

        # Fallback question
        if not question:
            verb = _("revert") if reverse else _("apply")
            basename = Path(path).name
            question = _("Do you want to {verb} patch file {path}?",
                         verb=btag(verb), path=bquoe(basename))

        # Build command
        stem = [
            "apply",
            *argsIf(reverse, "--reverse"),
            *argsIf(context >= 0, f"-C{context}"),
            path
        ]

        # Do a dry run first.
        driver = yield from task.flowCallGit(*stem, "--numstat", "-z", "--check")

        table = driver.stdoutTableNumstatZ()
        numFiles = len(table)
        details = []
        firstFile = ""
        for adds, dels, patchFile in table:
            if adds == "-" or dels == "-":
                details.append(_("(binary)") + " " + escape(patchFile))
            else:
                details.append(f"(<add>+{adds}</add> <del>-{dels}</del>) {escape(patchFile)}")
            firstFile = firstFile or patchFile

        addDelStyle = settings.prefs.addDelColorsStyleTag()
        listIntro = _n("<b>{n}</b> file will be modified in your working directory:",
                       "<b>{n}</b> files will be modified in your working directory:", n=numFiles)
        confirmText = addDelStyle + paragraphs(question, listIntro)

        yield from task.flowConfirm(title, confirmText, verb=_("Apply"), detailList=details)

        # Dry run confirmed, go ahead
        task.epilog.effects |= TaskEffects.Workdir

        yield from task.flowCallGit(*stem)

        task.epilog.jumpTo = NavLocator.inUnstaged(firstFile)
        task.epilog.status = _n("{n} file modified in the working directory.",
                                "{n} files modified in the working directory.", n=numFiles)


class ApplyPatchFileReverse(RepoTask):
    def flow(self, path: str = ""):
        yield from ApplyPatchFile.do(self, path=path, reverse=True)


class ApplyPatchData(RepoTask):
    def flow(self, patchData: str, title: str, question: str, reverse: bool = False, context: int = -1):
        assert isinstance(patchData, str), "patchData should be str"

        if not patchData:
            raise AbortTask(_("There’s nothing to apply in the selection."))

        template = os.path.join(qTempDir(), self.__class__.__name__ + "-XXXXXX.patch")
        tempPatch = QTemporaryFile(template, self)
        tempPatch.open()
        tempPatch.write(patchData.encode("utf-8"))
        tempPatch.close()
        path = tempPatch.fileName()

        yield from ApplyPatchFile.do(
            self,
            path=path,
            reverse=reverse,
            context=context,
            title=title,
            question=question)


class RestoreRevisionToWorkdir(RepoTask):
    def flow(self, delta: GitDelta, old: bool):
        if old:
            preposition = _p("preposition slotted into '...BEFORE this commit'", "before")
            diffFile = delta.old
            delete = delta.status == "A"
        else:
            preposition = _p("preposition slotted into '...AT this commit'", "at")
            diffFile = delta.new
            delete = delta.status == "D"

        path = self.repo.in_workdir(diffFile.path)
        pathObj = Path(path)
        existsNow = pathObj.is_file()

        if not existsNow and delete:
            message = _("Your working copy of {path} already matches the revision {preposition} this commit.",
                        path=bquo(diffFile.path), preposition=preposition)
            raise AbortTask(message, icon="information")

        if not existsNow:
            actionVerb = _("recreated")
        elif delete:
            actionVerb = _("deleted")
        else:
            actionVerb = _("overwritten")
        prompt = paragraphs(
            _("Do you want to restore {path} as it was {preposition} this commit?",
              path=bquo(diffFile.path), preposition=preposition),
            _("This file will be {processed} in your working directory.", processed=actionVerb))

        yield from self.flowConfirm(text=prompt, verb=_("Restore"))

        self.epilog.effects |= TaskEffects.Workdir

        if delete:
            pathObj.unlink()
        else:
            assert diffFile.sourceCommit not in [None, NULL_OID]
            yield from self.flowCallGit(
                "restore",
                "--progress",
                f"--source={diffFile.sourceCommit}",
                "--",
                diffFile.path)

        self.epilog.status = _("File {path} {processed}.", path=tquoe(diffFile.path), processed=actionVerb)
        self.epilog.jumpTo = NavLocator.inUnstaged(diffFile.path)


class SaveDeltaFileAs(RepoTask):
    def flow(self, file: GitDeltaFile, saveIntoDir: str = "", prefix: str = ""):
        dfPath = Path(file.path)
        suggStem = prefix + dfPath.stem

        if file.source == GitDeltaSource.Commit:
            assert file.sourceCommit not in [None, NULL_OID]
            suggStem += f"@{shortHash(file.sourceCommit)}"
            catFileArgs = ["--filters", f"{file.sourceCommit}:{file.path}"]  # --filters for LFS awareness
        elif file.source == GitDeltaSource.Index:
            catFileArgs = ["--filters", f":{file.path}"]  # --filters for LFS awareness
        elif file.source == GitDeltaSource.Unknown:  # Most likely from GitConflict
            assert file.isIdValid()
            catFileArgs = ["blob", str(file.id)]
        else:
            raise NotImplementedError()

        suggExt = dfPath.suffix
        suggName = suggStem + suggExt

        if saveIntoDir:
            targetStr = withUniqueSuffix(suggStem, ext=suggExt, reserved=lambda s: Path(saveIntoDir, s).exists())
            targetStr = str(Path(saveIntoDir, targetStr))
        else:
            qfd = PersistentFileDialog.saveFile(self.parentWidget(), "SaveFile", _("Save file revision as"), suggName)
            targetStr = yield from self.flowFileDialog(qfd)
        target = Path(targetStr)

        driver = yield from self.flowCallGit("cat-file", *catFileArgs)

        assert not driver._stdout, "stdout consumed prematurely"
        data = driver.readAllStandardOutput().data()
        target.write_bytes(data)

        if file.mode == FileMode.BLOB_EXECUTABLE:
            mode = 0o100 | target.lstat().st_mode
            target.lchmod(mode)

        return str(target)


class SaveRevisionAs(RepoTask):
    def flow(self, delta: GitDelta, old: bool, saveIntoDir: str = "", prefix: str = ""):
        if old:
            diffFile = delta.old
            if delta.status == "A":
                raise AbortTask(_("This file didn’t exist before the commit."))
        else:
            diffFile = delta.new
            if delta.status == "D":
                raise AbortTask(_("This file was deleted by the commit."))

        outPath = yield from self.flowSubtask(SaveDeltaFileAs, diffFile, saveIntoDir, prefix)
        return outPath


class OpenRevisionInEditor(RepoTask):
    def flow(self, delta: GitDelta, old: bool):
        path = yield from self.flowSubtask(SaveRevisionAs, delta, old, qTempDir())
        assert isinstance(path, str)

        ToolProcess.startTextEditor(self.parentWidget(), path)


class OpenInDiffTool(RepoTask):
    def flow(self, delta: GitDelta):
        if delta.new.isId0():
            raise FileNotFoundError(_("Can’t open external diff tool on a deleted file."))

        if delta.old.isId0():
            raise FileNotFoundError(_("Can’t open external diff tool on a new file."))

        into = qTempDir()

        def dump(file: GitDeltaFile, prefix: str):
            path = yield from self.flowSubtask(SaveDeltaFileAs, file, saveIntoDir=into, prefix=prefix)
            return path

        if delta.source == GitDeltaSource.Dirty:
            # Unstaged: compare indexed state to workdir file
            oldPath = yield from dump(delta.old, "[INDEXED]")
            newPath = self.repo.in_workdir(delta.new.path)
        elif delta.source == GitDeltaSource.Index:
            # Staged: compare HEAD state to indexed state
            oldPath = yield from dump(delta.old, "[HEAD]")
            newPath = yield from dump(delta.new, "[STAGED]")
        elif delta.source == GitDeltaSource.Commit:
            # Committed: compare parent state to this commit
            oldPath = yield from dump(delta.old, "[OLD]")
            newPath = yield from dump(delta.new, "[NEW]")
        else:
            raise NotImplementedError(f"unsupported source {delta.source}")

        return ToolProcess.startDiffTool(self.parentWidget(), oldPath, newPath)


class AbortMerge(RepoTask):
    def flow(self):
        self.repo.refresh_index()

        isMerging = self.repo.state() == RepositoryState.MERGE
        isCherryPicking = self.repo.state() == RepositoryState.CHERRYPICK
        isReverting = self.repo.state() == RepositoryState.REVERT
        anyConflicts = self.repo.index.conflicts

        if not (isMerging or isCherryPicking or isReverting or anyConflicts):
            raise AbortTask(_("No abortable state is in progress."), icon='information')

        if isCherryPicking:
            clause = _("abort the ongoing cherry-pick")
            title = _("Abort cherry-pick")
            postStatus = _("Cherry-pick aborted.")
            gitCommand = ["cherry-pick", "--abort"]
        elif isMerging:
            clause = _("abort the ongoing merge")
            title = _("Abort merge")
            postStatus = _("Merge aborted.")
            gitCommand = ["merge", "--abort"]
        elif isReverting:
            clause = _("abort the ongoing revert")
            title = _("Abort revert")
            postStatus = _("Revert aborted.")
            gitCommand = ["revert", "--abort"]
        else:
            clause = _("reset the index")
            title = _("Reset index")
            postStatus = _("Index reset.")
            gitCommand = ["reset", "--merge"]

        try:
            abortList = self.repo.get_reset_merge_file_list()
        except MultiFileError as exc:
            exc.message = _n(
                "Cannot {verb} right now, because a file contains both staged and unstaged changes.",
                "Cannot {verb} right now, because {n} files contain both staged and unstaged changes.",
                n=len(exc.file_exceptions), verb=clause)
            exc.message += " " + _("Please unstage the changes and try again.")
            raise  # re-raise MultiFileError

        lines = [_("Do you want to {0}?", clause)]

        if not abortList:
            lines.append(_("No files are affected."))
        else:
            if anyConflicts:
                lines.append(_("All conflicts will be cleared and all <b>staged</b> changes will be lost."))
            else:
                lines.append(_("All <b>staged</b> changes will be lost."))
            lines.append(_n("This file will be reset:", "{n} files will be reset:", len(abortList)))

        yield from self.flowConfirm(title=title, text=paragraphs(*lines), verb=englishTitleCase(title),
                                    detailList=[escape(f) for f in abortList])

        self.epilog.effects |= TaskEffects.DefaultRefresh

        yield from self.flowCallGit(*gitCommand)
        # self.repo.reset_merge()
        # self.repo.state_cleanup()

        self.epilog.status = postStatus

        # Clear draft commit message that was set in CherrypickCommit/RevertCommit/MergeBranch
        if isCherryPicking or isReverting or isMerging:
            self.repoModel.prefs.clearDraftCommit()
