ReGraph implements a framework for the version control (VC) of graph transformations
The data structure VersionedGraph
allows to store the history of transformations of a graph object and perform the following VC operations:
In [1]:
from regraph import NXGraph
from regraph.audit import VersionedGraph
from regraph.rules import Rule
from regraph import print_graph, plot_rule, plot_graph
Create a graph and pass it to the VersionedGraph
wrapper that will take care of the version control.
In [2]:
graph_obj = NXGraph()
g = VersionedGraph(graph_obj)
Now let's create a rule that adds to the graph two nodes connected with an edge and apply it. If we want the changes to be commited to the version control we rewrite through the rewrite
method of a VersioneGraph
object.
In [3]:
rule = Rule.from_transform(NXGraph())
rule.inject_add_node("a")
rule.inject_add_node("b")
rule.inject_add_edge("a", "b")
rhs_instance, _ = g.rewrite(rule, {}, message="Add a -> b")
plot_graph(g.graph)
Out[3]:
We create a new branch called "branch"
In [4]:
branch_commit = g.branch("branch")
In [5]:
print("Branches: ", g.branches())
print("Current branch '{}'".format(g.current_branch()))
Apply a rule that clones the node 'b' to the current vesion of the graph (branch 'branch')
In [6]:
pattern = NXGraph()
pattern.add_node("b")
rule = Rule.from_transform(pattern)
rule.inject_clone_node("b")
plot_rule(rule)
rhs_instance, commit_id = g.rewrite(rule, {"b": rhs_instance["b"]}, message="Clone b")
plot_graph(g.graph)
Out[6]:
The rewrite
method of VersionedGraph
returns the RHS instance of the applied and the id of the newly created commit corresponding to this rewrite.
In [7]:
print("RHS instance", rhs_instance)
print("Commit ID: ", commit_id)
Switch back to the 'master' branch
In [8]:
g.switch_branch("master")
print(g.current_branch())
Apply a rule that adds a loop form 'a' to itself, a new node 'c' and connects it with 'a'
In [9]:
pattern = NXGraph()
pattern.add_node("a")
rule = Rule.from_transform(pattern)
rule.inject_add_node("c")
rule.inject_add_edge("c", "a")
rule.inject_add_edge("a", "a")
rhs_instance, _ = g.rewrite(rule, {"a": "a"}, message="Add c and c->a")
plot_graph(g.graph)
Out[9]:
Create a new branch 'dev'
In [10]:
g.branch("dev")
Out[10]:
In this branch remove an edge from 'c' to 'a' and merge two nodes together
In [11]:
pattern = NXGraph()
pattern.add_node("c")
pattern.add_node("a")
pattern.add_edge("c", "a")
rule = Rule.from_transform(pattern)
rule.inject_remove_edge("c", "a")
rule.inject_merge_nodes(["c", "a"])
plot_rule(rule)
g.rewrite(rule, {"a": rhs_instance["a"], "c": rhs_instance["c"]}, message="Merge c and a")
plot_graph(g.graph)
Out[11]:
Switch back to the 'master' branch.
In [12]:
g.switch_branch("master")
Apply a rule that clones a node 'a'
In [13]:
pattern = NXGraph()
pattern.add_node("a")
rule = Rule.from_transform(pattern)
_, rhs_clone = rule.inject_clone_node("a")
rhs_instance, rollback_commit = g.rewrite(rule, {"a": rhs_instance["a"]}, message="Clone a")
plot_graph(g.graph)
Out[13]:
Create a new branch 'test'
In [14]:
g.branch("test")
Out[14]:
In this branch apply the rule that adds a new node 'd' and connects it with an edge to one of the cloned 'a' nodes
In [15]:
pattern = NXGraph()
pattern.add_node("a")
rule = Rule.from_transform(pattern)
rule.inject_add_node("d")
rule.inject_add_edge("a", "d")
g.rewrite(rule, {"a": rhs_instance[rhs_clone]}, message="Add d -> clone of a")
plot_graph(g.graph)
Out[15]:
Switch back to 'master'
In [16]:
g.switch_branch("master")
Remove a node 'a'
In [17]:
pattern = NXGraph()
pattern.add_node("a")
rule = Rule.from_transform(pattern)
rule.inject_remove_node("a")
rhs_instance, _ = g.rewrite(rule, {"a": rhs_instance["a"]}, message="Remove a")
plot_graph(g.graph)
Out[17]:
Merge the branch 'dev' into 'master'
In [18]:
g.merge_with("dev")
Out[18]:
In [19]:
plot_graph(g.graph)
Out[19]:
Merge 'test' into 'master'
In [20]:
g.merge_with("test")
Out[20]:
In [21]:
plot_graph(g.graph)
Out[21]:
We can inspect the version control object in more details and look at its attribute _revision_graph
, whose nodes represent the commits and whose edges represent graph deltas between different commits (basically, rewriting rules that constitute commits). Here we can see that on the nodes of the revision graph are stored branch names to which commits belong and user specified commit messages.
In [22]:
for n, attrs in g._revision_graph.nodes(data=True):
print("Node ID: ", n)
print("Attributes: ")
print("\t", attrs)
In [23]:
# Pretty-print the history
g.print_history()
Now we can rollback to some previous commit (commit where we first cloned the node 'a')
In [24]:
g.rollback(rollback_commit)
In [25]:
print("Branches: ", g.branches())
print("Current branch '{}'".format(g.current_branch()))
print("Updated revision graph:")
g.print_history()
print("Current graph object")
plot_graph(g.graph)
print_graph(g.graph)
In [26]:
g.switch_branch("branch")
In [27]:
g.rollback(branch_commit)
In [28]:
g.print_history()
In [29]:
print(g._heads)
plot_graph(g.graph)
Out[29]:
In [30]:
g.switch_branch("master")
In [31]:
plot_graph(g.graph)
Out[31]:
In [32]:
g.merge_with("branch")
Out[32]:
In [33]:
plot_graph(g.graph)
Out[33]: