summaryrefslogtreecommitdiff
path: root/tools/dev/mergegraph/mergegraph.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/dev/mergegraph/mergegraph.py')
-rw-r--r--tools/dev/mergegraph/mergegraph.py313
1 files changed, 313 insertions, 0 deletions
diff --git a/tools/dev/mergegraph/mergegraph.py b/tools/dev/mergegraph/mergegraph.py
new file mode 100644
index 0000000..834b164
--- /dev/null
+++ b/tools/dev/mergegraph/mergegraph.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python
+
+# ====================================================================
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ====================================================================
+
+# Config file format:
+example = """
+ [graph]
+ filename = merge-sync-1.png
+ title = Sync Merge: CC vs SVN
+ # Branches: (branch name, branched from node, first rev, last rev).
+ branches = [
+ ('A', 'O0', 1, 4),
+ ('O', None, 0, 0),
+ ('B', 'O0', 1, 5)
+ ]
+ # Changes: nodes in which a change was committed; merge targets need not
+ # be listed here.
+ changes = [
+ 'A1', 'A2', 'A3', 'A4',
+ 'B1', 'B2', 'B3', 'B4', 'B5'
+ ]
+ # Merges: (base node, source-right node, target node, label).
+ # Base is also known as source-left.
+ merges = [
+ ('O0', 'A:1', 'B3', 'sync'),
+ ('A2', 'A:3', 'B5', 'sync'),
+ ]
+ # Annotations for nodes: (node, annotation text).
+ annotations = [
+ ('A2', 'cc:YCA')
+ ]
+"""
+
+# Notes about different kinds of merge.
+#
+# A basic 3-way merge is ...
+#
+# The ClearCase style of merge is a 3-way merge.
+#
+# The Subversion style of merge (that is, one phase of a Subversion merge)
+# is a three-way merge with its base (typically the YCA) on the source branch.
+
+
+import sys
+import pydot
+from pydot import Node, Edge
+
+
+def mergeinfo_to_node_list(mi):
+ """Convert a mergeinfo string such as '/foo:1,3-5*' into a list of
+ node names such as ['foo1', 'foo3', 'foo4', 'foo5'].
+ """
+ ### Doesn't yet strip the leading slash.
+ l = []
+ if mi:
+ for mi_str in mi.split(' '):
+ path, ranges = mi_str.split(':')
+ for r in ranges.split(','):
+ if r.endswith('*'):
+ # TODO: store & use this 'non-inheritable' flag
+ # Remove the flag
+ r = r[:-1]
+ rlist = r.split('-')
+ r1 = int(rlist[0])
+ if len(rlist) == 2:
+ r2 = int(rlist[1])
+ else:
+ r2 = r1
+ for rev in range(r1, r2 + 1):
+ l.append(path + str(rev))
+ return l
+
+
+class MergeGraph(pydot.Graph):
+ """Base class, not intended for direct use. Use MergeDot for the main
+ graph and MergeSubgraph for a subgraph.
+ """
+
+ def mk_origin_node(graph, name, label):
+ """Add a node to the graph"""
+ graph.add_node(Node(name, label=label, shape='plaintext'))
+
+ def mk_invis_node(graph, name):
+ """Add a node to the graph"""
+ graph.add_node(Node(name, style='invis'))
+
+ def mk_node(graph, name, label=None):
+ """Add a node to the graph, if not already present"""
+ if not graph.get_node(name):
+ if not label:
+ label = name
+ if name in graph.changes:
+ graph.add_node(Node(name, label=label))
+ else:
+ graph.add_node(Node(name, color='grey', label=''))
+
+ def mk_merge_target(graph, target_node, important):
+ """Add a merge target node to the graph."""
+ if important:
+ color = 'red'
+ else:
+ color = 'black'
+ graph.add_node(Node(target_node, color=color, fontcolor=color, style='bold'))
+
+ def mk_edge(graph, name1, name2, **attrs):
+ """Add an ordinary edge to the graph"""
+ graph.add_edge(Edge(name1, name2, dir='none', style='dotted', color='grey', **attrs))
+
+ def mk_br_edge(graph, name1, name2):
+ """Add a branch-creation edge to the graph"""
+ # Constraint=false to avoid the Y-shape skewing the nice parallel branch lines
+ graph.mk_edge(name1, name2, constraint='false')
+
+ def mk_merge_edge(graph, src_node, tgt_node, kind, label, important):
+ """Add a merge edge to the graph"""
+ if important:
+ color = 'red'
+ else:
+ color = 'grey'
+ e = Edge(src_node, tgt_node, constraint='false',
+ label='"' + label + '"',
+ color=color, fontcolor=color,
+ style='bold')
+ if kind.startswith('cherry'):
+ e.set_style('dashed')
+ graph.add_edge(e)
+
+ def mk_mergeinfo_edge(graph, base_node, src_node, important):
+ """"""
+ if important:
+ color = 'red'
+ else:
+ color = 'grey'
+ graph.add_edge(Edge(base_node, src_node,
+ dir='both', arrowtail='odot', arrowhead='tee',
+ color=color, constraint='false'))
+
+ def mk_invis_edge(graph, name1, name2):
+ """Add an invisible edge to the graph"""
+ graph.add_edge(Edge(name1, name2, style='invis'))
+
+ def add_merge(graph, merge, important):
+ """Add a merge"""
+ base_node, src_node, tgt_node, kind = merge
+
+ if base_node and src_node: # and not kind.startwith('cherry'):
+ graph.mk_mergeinfo_edge(base_node, src_node, important)
+
+ # Merge target node
+ graph.mk_merge_target(tgt_node, important)
+
+ # Merge edge
+ graph.mk_merge_edge(src_node, tgt_node, kind, kind, important)
+
+ def add_annotation(graph, node, label, color='lightblue'):
+ """Add a graph node that serves as an annotation to a normal node.
+ More than one annotation can be added to the same normal node.
+ """
+ subg_name = node + '_annotations'
+
+ def get_subgraph(graph, name):
+ """Equivalent to pydot.Graph.get_subgraph() when there is no more than
+ one subgraph of the given name, but working aroung a bug in
+ pydot.Graph.get_subgraph().
+ """
+ for subg in graph.get_subgraph_list():
+ if subg.get_name() == name:
+ return subg
+ return None
+
+ g = get_subgraph(graph, subg_name)
+ if not g:
+ g = pydot.Subgraph(subg_name, rank='same')
+ graph.add_subgraph(g)
+
+ ann_node = node + '_'
+ while g.get_node(ann_node):
+ ann_node = ann_node + '_'
+ g.add_node(Node(ann_node, shape='box', style='filled', color=color,
+ label='"' + label + '"'))
+ g.add_edge(Edge(ann_node, node, style='solid', color=color,
+ dir='none', constraint='false'))
+
+class MergeSubgraph(MergeGraph, pydot.Subgraph):
+ """"""
+ def __init__(graph, **attrs):
+ """"""
+ MergeGraph.__init__(graph)
+ pydot.Subgraph.__init__(graph, **attrs)
+
+class MergeDot(MergeGraph, pydot.Dot):
+ """
+ # TODO: In the 'merges' input, find the predecessor automatically.
+ """
+ def __init__(graph, config_filename=None,
+ filename=None, title=None, branches=None, changes=None,
+ merges=[], annotations=[], **attrs):
+ """Return a new MergeDot graph generated from a config file or args."""
+ MergeGraph.__init__(graph)
+ pydot.Dot.__init__(graph, **attrs)
+
+ if config_filename:
+ graph.read_config(config_filename)
+ else:
+ graph.filename = filename
+ graph.title = title
+ graph.branches = branches
+ graph.changes = changes
+ graph.merges = merges
+ graph.annotations = annotations
+
+ graph.construct()
+
+ def read_config(graph, config_filename):
+ """Initialize a MergeDot graph's input data from a config file."""
+ import ConfigParser
+ if config_filename.endswith('.txt'):
+ default_basename = config_filename[:-4]
+ else:
+ default_basename = config_filename
+
+ config = ConfigParser.SafeConfigParser({ 'basename': default_basename,
+ 'title': None,
+ 'merges': '[]',
+ 'annotations': '[]' })
+ files_read = config.read(config_filename)
+ if len(files_read) == 0:
+ print >> sys.stderr, 'graph: unable to read graph config from "' + config_filename + '"'
+ sys.exit(1)
+ graph.basename = config.get('graph', 'basename')
+ graph.title = config.get('graph', 'title')
+ graph.branches = eval(config.get('graph', 'branches'))
+ graph.changes = eval(config.get('graph', 'changes'))
+ graph.merges = eval(config.get('graph', 'merges'))
+ graph.annotations = eval(config.get('graph', 'annotations'))
+
+ def construct(graph):
+ """"""
+ # Origin nodes (done first, in an attempt to set the order)
+ for br, orig, r1, head in graph.branches:
+ name = br + '0'
+ if r1 > 0:
+ graph.mk_origin_node(name, br)
+ else:
+ graph.mk_node(name, label=br)
+
+ # Edges and target nodes for merges
+ for merge in graph.merges:
+ # Emphasize the last merge, as it's the important one
+ important = (merge == graph.merges[-1])
+ graph.add_merge(merge, important)
+
+ # Parallel edges for basic lines of descent
+ for br, orig, r1, head in graph.branches:
+ sub_g = MergeSubgraph(ordering='out')
+ for i in range(1, head + 1):
+ prev_n = br + str(i - 1)
+ this_n = br + str(i)
+
+ # Normal edges and nodes
+ if i < r1:
+ graph.mk_invis_node(this_n)
+ else:
+ graph.mk_node(this_n)
+ if i <= r1:
+ graph.mk_invis_edge(prev_n, this_n)
+ else:
+ graph.mk_edge(prev_n, this_n)
+
+ # Branch creation edges
+ if orig:
+ sub_g.mk_br_edge(orig, br + str(r1))
+
+ graph.add_subgraph(sub_g)
+
+ # Annotations
+ for node, label in graph.annotations:
+ graph.add_annotation(node, label)
+
+ # A title for the graph (added last so it goes at the top)
+ if graph.title:
+ graph.add_node(Node('title', shape='plaintext', label='"' + graph.title + '"'))
+
+ def save(graph, format='png', filename=None):
+ """Save this merge graph to the given file format. If filename is None,
+ construct a filename from the basename of the original file (as passed
+ to the constructor and then stored in graph.basename) and the suffix
+ according to the given format.
+ """
+ if not filename:
+ filename = graph.basename + '.' + format
+ if format == 'sh':
+ import save_as_sh
+ save_as_sh.write_sh_file(graph, filename)
+ else:
+ pydot.Dot.write(graph, filename, format=format)