Skip to content

Commit 72c8655

Browse files
authored
stacks: refactor build_stack_graph endpoint and tests (bug 1819408) (#279)
- use prefetched revision data to retrieve stack graph - parse edges and nodes from above data - update tests and fixtures
1 parent 4bba29b commit 72c8655

File tree

6 files changed

+108
-90
lines changed

6 files changed

+108
-90
lines changed

landoapi/api/stacks.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from flask import current_app
99
from landoapi.commit_message import format_commit_message
1010
from landoapi.decorators import require_phabricator_api_key
11-
from landoapi.phabricator import PhabricatorClient, PhabricatorAPIException
11+
from landoapi.phabricator import PhabricatorClient
1212
from landoapi.projects import (
1313
get_release_managers,
1414
get_sec_approval_project_phid,
@@ -67,12 +67,7 @@ def get(phab: PhabricatorClient, revision_id: str):
6767
if revision is None:
6868
return not_found_problem
6969

70-
try:
71-
nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
72-
except PhabricatorAPIException:
73-
# If a revision within the stack causes an API exception, treat the whole stack
74-
# as not found.
75-
return not_found_problem
70+
nodes, edges = build_stack_graph(revision)
7671
stack_data = request_extended_revision_data(phab, [phid for phid in nodes])
7772

7873
supported_repos = get_repos_for_env(current_app.config.get("ENVIRONMENT"))

landoapi/api/transplants.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,7 @@ def _find_stack_from_landing_path(
130130
"The stack does not exist or you lack permission to see it.",
131131
type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
132132
)
133-
134-
# TODO: This assumes that all revisions and related objects in the stack
135-
# have uniform view permissions for the requesting user. Some revisions
136-
# being restricted could cause this to fail.
137-
return build_stack_graph(phab, phab.expect(revision, "phid"))
133+
return build_stack_graph(revision)
138134

139135

140136
def _assess_transplant_request(
@@ -524,11 +520,7 @@ def get_list(phab: PhabricatorClient, stack_revision_id: str):
524520
"The revision does not exist or you lack permission to see it.",
525521
type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
526522
)
527-
528-
# TODO: This assumes that all revisions and related objects in the stack
529-
# have uniform view permissions for the requesting user. Some revisions
530-
# being restricted could cause this to fail.
531-
nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
523+
nodes, edges = build_stack_graph(revision)
532524
revision_phids = list(nodes)
533525
revs = phab.call_conduit(
534526
"differential.revision.search",

landoapi/stacks.py

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,11 @@
2525
logger = logging.getLogger(__name__)
2626

2727

28-
def build_stack_graph(
29-
phab: PhabricatorClient, revision_phid: str
30-
) -> tuple[set[str], set[tuple[str, str]]]:
28+
def build_stack_graph(revision: dict) -> tuple[set[str], set[tuple[str, str]]]:
3129
"""Return a graph representation of a revision stack.
3230
33-
This function is expensive and can make up to approximately
34-
n/2 calls to phabricator for a linear stack where n is the
35-
number of revisions in the stack.
36-
3731
Args:
38-
phab: A landoapi.phabricator.PhabricatorClient.
39-
revision_phid: String PHID of a revision in the stack.
32+
revision: A dictionary containing Phabricator revision data.
4033
4134
Returns:
4235
A tuple of (nodes, edges). `nodes` is a set of strings
@@ -45,36 +38,13 @@ def build_stack_graph(
4538
between two nodes. `child` and `parent` are also string
4639
PHIDs.
4740
"""
48-
phids = set()
49-
new_phids = {revision_phid}
50-
edges = []
51-
52-
# Repeatedly request all related edges, adding connected revisions
53-
# each time until no new revisions are found.
54-
while new_phids:
55-
phids.update(new_phids)
56-
edges = phab.call_conduit(
57-
"edge.search",
58-
types=["revision.parent", "revision.child"],
59-
sourcePHIDs=[phid for phid in phids],
60-
limit=10000,
61-
)
62-
edges = phab.expect(edges, "data")
63-
new_phids = set()
64-
for edge in edges:
65-
new_phids.add(edge["sourcePHID"])
66-
new_phids.add(edge["destinationPHID"])
67-
68-
new_phids = new_phids - phids
69-
70-
# Treat the stack like a commit DAG, we only care about edges going
71-
# from child to parent. This is enough to represent the graph.
72-
edges = {
73-
(edge["sourcePHID"], edge["destinationPHID"])
74-
for edge in edges
75-
if edge["edgeType"] == "revision.parent"
76-
}
41+
stack_graph = PhabricatorClient.expect(revision, "fields", "stackGraph")
42+
phids = set(stack_graph.keys())
43+
edges = set()
7744

45+
for node, predecessors in stack_graph.items():
46+
for predecessor in predecessors:
47+
edges.add((node, predecessor))
7848
return phids, edges
7949

8050

landoapi/uplift.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from landoapi import bmo
2323
from landoapi.cache import cache, DEFAULT_CACHE_KEY_TIMEOUT_SECONDS
24-
from landoapi.phabricator import PhabricatorClient, PhabricatorAPIException
24+
from landoapi.phabricator import PhabricatorClient
2525
from landoapi.phabricator_patch import patch_to_changes
2626
from landoapi.repos import (
2727
Repo,
@@ -127,15 +127,7 @@ def get_uplift_conduit_state(
127127
if not revision:
128128
raise ValueError(f"No revision found with id {revision_id}")
129129

130-
try:
131-
nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
132-
except PhabricatorAPIException as e:
133-
# If a revision within the stack causes an API exception, treat the whole stack
134-
# as not found.
135-
logger.exception(
136-
f"Phabricator returned an error searching for {revision_id}: {str(e)}"
137-
)
138-
raise ValueError(f"Missing revision info for stack ending in {revision_id}")
130+
nodes, edges = build_stack_graph(revision)
139131

140132
stack_data = request_extended_revision_data(phab, [phid for phid in nodes])
141133

tests/mocks.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
import hashlib
55
import json
6+
from collections import defaultdict
67
from copy import deepcopy
78

89
from landoapi.phabricator import (
@@ -59,7 +60,7 @@ def validate_hunk(hunk):
5960
assert isinstance(hunk["corpus"], str)
6061
lines = hunk["corpus"].splitlines()
6162
assert len(lines) > 0
62-
assert all([l[0] in (" ", "-", "+") for l in lines])
63+
assert all([line[0] in (" ", "-", "+") for line in lines])
6364

6465
return True
6566

@@ -96,6 +97,51 @@ def validate_change(change):
9697
return True
9798

9899

100+
def get_stack(phid, phabdouble):
101+
phids = set()
102+
new_phids = {phid}
103+
edges = []
104+
105+
# Repeatedly request all related edges, adding connected revisions
106+
# each time until no new revisions are found.
107+
# NOTE: this was adapted from previous implementation of build_stack_graph.
108+
while new_phids:
109+
phids.update(new_phids)
110+
edges = [
111+
edge
112+
for edge in phabdouble._edges
113+
if edge["sourcePHID"] in phids
114+
and edge["edgeType"] in ("revision.parent", "revision.child")
115+
]
116+
new_phids = set()
117+
for edge in edges:
118+
new_phids.add(edge["sourcePHID"])
119+
new_phids.add(edge["destinationPHID"])
120+
121+
new_phids = new_phids - phids
122+
123+
# Treat the stack like a commit DAG, we only care about edges going
124+
# from child to parent. This is enough to represent the graph.
125+
edges = {
126+
(edge["sourcePHID"], edge["destinationPHID"])
127+
for edge in edges
128+
if edge["edgeType"] == "revision.parent"
129+
}
130+
131+
stack_graph = defaultdict(list)
132+
sources = [source for source, dest in edges]
133+
for source, dest in edges:
134+
# Check that destination phid has a corresponding source phid.
135+
if dest not in sources:
136+
# We are at a root node.
137+
stack_graph[dest] = []
138+
stack_graph[source].append(dest)
139+
if not stack_graph:
140+
# There is only one node, the root node.
141+
stack_graph[phid] = []
142+
return dict(stack_graph)
143+
144+
99145
class PhabricatorDouble:
100146
"""Phabricator test double.
101147
@@ -190,6 +236,30 @@ def api_object_for(self, mock_object: dict, **kwargs) -> dict:
190236
def get_phabricator_client():
191237
return PhabricatorClient("https://localhost", "DOESNT-MATTER")
192238

239+
def update_revision_dependencies(self, phid: str, depends_on: list[str]):
240+
"""Updates edges of `phid` so they match `depends_on`."""
241+
# Remove all previous edges related to this revision.
242+
def philter(edge):
243+
return phid not in (edge["sourcePHID"], edge["destinationPHID"])
244+
245+
self._edges = list(filter(philter, self._edges))
246+
247+
for rev in depends_on:
248+
self._edges.append(
249+
{
250+
"edgeType": "revision.parent",
251+
"sourcePHID": phid,
252+
"destinationPHID": rev["phid"],
253+
}
254+
)
255+
self._edges.append(
256+
{
257+
"edgeType": "revision.child",
258+
"sourcePHID": rev["phid"],
259+
"destinationPHID": phid,
260+
}
261+
)
262+
193263
def revision(
194264
self,
195265
*,
@@ -905,6 +975,7 @@ def to_response(i):
905975
"fields": {
906976
"title": i["title"],
907977
"authorPHID": i["authorPHID"],
978+
"stackGraph": i["stackGraph"],
908979
"status": {
909980
"value": i["status"].value,
910981
"name": i["status"].output_name,
@@ -913,6 +984,7 @@ def to_response(i):
913984
},
914985
"repositoryPHID": i["repositoryPHID"],
915986
"diffPHID": diffs[-1]["phid"],
987+
"diffID": diffs[-1]["id"],
916988
"summary": i["summary"],
917989
"dateCreated": i["dateCreated"],
918990
"dateModified": i["dateModified"],
@@ -955,7 +1027,10 @@ def to_response(i):
9551027

9561028
return deepcopy(resp)
9571029

958-
items = [r for r in self._revisions]
1030+
items = []
1031+
for revision in self._revisions:
1032+
revision["stackGraph"] = get_stack(revision["phid"], self)
1033+
items.append(revision)
9591034

9601035
if constraints and "ids" in constraints:
9611036
items = [i for i in items if i["id"] in constraints["ids"]]

0 commit comments

Comments
 (0)