summaryrefslogtreecommitdiff
path: root/src/setuptools_scm/hg_git.py
blob: 461215b42ed4ba88710ad0d9917f7fe04110f9c1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
from __future__ import annotations

import logging
import os
from contextlib import suppress
from datetime import date
from datetime import datetime

from . import _types as _t
from .git import GitWorkdir
from .hg import HgWorkdir
from .utils import _CmdResult
from .utils import do_ex
from .utils import require_command

log = logging.getLogger(__name__)

_FAKE_GIT_DESCRIBE_ERROR = _CmdResult("<>hg git failed", "", 1)


class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
    COMMAND = "hg"

    @classmethod
    def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None:
        require_command(cls.COMMAND)
        root, _, ret = do_ex(["hg", "root"], wd)
        if ret:
            return None
        return cls(root)

    def is_dirty(self) -> bool:
        out, _, _ = self.do_ex('hg id -T "{dirty}"')
        return bool(out)

    def get_branch(self) -> str | None:
        res = self.do_ex('hg id -T "{bookmarks}"')
        if res.returncode:
            log.info("branch err %s", res)
            return None
        return res.out

    def get_head_date(self) -> date | None:
        date_part, err, ret = self.do_ex('hg log -r . -T "{shortdate(date)}"')
        if ret:
            log.info("head date err %s %s %s", date_part, err, ret)
            return None
        return datetime.strptime(date_part, r"%Y-%m-%d").date()

    def is_shallow(self) -> bool:
        return False

    def fetch_shallow(self) -> None:
        pass

    def get_hg_node(self) -> str | None:
        node, _, ret = self.do_ex('hg log -r . -T "{node}"')
        if not ret:
            return node
        else:
            return None

    def _hg2git(self, hg_node: str) -> str | None:
        with suppress(FileNotFoundError):
            with open(os.path.join(self.path, ".hg/git-mapfile")) as map_items:
                for item in map_items:
                    if hg_node in item:
                        git_node, hg_node = item.split()
                        return git_node
        return None

    def node(self) -> str | None:
        hg_node = self.get_hg_node()
        if hg_node is None:
            return None

        git_node = self._hg2git(hg_node)

        if git_node is None:
            # trying again after hg -> git
            self.do_ex("hg gexport")
            git_node = self._hg2git(hg_node)

            if git_node is None:
                log.debug("Cannot get git node so we use hg node %s", hg_node)

                if hg_node == "0" * len(hg_node):
                    # mimic Git behavior
                    return None

                return hg_node

        return git_node[:7]

    def count_all_nodes(self) -> int:
        revs, _, _ = self.do_ex(["hg", "log", "-r", "ancestors(.)", "-T", "."])
        return len(revs)

    def default_describe(self) -> _CmdResult:
        """
        Tentative to reproduce the output of

        `git describe --dirty --tags --long --match *[0-9]*`

        """
        hg_tags_str, _, ret = self.do_ex(
            [
                "hg",
                "log",
                "-r",
                "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))",
                "-T",
                "{tags}{if(tags, ' ', '')}",
            ]
        )
        if ret:
            return _FAKE_GIT_DESCRIBE_ERROR
        hg_tags: list[str] = hg_tags_str.split()

        if not hg_tags:
            return _FAKE_GIT_DESCRIBE_ERROR

        with open(os.path.join(self.path, ".hg/git-tags")) as fp:
            git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp)

        tag: str
        for hg_tag in hg_tags:
            if hg_tag in git_tags:
                tag = hg_tag
                break
        else:
            logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
            return _FAKE_GIT_DESCRIBE_ERROR

        out, _, ret = self.do_ex(["hg", "log", "-r", f"'{tag}'::.", "-T", "."])
        if ret:
            return _FAKE_GIT_DESCRIBE_ERROR
        distance = len(out) - 1

        node = self.node()
        assert node is not None
        desc = f"{tag}-{distance}-g{node}"

        if self.is_dirty():
            desc += "-dirty"
        log.debug("faked describe %r", desc)
        return _CmdResult(desc, "", 0)