vcsgraph 0.3.3__cp315-cp315-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vcsgraph/__init__.py +49 -0
- vcsgraph/_graph_rs.cp315-win_amd64.pyd +0 -0
- vcsgraph/errors.py +180 -0
- vcsgraph/graph.py +338 -0
- vcsgraph/known_graph.py +24 -0
- vcsgraph/py.typed +0 -0
- vcsgraph/tests/__init__.py +60 -0
- vcsgraph/tests/test_graph.py +1964 -0
- vcsgraph/tests/test_known_graph.py +891 -0
- vcsgraph/tests/test_tsort.py +709 -0
- vcsgraph/tsort.py +46 -0
- vcsgraph-0.3.3.dist-info/METADATA +130 -0
- vcsgraph-0.3.3.dist-info/RECORD +16 -0
- vcsgraph-0.3.3.dist-info/WHEEL +5 -0
- vcsgraph-0.3.3.dist-info/licenses/COPYING.txt +339 -0
- vcsgraph-0.3.3.dist-info/top_level.txt +1 -0
vcsgraph/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (C) 2005-2010 Canonical Ltd
|
|
2
|
+
# Copyright (C) 2018-2025 Breezy Developers
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
|
|
18
|
+
"""Graph algorithms for version control systems.
|
|
19
|
+
|
|
20
|
+
This package contains graph-related algorithms that are useful for
|
|
21
|
+
version control systems, including:
|
|
22
|
+
|
|
23
|
+
- Topological sorting (tsort)
|
|
24
|
+
- Graph operations (graph)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BaseVersionedFile",
|
|
29
|
+
"DictParentsProvider",
|
|
30
|
+
"FrozenHeadsCache",
|
|
31
|
+
"Graph",
|
|
32
|
+
"KnownGraph",
|
|
33
|
+
"MultiMemoryVersionedFile",
|
|
34
|
+
"MultiVersionedFile",
|
|
35
|
+
"invert_parent_map",
|
|
36
|
+
"topo_sort",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = (0, 3, 3)
|
|
40
|
+
|
|
41
|
+
# Re-export commonly used functions and classes
|
|
42
|
+
from .graph import (
|
|
43
|
+
DictParentsProvider,
|
|
44
|
+
FrozenHeadsCache,
|
|
45
|
+
Graph,
|
|
46
|
+
invert_parent_map,
|
|
47
|
+
)
|
|
48
|
+
from .known_graph import KnownGraph
|
|
49
|
+
from .tsort import topo_sort
|
|
Binary file
|
vcsgraph/errors.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Copyright (C) 2005-2010 Canonical Ltd
|
|
2
|
+
#
|
|
3
|
+
# This program is free software; you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
16
|
+
|
|
17
|
+
"""Exceptions for vcsgraph operations."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Error(Exception):
|
|
21
|
+
"""Base class for vcsgraph exceptions."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UnsupportedOperation(Error):
|
|
27
|
+
"""Requested operation is not supported."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GhostRevisionsHaveNoRevno(Error):
|
|
33
|
+
"""When searching for revnos, we encounter a ghost."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, revision_id, ghost_revision_id):
|
|
36
|
+
"""Initialize GhostRevisionsHaveNoRevno exception.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
revision_id: The revision ID being searched for.
|
|
40
|
+
ghost_revision_id: The ghost revision ID encountered.
|
|
41
|
+
"""
|
|
42
|
+
self.revision_id = revision_id
|
|
43
|
+
self.ghost_revision_id = ghost_revision_id
|
|
44
|
+
super().__init__(
|
|
45
|
+
f"Ghost revision {ghost_revision_id!r} has no revno, "
|
|
46
|
+
f"cannot determine revno for {revision_id!r}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other):
|
|
50
|
+
"""Check equality with another GhostRevisionsHaveNoRevno instance.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
other: Object to compare with.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if both instances have the same revision_id and ghost_revision_id.
|
|
57
|
+
"""
|
|
58
|
+
if not isinstance(other, GhostRevisionsHaveNoRevno):
|
|
59
|
+
return False
|
|
60
|
+
return (
|
|
61
|
+
self.revision_id == other.revision_id
|
|
62
|
+
and self.ghost_revision_id == other.ghost_revision_id
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class InvalidRevisionId(Error):
|
|
67
|
+
"""Invalid revision ID."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, revision_id, client):
|
|
70
|
+
"""Initialize InvalidRevisionId exception.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
revision_id: The invalid revision ID.
|
|
74
|
+
client: The client that encountered the invalid revision.
|
|
75
|
+
"""
|
|
76
|
+
self.revision_id = revision_id
|
|
77
|
+
self.client = client
|
|
78
|
+
super().__init__(f"Invalid revision ID {revision_id!r}")
|
|
79
|
+
|
|
80
|
+
def __eq__(self, other):
|
|
81
|
+
"""Check equality with another InvalidRevisionId instance.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
other: Object to compare with.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if both instances have the same revision_id and client.
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(other, InvalidRevisionId):
|
|
90
|
+
return False
|
|
91
|
+
return self.revision_id == other.revision_id and self.client == other.client
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NoCommonAncestor(Error):
|
|
95
|
+
"""No common ancestor found between revisions."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, revision_a, revision_b):
|
|
98
|
+
"""Initialize NoCommonAncestor exception.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
revision_a: First revision ID.
|
|
102
|
+
revision_b: Second revision ID.
|
|
103
|
+
"""
|
|
104
|
+
self.revision_a = revision_a
|
|
105
|
+
self.revision_b = revision_b
|
|
106
|
+
super().__init__(
|
|
107
|
+
f"No common ancestor found between {revision_a!r} and {revision_b!r}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def __eq__(self, other):
|
|
111
|
+
"""Check equality with another NoCommonAncestor instance.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
other: Object to compare with.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True if both instances have the same revision_a and revision_b.
|
|
118
|
+
"""
|
|
119
|
+
if not isinstance(other, NoCommonAncestor):
|
|
120
|
+
return False
|
|
121
|
+
return (
|
|
122
|
+
self.revision_a == other.revision_a and self.revision_b == other.revision_b
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class RevisionNotPresent(Error):
|
|
127
|
+
"""Revision not present in the graph."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, revision_id, graph):
|
|
130
|
+
"""Initialize RevisionNotPresent exception.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
revision_id: The revision ID not present in the graph.
|
|
134
|
+
graph: The graph object where the revision was not found.
|
|
135
|
+
"""
|
|
136
|
+
self.revision_id = revision_id
|
|
137
|
+
self.graph = graph
|
|
138
|
+
super().__init__(f"Revision {revision_id!r} not present in graph")
|
|
139
|
+
|
|
140
|
+
def __eq__(self, other):
|
|
141
|
+
"""Check equality with another RevisionNotPresent instance.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
other: Object to compare with.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if both instances have the same revision_id and graph.
|
|
148
|
+
"""
|
|
149
|
+
if not isinstance(other, RevisionNotPresent):
|
|
150
|
+
return False
|
|
151
|
+
return self.revision_id == other.revision_id and self.graph == other.graph
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class GraphCycleError(Error):
|
|
155
|
+
"""Cycle detected in graph.
|
|
156
|
+
|
|
157
|
+
Raised when a cycle is detected in a directed graph that should be acyclic.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, graph):
|
|
161
|
+
"""Initialize with the graph containing the cycle.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
graph: The graph object that contains a cycle.
|
|
165
|
+
"""
|
|
166
|
+
self.graph = graph
|
|
167
|
+
super().__init__(f"Cycle in graph {graph!r}")
|
|
168
|
+
|
|
169
|
+
def __eq__(self, other):
|
|
170
|
+
"""Check equality with another GraphCycleError instance.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
other: Object to compare with.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if both instances have the same graph.
|
|
177
|
+
"""
|
|
178
|
+
if not isinstance(other, GraphCycleError):
|
|
179
|
+
return False
|
|
180
|
+
return self.graph == other.graph
|
vcsgraph/graph.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Copyright (C) 2007-2011 Canonical Ltd
|
|
2
|
+
#
|
|
3
|
+
# This program is free software; you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
16
|
+
|
|
17
|
+
"""Graph algorithms for version control systems."""
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"CachingParentsProvider",
|
|
21
|
+
"CallableToParentsProviderAdapter",
|
|
22
|
+
"DictParentsProvider",
|
|
23
|
+
"FrozenHeadsCache",
|
|
24
|
+
"GraphThunkIdsToKeys",
|
|
25
|
+
"HeadsCache",
|
|
26
|
+
"StackedParentsProvider",
|
|
27
|
+
"collapse_linear_regions",
|
|
28
|
+
"invert_parent_map",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
from ._graph_rs import (
|
|
32
|
+
CachingParentsProvider,
|
|
33
|
+
CallableToParentsProviderAdapter,
|
|
34
|
+
DictParentsProvider,
|
|
35
|
+
FrozenHeadsCache,
|
|
36
|
+
GraphThunkIdsToKeys,
|
|
37
|
+
HeadsCache,
|
|
38
|
+
StackedParentsProvider,
|
|
39
|
+
_RustGraph,
|
|
40
|
+
collapse_linear_regions,
|
|
41
|
+
invert_parent_map,
|
|
42
|
+
)
|
|
43
|
+
from ._graph_rs import (
|
|
44
|
+
_BreadthFirstSearcher as _RustBreadthFirstSearcher,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# NULL_REVISION constant
|
|
48
|
+
NULL_REVISION = b"null:"
|
|
49
|
+
|
|
50
|
+
# DIAGRAM of terminology
|
|
51
|
+
# A
|
|
52
|
+
# /\
|
|
53
|
+
# B C
|
|
54
|
+
# | |\
|
|
55
|
+
# D E F
|
|
56
|
+
# |\/| |
|
|
57
|
+
# |/\|/
|
|
58
|
+
# G H
|
|
59
|
+
#
|
|
60
|
+
# In this diagram, relative to G and H:
|
|
61
|
+
# A, B, C, D, E are common ancestors.
|
|
62
|
+
# C, D and E are border ancestors, because each has a non-common descendant.
|
|
63
|
+
# D and E are least common ancestors because none of their descendants are
|
|
64
|
+
# common ancestors.
|
|
65
|
+
# C is not a least common ancestor because its descendant, E, is a common
|
|
66
|
+
# ancestor.
|
|
67
|
+
#
|
|
68
|
+
# The find_unique_lca algorithm will pick A in two steps:
|
|
69
|
+
# 1. find_lca('G', 'H') => ['D', 'E']
|
|
70
|
+
# 2. Since len(['D', 'E']) > 1, find_lca('D', 'E') => ['A']
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Graph:
|
|
74
|
+
"""Provide incremental access to revision graphs.
|
|
75
|
+
|
|
76
|
+
This is the generic implementation; it is intended to be subclassed to
|
|
77
|
+
specialize it for other repository types.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, parents_provider):
|
|
81
|
+
"""Construct a Graph that uses several graphs as its input.
|
|
82
|
+
|
|
83
|
+
This should not normally be invoked directly, because there may be
|
|
84
|
+
specialized implementations for particular repository types. See
|
|
85
|
+
Repository.get_graph().
|
|
86
|
+
|
|
87
|
+
:param parents_provider: An object providing a get_parent_map call
|
|
88
|
+
conforming to the behavior of
|
|
89
|
+
StackedParentsProvider.get_parent_map.
|
|
90
|
+
"""
|
|
91
|
+
if getattr(parents_provider, "get_parents", None) is not None:
|
|
92
|
+
self.get_parents = parents_provider.get_parents
|
|
93
|
+
if getattr(parents_provider, "get_parent_map", None) is not None:
|
|
94
|
+
self.get_parent_map = parents_provider.get_parent_map
|
|
95
|
+
self._parents_provider = parents_provider
|
|
96
|
+
# Rust-backed helper for methods that have been ported. Uses the same
|
|
97
|
+
# parents provider; the Rust side calls back into Python via a GIL
|
|
98
|
+
# adapter when it needs a parent lookup.
|
|
99
|
+
self._rs = _RustGraph(parents_provider)
|
|
100
|
+
|
|
101
|
+
def __repr__(self):
|
|
102
|
+
return f"Graph({self._parents_provider!r})"
|
|
103
|
+
|
|
104
|
+
def find_lca(self, *revisions):
|
|
105
|
+
"""Determine the lowest common ancestors of the provided revisions.
|
|
106
|
+
|
|
107
|
+
A lowest common ancestor is a common ancestor none of whose
|
|
108
|
+
descendants are common ancestors. In graphs, unlike trees, there may
|
|
109
|
+
be multiple lowest common ancestors.
|
|
110
|
+
|
|
111
|
+
This algorithm has two phases. Phase 1 identifies border ancestors,
|
|
112
|
+
and phase 2 filters border ancestors to determine lowest common
|
|
113
|
+
ancestors.
|
|
114
|
+
|
|
115
|
+
In phase 1, border ancestors are identified, using a breadth-first
|
|
116
|
+
search starting at the bottom of the graph. Searches are stopped
|
|
117
|
+
whenever a node or one of its descendants is determined to be common
|
|
118
|
+
|
|
119
|
+
In phase 2, the border ancestors are filtered to find the least
|
|
120
|
+
common ancestors. This is done by searching the ancestries of each
|
|
121
|
+
border ancestor.
|
|
122
|
+
|
|
123
|
+
Phase 2 is perfomed on the principle that a border ancestor that is
|
|
124
|
+
not an ancestor of any other border ancestor is a least common
|
|
125
|
+
ancestor.
|
|
126
|
+
|
|
127
|
+
Searches are stopped when they find a node that is determined to be a
|
|
128
|
+
common ancestor of all border ancestors, because this shows that it
|
|
129
|
+
cannot be a descendant of any border ancestor.
|
|
130
|
+
|
|
131
|
+
The scaling of this operation should be proportional to:
|
|
132
|
+
|
|
133
|
+
1. The number of uncommon ancestors
|
|
134
|
+
2. The number of border ancestors
|
|
135
|
+
3. The length of the shortest path between a border ancestor and an
|
|
136
|
+
ancestor of all border ancestors.
|
|
137
|
+
"""
|
|
138
|
+
return self._rs.find_lca(revisions)
|
|
139
|
+
|
|
140
|
+
def find_difference(self, left_revision, right_revision):
|
|
141
|
+
"""Determine the graph difference between two revisions."""
|
|
142
|
+
return self._rs.find_difference(left_revision, right_revision)
|
|
143
|
+
|
|
144
|
+
def find_descendants(self, old_key, new_key):
|
|
145
|
+
"""Find descendants of old_key that are ancestors of new_key."""
|
|
146
|
+
return self._rs.find_descendants(old_key, new_key)
|
|
147
|
+
|
|
148
|
+
def _find_descendant_ancestors(self, old_key, new_key):
|
|
149
|
+
"""Find ancestors of new_key that may be descendants of old_key."""
|
|
150
|
+
return self._rs._find_descendant_ancestors(old_key, new_key)
|
|
151
|
+
|
|
152
|
+
def _remove_simple_descendants(self, revisions, parent_map):
|
|
153
|
+
"""Remove revisions which are children of other ones in the set.
|
|
154
|
+
|
|
155
|
+
This doesn't do any graph searching, it just checks the immediate
|
|
156
|
+
parent_map to find if there are any children which can be removed.
|
|
157
|
+
|
|
158
|
+
:param revisions: A set of revision_ids
|
|
159
|
+
:return: A set of revision_ids with the children removed
|
|
160
|
+
"""
|
|
161
|
+
return self._rs._remove_simple_descendants(revisions, parent_map)
|
|
162
|
+
|
|
163
|
+
def get_child_map(self, keys):
|
|
164
|
+
"""Get a mapping from parents to children of the specified keys.
|
|
165
|
+
|
|
166
|
+
This is simply the inversion of get_parent_map. Only supplied keys
|
|
167
|
+
will be discovered as children.
|
|
168
|
+
:return: a dict of key:child_list for keys.
|
|
169
|
+
"""
|
|
170
|
+
parent_map = self._parents_provider.get_parent_map(keys)
|
|
171
|
+
parent_child = {}
|
|
172
|
+
for child, parents in sorted(parent_map.items()):
|
|
173
|
+
for parent in parents:
|
|
174
|
+
parent_child.setdefault(parent, []).append(child)
|
|
175
|
+
return parent_child
|
|
176
|
+
|
|
177
|
+
def find_distance_to_null(self, target_revision_id, known_revision_ids):
|
|
178
|
+
"""Find the left-hand distance to the NULL_REVISION.
|
|
179
|
+
|
|
180
|
+
(This can also be considered the revno of a branch at
|
|
181
|
+
target_revision_id.)
|
|
182
|
+
|
|
183
|
+
:param target_revision_id: A revision_id which we would like to know
|
|
184
|
+
the revno for.
|
|
185
|
+
:param known_revision_ids: [(revision_id, revno)] A list of known
|
|
186
|
+
revno, revision_id tuples. We'll use this to seed the search.
|
|
187
|
+
"""
|
|
188
|
+
return self._rs.find_distance_to_null(target_revision_id, known_revision_ids)
|
|
189
|
+
|
|
190
|
+
def find_lefthand_distances(self, keys):
|
|
191
|
+
"""Find the distance to null for all the keys in keys.
|
|
192
|
+
|
|
193
|
+
:param keys: keys to lookup.
|
|
194
|
+
:return: A dict key->distance for all of keys.
|
|
195
|
+
"""
|
|
196
|
+
return self._rs.find_lefthand_distances(keys)
|
|
197
|
+
|
|
198
|
+
def find_unique_ancestors(self, unique_revision, common_revisions):
|
|
199
|
+
"""Find the unique ancestors for a revision versus others.
|
|
200
|
+
|
|
201
|
+
This returns the ancestry of unique_revision, excluding all revisions
|
|
202
|
+
in the ancestry of common_revisions. If unique_revision is in the
|
|
203
|
+
ancestry, then the empty set will be returned.
|
|
204
|
+
|
|
205
|
+
:param unique_revision: The revision_id whose ancestry we are
|
|
206
|
+
interested in.
|
|
207
|
+
(XXX: Would this API be better if we allowed multiple revisions on
|
|
208
|
+
to be searched here?)
|
|
209
|
+
:param common_revisions: Revision_ids of ancestries to exclude.
|
|
210
|
+
:return: A set of revisions in the ancestry of unique_revision
|
|
211
|
+
"""
|
|
212
|
+
return self._rs.find_unique_ancestors(unique_revision, common_revisions)
|
|
213
|
+
|
|
214
|
+
def get_parent_map(self, revisions): # type: ignore
|
|
215
|
+
"""Get a map of key:parent_list for revisions.
|
|
216
|
+
|
|
217
|
+
This implementation delegates to get_parents, for old parent_providers
|
|
218
|
+
that do not supply get_parent_map.
|
|
219
|
+
"""
|
|
220
|
+
result = {}
|
|
221
|
+
for rev, parents in self.get_parents(revisions):
|
|
222
|
+
if parents is not None:
|
|
223
|
+
result[rev] = parents
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
def _make_breadth_first_searcher(self, revisions):
|
|
227
|
+
return _RustBreadthFirstSearcher(revisions, self)
|
|
228
|
+
|
|
229
|
+
def heads(self, keys):
|
|
230
|
+
"""Return the heads from amongst keys.
|
|
231
|
+
|
|
232
|
+
This is done by searching the ancestries of each key. Any key that is
|
|
233
|
+
reachable from another key is not returned; all the others are.
|
|
234
|
+
|
|
235
|
+
This operation scales with the relative depth between any two keys. If
|
|
236
|
+
any two keys are completely disconnected all ancestry of both sides
|
|
237
|
+
will be retrieved.
|
|
238
|
+
|
|
239
|
+
:param keys: An iterable of keys.
|
|
240
|
+
:return: A set of the heads. Note that as a set there is no ordering
|
|
241
|
+
information. Callers will need to filter their input to create
|
|
242
|
+
order if they need it.
|
|
243
|
+
"""
|
|
244
|
+
return self._rs.heads(keys)
|
|
245
|
+
|
|
246
|
+
def find_merge_order(self, tip_revision_id, lca_revision_ids):
|
|
247
|
+
"""Find the order that each revision was merged into tip.
|
|
248
|
+
|
|
249
|
+
This basically just walks backwards with a stack, and walks left-first
|
|
250
|
+
until it finds a node to stop.
|
|
251
|
+
"""
|
|
252
|
+
return self._rs.find_merge_order(tip_revision_id, lca_revision_ids)
|
|
253
|
+
|
|
254
|
+
def find_lefthand_merger(self, merged_key, tip_key):
|
|
255
|
+
"""Find the first lefthand ancestor of tip_key that merged merged_key.
|
|
256
|
+
|
|
257
|
+
We do this by first finding the descendants of merged_key, then
|
|
258
|
+
walking through the lefthand ancestry of tip_key until we find a key
|
|
259
|
+
that doesn't descend from merged_key. Its child is the key that
|
|
260
|
+
merged merged_key.
|
|
261
|
+
|
|
262
|
+
:return: The first lefthand ancestor of tip_key to merge merged_key.
|
|
263
|
+
merged_key if it is a lefthand ancestor of tip_key.
|
|
264
|
+
None if no ancestor of tip_key merged merged_key.
|
|
265
|
+
"""
|
|
266
|
+
return self._rs.find_lefthand_merger(merged_key, tip_key)
|
|
267
|
+
|
|
268
|
+
def find_unique_lca(self, left_revision, right_revision, count_steps=False):
|
|
269
|
+
"""Find a unique LCA.
|
|
270
|
+
|
|
271
|
+
Find lowest common ancestors. If there is no unique common
|
|
272
|
+
ancestor, find the lowest common ancestors of those ancestors.
|
|
273
|
+
|
|
274
|
+
Iteration stops when a unique lowest common ancestor is found.
|
|
275
|
+
The graph origin is necessarily a unique lowest common ancestor.
|
|
276
|
+
|
|
277
|
+
Note that None is not an acceptable substitute for NULL_REVISION.
|
|
278
|
+
in the input for this method.
|
|
279
|
+
|
|
280
|
+
:param count_steps: If True, the return value will be a tuple of
|
|
281
|
+
(unique_lca, steps) where steps is the number of times that
|
|
282
|
+
find_lca was run. If False, only unique_lca is returned.
|
|
283
|
+
"""
|
|
284
|
+
return self._rs.find_unique_lca(left_revision, right_revision, count_steps)
|
|
285
|
+
|
|
286
|
+
def iter_ancestry(self, revision_ids):
|
|
287
|
+
"""Iterate the ancestry of this revision.
|
|
288
|
+
|
|
289
|
+
:param revision_ids: Nodes to start the search
|
|
290
|
+
:return: Yield tuples mapping a revision_id to its parents for the
|
|
291
|
+
ancestry of revision_id.
|
|
292
|
+
Ghosts will be returned with None as their parents, and nodes
|
|
293
|
+
with no parents will have NULL_REVISION as their only parent. (As
|
|
294
|
+
defined by get_parent_map.)
|
|
295
|
+
There will also be a node for (NULL_REVISION, ())
|
|
296
|
+
"""
|
|
297
|
+
yield from self._rs.iter_ancestry(revision_ids)
|
|
298
|
+
|
|
299
|
+
def iter_lefthand_ancestry(self, start_key, stop_keys=None):
|
|
300
|
+
"""Iterate the lefthand ancestry of start_key.
|
|
301
|
+
|
|
302
|
+
Yields revisions in lefthand order. Callers may break early; the
|
|
303
|
+
walk is lazy and does not query further than the last yielded key.
|
|
304
|
+
Raises RevisionNotPresent if the walk reaches a key that is not in
|
|
305
|
+
the parents provider.
|
|
306
|
+
"""
|
|
307
|
+
return self._rs.iter_lefthand_ancestry(start_key, stop_keys)
|
|
308
|
+
|
|
309
|
+
def iter_topo_order(self, revisions):
|
|
310
|
+
"""Iterate through the input revisions in topological order.
|
|
311
|
+
|
|
312
|
+
This sorting only ensures that parents come before their children.
|
|
313
|
+
An ancestor may sort after a descendant if the relationship is not
|
|
314
|
+
visible in the supplied list of revisions.
|
|
315
|
+
"""
|
|
316
|
+
return iter(self._rs.iter_topo_order(revisions))
|
|
317
|
+
|
|
318
|
+
def is_ancestor(self, candidate_ancestor, candidate_descendant):
|
|
319
|
+
"""Determine whether a revision is an ancestor of another.
|
|
320
|
+
|
|
321
|
+
We answer this using heads() as heads() has the logic to perform the
|
|
322
|
+
smallest number of parent lookups to determine the ancestral
|
|
323
|
+
relationship between N revisions.
|
|
324
|
+
"""
|
|
325
|
+
return self._rs.is_ancestor(candidate_ancestor, candidate_descendant)
|
|
326
|
+
|
|
327
|
+
def is_between(self, revid, lower_bound_revid, upper_bound_revid):
|
|
328
|
+
"""Determine whether a revision is between two others.
|
|
329
|
+
|
|
330
|
+
returns true if and only if:
|
|
331
|
+
lower_bound_revid <= revid <= upper_bound_revid
|
|
332
|
+
"""
|
|
333
|
+
return self._rs.is_between(revid, lower_bound_revid, upper_bound_revid)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
_counters = [0, 0, 0, 0, 0, 0, 0]
|
|
337
|
+
|
|
338
|
+
# Import KnownGraph to make it available through this module for compatibility
|
vcsgraph/known_graph.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright (C) 2009, 2010 Canonical Ltd
|
|
2
|
+
#
|
|
3
|
+
# This program is free software; you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
16
|
+
|
|
17
|
+
"""KnownGraph: graph algorithms for the case where the full ancestry is known.
|
|
18
|
+
|
|
19
|
+
The implementation is provided by the Rust extension module ``_graph_rs``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from ._graph_rs import KnownGraph, _KnownGraphNode, _MergeSortNode
|
|
23
|
+
|
|
24
|
+
__all__ = ["KnownGraph", "_KnownGraphNode", "_MergeSortNode"]
|
vcsgraph/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright (C) 2025 Breezy Developers
|
|
2
|
+
#
|
|
3
|
+
# This program is free software; you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation; either version 2 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import tempfile
|
|
20
|
+
import unittest
|
|
21
|
+
from unittest import TestCase
|
|
22
|
+
|
|
23
|
+
__all__ = ["TestCaseInTempDir"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestCaseInTempDir(TestCase):
|
|
27
|
+
"""Minimal TestCase that runs in a temporary directory.
|
|
28
|
+
|
|
29
|
+
Only implements the functionality actually needed by vcsgraph tests.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def setUp(self):
|
|
33
|
+
super().setUp()
|
|
34
|
+
self.test_dir = tempfile.mkdtemp(prefix="vcsgraph_test_")
|
|
35
|
+
self.original_dir = os.getcwd()
|
|
36
|
+
os.chdir(self.test_dir)
|
|
37
|
+
|
|
38
|
+
def tearDown(self):
|
|
39
|
+
os.chdir(self.original_dir)
|
|
40
|
+
shutil.rmtree(self.test_dir)
|
|
41
|
+
super().tearDown()
|
|
42
|
+
|
|
43
|
+
def assertPathExists(self, path):
|
|
44
|
+
"""Fail unless path exists."""
|
|
45
|
+
self.assertTrue(os.path.lexists(path), f"{path} does not exist")
|
|
46
|
+
|
|
47
|
+
def assertPathDoesNotExist(self, path):
|
|
48
|
+
"""Fail if path exists."""
|
|
49
|
+
self.assertFalse(os.path.lexists(path), f"{path} exists")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_suite() -> unittest.TestSuite:
|
|
53
|
+
names = [
|
|
54
|
+
"known_graph",
|
|
55
|
+
"graph",
|
|
56
|
+
"tsort",
|
|
57
|
+
]
|
|
58
|
+
module_names = ["vcsgraph.tests.test_" + name for name in names]
|
|
59
|
+
loader = unittest.TestLoader()
|
|
60
|
+
return loader.loadTestsFromNames(module_names)
|