Started on getting --local to merge properly. Needs lots more testing!
[0publish.git] / merge.py
bloba56e82b852a06ecf9c3ba529dea1a65da6a06982
1 import os
2 from xml.dom import minidom, XMLNS_NAMESPACE, Node
3 from zeroinstall.injector.namespaces import XMLNS_IFACE
4 from zeroinstall.injector import model, reader
5 from logging import info
7 def childNodes(parent, namespaceURI, localName = None):
8 for x in parent.childNodes:
9 if x.nodeType != Node.ELEMENT_NODE: continue
10 if x.namespaceURI != namespaceURI: continue
12 if localName is None or x.localName == localName:
13 yield x
15 class Context:
16 def __init__(self, impl):
17 doc = impl.ownerDocument
18 self.attribs = {}
19 self.requires = []
21 node = impl
22 while True:
23 for name, value in node.attributes.itemsNS():
24 if name not in self.attribs:
25 self.attribs[name] = value
26 if node.nodeName == 'group':
27 # We don't care about <requires> inside <implementation>; they'll get copied over anyway
28 for x in childNodes(node, XMLNS_IFACE, 'requires'):
29 self.requires.append(x)
30 node = node.parentNode
31 if node.nodeName != 'group':
32 break
34 def find_impls(parent):
35 """Return all <implementation> children, including those inside groups."""
36 for x in childNodes(parent, XMLNS_IFACE):
37 if x.localName == 'implementation':
38 yield x
39 elif x.localName == 'group':
40 for y in find_impls(x):
41 yield y
43 def find_groups(parent):
44 """Return all <group> children, including those inside other groups."""
45 for x in childNodes(parent, XMLNS_IFACE, 'group'):
46 yield x
47 for y in find_groups(x):
48 yield y
50 def is_subset(group, impl):
51 for key in group.attribs:
52 if key not in impl.attribs.keys():
53 print "BAD", key
54 return False # Group sets an attribute the impl doesn't want
55 for g_req in group.requires:
56 for i_req in impl.requires:
57 if nodesEqual(g_req, i_req): break
58 else:
59 return False # Group adds a requires that the impl doesn't want
60 return True
62 def merge(data, local):
63 local_doc = minidom.parse(local)
64 master_doc = minidom.parseString(data)
66 # Merge each implementation in the local feed in turn (normally there will only be one)
67 for impl in find_impls(local_doc.documentElement):
68 # 1. Get the context of the implementation to add. This is:
69 # - The set of its requirements
70 # - Its attributes
71 new_impl_context = Context(impl)
73 # 2. For each <group> in the master feed, see if it provides a compatible context:
74 # - A subset of the new implementation's requirements
75 # - A subset of the new implementation's attributes (names, not values)
76 # Choose the most compatible <group> (the root counts as a minimally compatible group)
78 best_group = (0, master_doc.documentElement)
80 for group in find_groups(master_doc.documentElement):
81 group_context = Context(group)
82 if is_subset(group_context, new_impl_context):
83 best_group = (0, group)
85 group = best_group[1]
86 group_context = Context(group)
88 # If we have additional requirements, we'll need to create a subgroup and add them
89 if len(new_impl_context.requires) > len(group_context.requires):
90 subgroup = master_doc.createElementNS(XMLNS_IFACE, 'group')
91 group.appendChild(subgroup)
92 group = subgroup
93 group_context = Context(group)
94 for x in new_impl_context.requires:
95 for y in group_context.requires:
96 if nodesEqual(x, y): break
97 else:
98 req = master_doc.importNode(x, True)
99 #print "Add", req
100 group.appendChild(req)
102 new_impl = master_doc.importNode(impl, True)
103 for name, value in new_impl.attributes.itemsNS():
104 if name in group_context.attribs and group_context.attribs[name] == value:
105 #print "Deleting duplicate attribute", name, value
106 new_impl.removeAttributeNS(name[0], name[1])
107 else:
108 # Might have been on a parent group; move to the impl
109 #print "Set", name, value
110 new_impl.setAttributeNS(name[0], name[1], value)
112 group.appendChild(master_doc.createTextNode(' '))
113 group.appendChild(new_impl)
114 group.appendChild(master_doc.createTextNode('\n'))
116 return master_doc.toxml()