summaryrefslogtreecommitdiffstats
path: root/taskcluster/gecko_taskgraph/util/hg.py
blob: d03af87f1303490b4127ebfe079f3e43e1dda187 (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
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.


import logging
import subprocess

import requests
from mozbuild.util import memoize
from redo import retry

logger = logging.getLogger(__name__)

PUSHLOG_CHANGESET_TMPL = (
    "{repository}/json-pushes?version=2&changeset={revision}&tipsonly=1"
)
PUSHLOG_PUSHES_TMPL = (
    "{repository}/json-pushes/?version=2&startID={push_id_start}&endID={push_id_end}"
)


def _query_pushlog(url):
    response = retry(
        requests.get,
        attempts=5,
        sleeptime=10,
        args=(url,),
        kwargs={"timeout": 60, "headers": {"User-Agent": "TaskCluster"}},
    )

    return response.json()["pushes"]


def find_hg_revision_push_info(repository, revision):
    """Given the parameters for this action and a revision, find the
    pushlog_id of the revision."""
    url = PUSHLOG_CHANGESET_TMPL.format(repository=repository, revision=revision)

    pushes = _query_pushlog(url)

    if len(pushes) != 1:
        raise RuntimeError(
            "Found {} pushlog_ids, expected 1, for {} revision {}: {}".format(
                len(pushes), repository, revision, pushes
            )
        )

    pushid = list(pushes.keys())[0]
    return {
        "pushdate": pushes[pushid]["date"],
        "pushid": pushid,
        "user": pushes[pushid]["user"],
    }


@memoize
def get_push_data(repository, project, push_id_start, push_id_end):
    url = PUSHLOG_PUSHES_TMPL.format(
        repository=repository,
        push_id_start=push_id_start - 1,
        push_id_end=push_id_end,
    )

    try:
        pushes = _query_pushlog(url)

        return {
            push_id: pushes[str(push_id)]
            for push_id in range(push_id_start, push_id_end + 1)
        }

    # In the event of request times out, requests will raise a TimeoutError.
    except requests.exceptions.Timeout:
        logger.warning("json-pushes timeout")

    # In the event of a network problem (e.g. DNS failure, refused connection, etc),
    # requests will raise a ConnectionError.
    except requests.exceptions.ConnectionError:
        logger.warning("json-pushes connection error")

    # In the event of the rare invalid HTTP response(e.g 404, 401),
    # requests will raise an HTTPError exception
    except requests.exceptions.HTTPError:
        logger.warning("Bad Http response")

    # When we get invalid JSON (i.e. 500 error), it results in a ValueError (bug 1313426)
    except ValueError as error:
        logger.warning(f"Invalid JSON, possible server error: {error}")

    # We just print the error out as a debug message if we failed to catch the exception above
    except requests.exceptions.RequestException as error:
        logger.warning(error)

    return None


@memoize
def get_json_pushchangedfiles(repository, revision):
    url = "{}/json-pushchangedfiles/{}".format(repository.rstrip("/"), revision)
    logger.debug("Querying version control for metadata: %s", url)

    def get_pushchangedfiles():
        response = requests.get(url, timeout=60)
        return response.json()

    return retry(get_pushchangedfiles, attempts=10, sleeptime=10)


def get_hg_revision_branch(root, revision):
    """Given the parameters for a revision, find the hg_branch (aka
    relbranch) of the revision."""
    return subprocess.check_output(
        [
            "hg",
            "identify",
            "-T",
            "{branch}",
            "--rev",
            revision,
        ],
        cwd=root,
        universal_newlines=True,
    )


# For these functions, we assume that run-task has correctly checked out the
# revision indicated by GECKO_HEAD_REF, so all that remains is to see what the
# current revision is.  Mercurial refers to that as `.`.
def get_hg_commit_message(root, rev="."):
    return subprocess.check_output(
        ["hg", "log", "-r", rev, "-T", "{desc}"], cwd=root, universal_newlines=True
    )


def calculate_head_rev(root):
    return subprocess.check_output(
        ["hg", "log", "-r", ".", "-T", "{node}"], cwd=root, universal_newlines=True
    )