• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #5131

02 Jul 2024 03:14PM CUT coverage: 21.648% (-0.001%) from 21.649%
#5131

push

coveralls-python

benjaminmah
Removed try-except block

716 of 3630 branches covered (19.72%)

0 of 1 new or added line in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1934 of 8934 relevant lines covered (21.65%)

0.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/bugbot/rules/inactive_patch_author.py
1
# # This Source Code Form is subject to the terms of the Mozilla Public
2
# # License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
# # You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
import logging
×
6
import re
×
7
from typing import Dict, List
×
8

9
from libmozdata.connection import Connection
×
10
from libmozdata.phabricator import PhabricatorAPI
×
11
from tenacity import retry, stop_after_attempt, wait_exponential
×
12

13
from bugbot import people, utils
×
14
from bugbot.bzcleaner import BzCleaner
×
15
from bugbot.nag_me import Nag
×
16
from bugbot.user_activity import PHAB_CHUNK_SIZE, UserActivity, UserStatus
×
17

18
logging.basicConfig(level=logging.DEBUG)
×
19
PHAB_FILE_NAME_PAT = re.compile(r"phabricator-D([0-9]+)-url\.txt")
×
20

21

22
class InactivePatchAuthors(BzCleaner, Nag):
×
23
    """Bugs with patches authored by inactive patch authors"""
24

25
    def __init__(self):
×
26
        super(InactivePatchAuthors, self).__init__()
×
27
        self.phab = PhabricatorAPI(utils.get_login_info()["phab_api_key"])
×
28
        self.user_activity = UserActivity(include_fields=["nick"], phab=self.phab)
×
29
        self.default_assignees = utils.get_default_assignees()
×
30
        self.people = people.People.get_instance()
×
31
        self.no_bugmail = True
×
32

33
    def description(self):
×
34
        return "Bugs with inactive patch authors"
×
35

36
    def columns(self):
×
37
        return ["id", "summary"]
×
38

39
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
40
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
41

42
        rev_ids = {rev_id for bug in bugs.values() for rev_id in bug["rev_ids"]}
×
43

NEW
44
        inactive_authors = self._get_inactive_patch_authors(list(rev_ids))
×
45

46
        for bugid, bug in list(bugs.items()):
×
47
            inactive_patches = [
×
48
                {"rev_id": rev_id, "author": inactive_authors[rev_id]}
49
                for rev_id in bug["rev_ids"]
50
                if rev_id in inactive_authors
51
            ]
52

53
            if inactive_patches:
×
54
                bug["inactive_patches"] = inactive_patches
×
55
                self.unassign_inactive_author(bugid, bug, inactive_patches)
×
56
                self.add([bug["assigned_to"], bug["triage_owner"]], bug)
×
57
            else:
58
                del bugs[bugid]
×
59

60
        return bugs
×
61

62
    def nag_template(self):
×
63
        return self.name() + ".html"
×
64

65
    def unassign_inactive_author(self, bugid, bug, inactive_patches):
×
66
        prod = bug["product"]
×
67
        comp = bug["component"]
×
68
        default_assignee = self.default_assignees[prod][comp]
×
69
        autofix = {"assigned_to": default_assignee}
×
70

71
        comment = (
×
72
            "The patch author is inactive on Bugzilla, so the assignee is being reset."
73
        )
74
        autofix["comment"] = {"body": comment}
×
75

76
        # Abandon the patches
77
        for patch in inactive_patches:
×
78
            rev_id = patch["rev_id"]
×
79
            revision = self.phab.request(
×
80
                "differential.revision.search",
81
                constraints={"ids": [rev_id]},
82
            )["data"][0]
83
            try:
×
84
                if revision["fields"]["status"]["value"] in [
×
85
                    "needs-review",
86
                    "needs-revision",
87
                    "accepted",
88
                    "changed-planned",
89
                ]:
90
                    self.phab.request(
×
91
                        "differential.revision.edit",
92
                        objectIdentifier=rev_id,
93
                        transactions=[{"type": "abandon", "value": True}],
94
                    )
95
                    logging.info(f"Abandoned patch {rev_id} for bug {bugid}.")
×
96
                else:
97
                    logging.info(f"Patch {rev_id} for bug {bugid} is already closed.")
×
98

99
            except Exception as e:
×
100
                logging.error(f"Failed to abandon patch {rev_id} for bug {bugid}: {e}")
×
101

102
        self.autofix_changes[bugid] = autofix
×
103

104
    def _get_inactive_patch_authors(self, rev_ids: list) -> Dict[int, dict]:
×
105
        revisions: List[dict] = []
×
106

107
        for _rev_ids in Connection.chunks(rev_ids, PHAB_CHUNK_SIZE):
×
108
            try:
×
109
                for revision in self._fetch_revisions(_rev_ids):
×
110
                    author_phid = revision["fields"]["authorPHID"]
×
111
                    created_at = revision["fields"]["dateCreated"]
×
112
                    # if author_phid == "PHID-USER-eltrc7x5oplwzfguutrb":
113
                    #     continue
114
                    revisions.append(
×
115
                        {
116
                            "rev_id": revision["id"],
117
                            "author_phid": author_phid,
118
                            "created_at": created_at,
119
                            "status": revision["fields"]["status"]["value"],
120
                        }
121
                    )
122
            except Exception as e:
×
123
                logging.error(f"Error fetching revisions: {e}")
×
124
                continue
×
125

126
        user_phids = set()
×
127

128
        for revision in revisions:
×
129
            user_phids.add(revision["author_phid"])
×
130

131
        users = self.user_activity.get_phab_users_with_status(
×
132
            list(user_phids), keep_active=False
133
        )
134

135
        result: Dict[int, dict] = {}
×
136
        for revision in revisions:
×
137
            author_phid = revision["author_phid"]
×
138

139
            if author_phid not in users:
×
140
                continue
×
141

142
            author_info = users[author_phid]
×
143
            if author_info["status"] == UserStatus.INACTIVE:
×
144
                result[revision["rev_id"]] = {
×
145
                    "name": author_info["name"],
146
                    "status": author_info["status"],
147
                    "last_active": author_info.get("last_seen_date"),
148
                }
149

150
        return result
×
151

152
    @retry(
×
153
        wait=wait_exponential(min=4),
154
        stop=stop_after_attempt(5),
155
    )
156
    def _fetch_revisions(self, ids: list):
×
157
        return self.phab.request(
×
158
            "differential.revision.search",
159
            constraints={"ids": ids},
160
        )["data"]
161

162
    def handle_bug(self, bug, data):
×
163
        rev_ids = [
×
164
            int(attachment["file_name"][13:-8])
165
            for attachment in bug["attachments"]
166
            if attachment["content_type"] == "text/x-phabricator-request"
167
            and PHAB_FILE_NAME_PAT.match(attachment["file_name"])
168
            and not attachment["is_obsolete"]
169
        ]
170

171
        if not rev_ids:
×
172
            return
×
173

174
        bugid = str(bug["id"])
×
175
        data[bugid] = {
×
176
            "rev_ids": rev_ids,
177
            "product": bug["product"],
178
            "component": bug["component"],
179
            "assigned_to": bug["assigned_to"],
180
            "triage_owner": bug["triage_owner"],
181
        }
182
        return bug
×
183

184
    def get_bz_params(self, date):
×
185
        fields = [
×
186
            "comments.raw_text",
187
            "comments.creator",
188
            "attachments.file_name",
189
            "attachments.content_type",
190
            "attachments.is_obsolete",
191
            "product",
192
            "component",
193
            "assigned_to",
194
            "triage_owner",
195
        ]
196
        params = {
×
197
            "include_fields": fields,
198
            "resolution": "---",
199
            "f1": "attachments.ispatch",
200
            "o1": "equals",
201
            "v1": "1",
202
        }
203

204
        return params
×
205

206

207
if __name__ == "__main__":
×
208
    InactivePatchAuthors().run()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc