3
# ====================================================================
4
# Licensed to the Apache Software Foundation (ASF) under one
5
# or more contributor license agreements. See the NOTICE file
6
# distributed with this work for additional information
7
# regarding copyright ownership. The ASF licenses this file
8
# to you under the Apache License, Version 2.0 (the
9
# "License"); you may not use this file except in compliance
10
# with the License. You may obtain a copy of the License at
12
# http://www.apache.org/licenses/LICENSE-2.0
14
# Unless required by applicable law or agreed to in writing,
15
# software distributed under the License is distributed on an
16
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17
# KIND, either express or implied. See the License for the
18
# specific language governing permissions and limitations
20
# ====================================================================
25
filename = merge-sync-1.png
26
title = Sync Merge: CC vs SVN
27
# Branches: (branch name, branched from node, first rev, last rev).
33
# Changes: nodes in which a change was committed; merge targets need not
36
'A1', 'A2', 'A3', 'A4',
37
'B1', 'B2', 'B3', 'B4', 'B5'
39
# Merges: (base node, source-right node, target node, label).
40
# Base is also known as source-left.
42
('O0', 'A:1', 'B3', 'sync'),
43
('A2', 'A:3', 'B5', 'sync'),
45
# Annotations for nodes: (node, annotation text).
51
# Notes about different kinds of merge.
53
# A basic 3-way merge is ...
55
# The ClearCase style of merge is a 3-way merge.
57
# The Subversion style of merge (that is, one phase of a Subversion merge)
58
# is a three-way merge with its base (typically the YCA) on the source branch.
63
from pydot import Node, Edge
66
def mergeinfo_to_node_list(mi):
67
"""Convert a mergeinfo string such as '/foo:1,3-5*' into a list of
68
node names such as ['foo1', 'foo3', 'foo4', 'foo5'].
70
### Doesn't yet strip the leading slash.
73
for mi_str in mi.split(' '):
74
path, ranges = mi_str.split(':')
75
for r in ranges.split(','):
77
# TODO: store & use this 'non-inheritable' flag
86
for rev in range(r1, r2 + 1):
87
l.append(path + str(rev))
91
class MergeGraph(pydot.Graph):
92
"""Base class, not intended for direct use. Use MergeDot for the main
93
graph and MergeSubgraph for a subgraph.
96
def mk_origin_node(graph, name, label):
97
"""Add a node to the graph"""
98
graph.add_node(Node(name, label=label, shape='plaintext'))
100
def mk_invis_node(graph, name):
101
"""Add a node to the graph"""
102
graph.add_node(Node(name, style='invis'))
104
def mk_node(graph, name, label=None):
105
"""Add a node to the graph, if not already present"""
106
if not graph.get_node(name):
109
if name in graph.changes:
110
graph.add_node(Node(name, label=label))
112
graph.add_node(Node(name, color='grey', label=''))
114
def mk_merge_target(graph, target_node, important):
115
"""Add a merge target node to the graph."""
120
graph.add_node(Node(target_node, color=color, fontcolor=color, style='bold'))
122
def mk_edge(graph, name1, name2, **attrs):
123
"""Add an ordinary edge to the graph"""
124
graph.add_edge(Edge(name1, name2, dir='none', style='dotted', color='grey', **attrs))
126
def mk_br_edge(graph, name1, name2):
127
"""Add a branch-creation edge to the graph"""
128
# Constraint=false to avoid the Y-shape skewing the nice parallel branch lines
129
graph.mk_edge(name1, name2, constraint='false')
131
def mk_merge_edge(graph, src_node, tgt_node, kind, label, important):
132
"""Add a merge edge to the graph"""
137
e = Edge(src_node, tgt_node, constraint='false',
138
label='"' + label + '"',
139
color=color, fontcolor=color,
141
if kind.startswith('cherry'):
142
e.set_style('dashed')
145
def mk_mergeinfo_edge(graph, base_node, src_node, important):
151
graph.add_edge(Edge(base_node, src_node,
152
dir='both', arrowtail='odot', arrowhead='tee',
153
color=color, constraint='false'))
155
def mk_invis_edge(graph, name1, name2):
156
"""Add an invisible edge to the graph"""
157
graph.add_edge(Edge(name1, name2, style='invis'))
159
def add_merge(graph, merge, important):
161
base_node, src_node, tgt_node, kind = merge
163
if base_node and src_node: # and not kind.startwith('cherry'):
164
graph.mk_mergeinfo_edge(base_node, src_node, important)
167
graph.mk_merge_target(tgt_node, important)
170
graph.mk_merge_edge(src_node, tgt_node, kind, kind, important)
172
def add_annotation(graph, node, label, color='lightblue'):
173
"""Add a graph node that serves as an annotation to a normal node.
174
More than one annotation can be added to the same normal node.
176
subg_name = node + '_annotations'
178
def get_subgraph(graph, name):
179
"""Equivalent to pydot.Graph.get_subgraph() when there is no more than
180
one subgraph of the given name, but working aroung a bug in
181
pydot.Graph.get_subgraph().
183
for subg in graph.get_subgraph_list():
184
if subg.get_name() == name:
188
g = get_subgraph(graph, subg_name)
190
g = pydot.Subgraph(subg_name, rank='same')
191
graph.add_subgraph(g)
193
ann_node = node + '_'
194
while g.get_node(ann_node):
195
ann_node = ann_node + '_'
196
g.add_node(Node(ann_node, shape='box', style='filled', color=color,
197
label='"' + label + '"'))
198
g.add_edge(Edge(ann_node, node, style='solid', color=color,
199
dir='none', constraint='false'))
201
class MergeSubgraph(MergeGraph, pydot.Subgraph):
203
def __init__(graph, **attrs):
205
MergeGraph.__init__(graph)
206
pydot.Subgraph.__init__(graph, **attrs)
208
class MergeDot(MergeGraph, pydot.Dot):
210
# TODO: In the 'merges' input, find the predecessor automatically.
212
def __init__(graph, config_filename=None,
213
filename=None, title=None, branches=None, changes=None,
214
merges=[], annotations=[], **attrs):
215
"""Return a new MergeDot graph generated from a config file or args."""
216
MergeGraph.__init__(graph)
217
pydot.Dot.__init__(graph, **attrs)
220
graph.read_config(config_filename)
222
graph.filename = filename
224
graph.branches = branches
225
graph.changes = changes
226
graph.merges = merges
227
graph.annotations = annotations
231
def read_config(graph, config_filename):
232
"""Initialize a MergeDot graph's input data from a config file."""
234
if config_filename.endswith('.txt'):
235
default_basename = config_filename[:-4]
237
default_basename = config_filename
239
config = ConfigParser.SafeConfigParser({ 'basename': default_basename,
242
'annotations': '[]' })
243
files_read = config.read(config_filename)
244
if len(files_read) == 0:
245
print >> sys.stderr, 'graph: unable to read graph config from "' + config_filename + '"'
247
graph.basename = config.get('graph', 'basename')
248
graph.title = config.get('graph', 'title')
249
graph.branches = eval(config.get('graph', 'branches'))
250
graph.changes = eval(config.get('graph', 'changes'))
251
graph.merges = eval(config.get('graph', 'merges'))
252
graph.annotations = eval(config.get('graph', 'annotations'))
254
def construct(graph):
256
# Origin nodes (done first, in an attempt to set the order)
257
for br, orig, r1, head in graph.branches:
260
graph.mk_origin_node(name, br)
262
graph.mk_node(name, label=br)
264
# Edges and target nodes for merges
265
for merge in graph.merges:
266
# Emphasize the last merge, as it's the important one
267
important = (merge == graph.merges[-1])
268
graph.add_merge(merge, important)
270
# Parallel edges for basic lines of descent
271
for br, orig, r1, head in graph.branches:
272
sub_g = MergeSubgraph(ordering='out')
273
for i in range(1, head + 1):
274
prev_n = br + str(i - 1)
277
# Normal edges and nodes
279
graph.mk_invis_node(this_n)
281
graph.mk_node(this_n)
283
graph.mk_invis_edge(prev_n, this_n)
285
graph.mk_edge(prev_n, this_n)
287
# Branch creation edges
289
sub_g.mk_br_edge(orig, br + str(r1))
291
graph.add_subgraph(sub_g)
294
for node, label in graph.annotations:
295
graph.add_annotation(node, label)
297
# A title for the graph (added last so it goes at the top)
299
graph.add_node(Node('title', shape='plaintext', label='"' + graph.title + '"'))
301
def save(graph, format='png', filename=None):
302
"""Save this merge graph to the given file format. If filename is None,
303
construct a filename from the basename of the original file (as passed
304
to the constructor and then stored in graph.basename) and the suffix
305
according to the given format.
308
filename = graph.basename + '.' + format
311
save_as_sh.write_sh_file(graph, filename)
313
pydot.Dot.write(graph, filename, format=format)