Download:
child 1:ca3e20296065
0:e71c3189c282 draft
Anton Shestakov <av6@dwimlabs.net>, Fri, 10 Nov 2017 11:52:37 +0800
first version

2 файлов изменено, 144 вставок(+), 0 удалений(-) [+]
.hgignore file | annotate | diff | comparison | revisions
diff.py file | annotate | diff | comparison | revisions
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Fri Nov 10 11:52:37 2017 +0800
@@ -0,0 +1,3 @@
+syntax: glob
+
+*.pyc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/diff.py Fri Nov 10 11:52:37 2017 +0800
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+from argparse import ArgumentParser
+from difflib import unified_diff
+from subprocess import run
+from sys import stderr
+from tempfile import NamedTemporaryFile
+from time import sleep
+from urllib.request import urlopen
+from xml.etree import ElementTree as ET
+
+
+API = 'https://api.openstreetmap.org/api/0.6'
+
+
+def clear_progress():
+ stderr.write('\n')
+ stderr.flush()
+
+
+def fetch_xml(url, what, n, total, delay=200, spins=r'/-\|'):
+ sleep(delay / 1000)
+ stderr.write('\r{} {} {} out of {}'.format(spins[n % len(spins)], what, n, total))
+ stderr.flush()
+ with urlopen(url) as response:
+ tree = ET.parse(response)
+ return tree.getroot()
+
+
+def parse_item_xml(kind, el):
+ data = {
+ 'attrs': el.attrib,
+ 'tags': {
+ tag.get('k'): tag.get('v')
+ for tag in el.findall('tag')
+ }
+ }
+ if kind == 'way':
+ data['refs'] = [
+ (ref.tag, ref.get('ref'), None)
+ for ref in el.findall('*[@ref]')
+ ]
+ elif kind == 'relation':
+ data['refs'] = [
+ (member.get('type'), member.get('ref'), member.get('role'))
+ for member in el.findall('member')
+ ]
+ return data
+
+
+def bootstrap(csid):
+ items = {'node': {}, 'way': {}, 'relation': {}}
+
+ # changeset metadata mostly
+ xml = fetch_xml('{}/changeset/{}'.format(API, csid), 'changeset data', 1, 2, delay=0)
+ changeset = {
+ 'attrs': xml.find('changeset').attrib,
+ 'tags': {
+ tag.get('k'): tag.get('v')
+ for tag in xml.find('changeset').findall('tag')
+ }
+ }
+
+ # current versions of nodes, ways, relations
+ xml = fetch_xml('{}/changeset/{}/download'.format(API, csid), 'changeset data', 2, 2)
+ for node in xml:
+ if node.tag not in ('create', 'modify', 'delete'):
+ continue
+ el = node[0]
+ if el.tag not in ('node', 'way', 'relation'):
+ continue
+ id = int(el.get('id'))
+ version = int(el.get('version'))
+ items[el.tag][id] = {
+ version: parse_item_xml(el.tag, el)
+ }
+
+ # fetch previous versions
+ for kind in ('node', 'way', 'relation'):
+ clear_progress()
+ total = len(items[kind])
+ for n, id in enumerate(items[kind], 1):
+ version = min(items[kind][id])
+ if version == 1:
+ continue
+ version -= 1
+ xml = fetch_xml('{}/{}/{}/{}'.format(API, kind, id, version), kind + 's', n, total)
+ items[kind][id][version] = parse_item_xml(kind, xml.find(kind))
+
+ return changeset, items
+
+
+def level0lify(changeset, items, refmap={'node': 'nd', 'way': 'wy'}):
+ a = []
+ b = []
+ a.append('# previous changeset is most likely unrelated')
+ b.append('changeset {}'.format(changeset['attrs']['id']))
+ b += [' {} = {}'.format(k, v) for k, v in sorted(changeset['tags'].items())]
+ for kind in ('relation', 'way', 'node'):
+ for id in sorted(items[kind]):
+ versions = items[kind][id]
+ old = min(versions)
+ new = max(versions)
+ for lines, version in ((a, old), (b, new)):
+ lines.append('')
+ lines.append('{} {}'.format(kind, id))
+ lines += [
+ ' {} = {}'.format(k, v)
+ for k, v in sorted(versions[version]['tags'].items())
+ ]
+ lines += [
+ ' {} {}{}'.format(refmap.get(kind, kind), id, ' ' + role if role is not None else '')
+ for kind, id, role in versions[version]['refs']
+ ]
+ return a, b
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument('changeset', type=int, help='changeset id')
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument('-U', '--unified', type=int, help='lines of context')
+ group.add_argument('-t', '--tool', help='external tool to use for diffing')
+ args = parser.parse_args()
+
+ changeset, items = bootstrap(args.changeset)
+ a, b = level0lify(changeset, items)
+ clear_progress()
+ if args.tool is None:
+ context = args.unified if args.unified is not None else 3
+ diff = unified_diff(a, b, n=context, lineterm='')
+ print('\n'.join(diff))
+ else:
+ with NamedTemporaryFile() as afile, NamedTemporaryFile() as bfile:
+ for fd, lines in ((afile, a), (bfile, b)):
+ fd.write('\n'.join(lines).encode('utf-8'))
+ fd.flush()
+ run([args.tool, afile.name, bfile.name])
+
+
+if __name__ == '__main__':
+ main()