osm-git

A WIP POC based on the idea from https://blog.andygol.co.ua/en/2023/05/07/osm-2-0-api-using-git/
git clone git://archive.git.mtrnord.blog/MTRNord/osm-git.git
Log | Files | Refs | LICENSE

commit 55f0f5f351689b5031b3e843fb8a86751798533c
parent c298f43d901567c1fa946974360169638ac613a8
Author: MTRNord <mtrnord1@gmail.com>
Date:   Thu, 25 May 2023 20:19:33 +0200

Improve overall speed of the script by improving the changeset data loading and also fix a bug in the commit logic

Diffstat:
Msrc/git/mod.rs | 17++++++++++++++---
Msrc/osm/changesets.rs | 106++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/osm/osm_data.rs | 68++++++++++++++++++++++++++++++++++++++++----------------------------
3 files changed, 118 insertions(+), 73 deletions(-)

diff --git a/src/git/mod.rs b/src/git/mod.rs @@ -2,7 +2,7 @@ use std::{io::Write, path::Path}; use color_eyre::eyre::Result; use git2::{Oid, Repository, Signature}; -use tracing::info; +use tracing::{info, warn}; /// Initialize the git repository /// @@ -102,7 +102,15 @@ pub fn commit( } else { Path::new(&file) }; - index.add_path(path)?; + // TODO: I am tired to actually debug this so we just do a sanity check if the file exists + if file_path.exists() { + index.add_path(path)?; + } else { + warn!( + "File {} does not exist but was meant to be added", + path.to_str().unwrap() + ); + } } for file in removed_files { let file_path = Path::new(&file); @@ -111,7 +119,10 @@ pub fn commit( } else { Path::new(&file) }; - index.remove_path(path)?; + // We check if it was tracked before. If not we don't need to remove it + if index.get_path(path, 0).is_some() { + index.remove_path(path)?; + } } index.write()?; index.write_tree()? diff --git a/src/osm/changesets.rs b/src/osm/changesets.rs @@ -6,7 +6,7 @@ use quick_xml::{ }; use std::{ borrow::Cow, - collections::HashMap, + collections::{BTreeSet, HashMap}, convert::Infallible, fs::File, io::{BufReader, Write}, @@ -33,7 +33,8 @@ impl Changeset { fn new_from_element( reader: &mut Reader<BufReader<Decoder<'_, BufReader<File>>>>, element: &BytesStart, - ) -> Result<Self> { + changeset_list: &[u64], + ) -> Result<Option<Self>> { let changeset_attributes: HashMap<String, String> = element .attributes() .filter_map(|attr_result| attr_result.ok()) @@ -67,6 +68,11 @@ impl Changeset { //debug!("changeset_attributes: {:?}", changeset_attributes); + let id = changeset_attributes.get("id").unwrap().parse().unwrap(); + if !changeset_list.contains(&id) { + return Ok(None); + } + let mut changeset = Changeset { id: changeset_attributes.get("id").unwrap().parse().unwrap(), created_at: changeset_attributes.get("created_at").unwrap().to_string(), @@ -101,51 +107,55 @@ impl Changeset { loop { let event = reader.read_event_into(&mut new_buf)?; - if let Event::End(ref e) = event { - if e.name() == element.name() { - break; + match event { + Event::End(ref e) => { + if e.name() == element.name() { + break; + } } - } - if let Event::Start(ref e) = event { - let name = e.name(); - if name == QName(b"tag") { - let mut key = Cow::Borrowed(""); - let mut value = Cow::Borrowed(""); - - for attr_result in element.attributes() { - let a = attr_result?; - match a.key.as_ref() { - b"k" => key = a.decode_and_unescape_value(reader)?, - b"v" => value = a.decode_and_unescape_value(reader)?, - _ => (), + Event::Start(ref e) => { + let name = e.name(); + if name == QName(b"tag") { + let mut key = Cow::Borrowed(""); + let mut value = Cow::Borrowed(""); + + for attr_result in element.attributes() { + let a = attr_result?; + match a.key.as_ref() { + b"k" => key = a.decode_and_unescape_value(reader)?, + b"v" => value = a.decode_and_unescape_value(reader)?, + _ => (), + } } - } - changeset.tags.insert(key.to_string(), value.to_string()); - } else { - warn!("Unexpected tag: {:?}", name); - } - } else { - if let Event::Text(ref text) = event { - if text.borrow().starts_with(b"\n") { - continue; - } - } else if let Event::End(ref e) = event { - if e.name() == QName(b"tag") { - continue; + changeset.tags.insert(key.to_string(), value.to_string()); + } else { + warn!("Unexpected tag: {:?}", name); + //reader.read_to_end_into(e.name(), &mut new_buf); } } - warn!("Unexpected event in changeset: {:?}", event); - // Write the data to file for debugging + _ => { + if let Event::Text(ref text) = event { + if text.borrow().starts_with(b"\n") { + continue; + } + } else if let Event::End(ref e) = event { + if e.name() == QName(b"tag") { + continue; + } + } + warn!("Unexpected event in changeset: {:?}", event); + // Write the data to file for debugging - let mut file = std::fs::File::create("debug.xml")?; - file.write_all(&new_buf)?; - file.sync_all()?; + let mut file = std::fs::File::create("debug.xml")?; + file.write_all(&new_buf)?; + file.sync_all()?; + } } new_buf = Vec::new(); } - Ok(changeset) + Ok(Some(changeset)) } } @@ -170,23 +180,33 @@ pub fn parse_changeset( changeset_data.expand_empty_elements(true); let mut changesets = Vec::new(); + let changeset_hashset = changeset_list.iter().cloned().collect::<BTreeSet<u64>>(); let mut buf = Vec::new(); // Parse the changeset file info!("Parsing changeset file"); loop { + // If we already have all of them then break + // We compare the ids even if its a little more expensive + let ids_parsed = changesets + .iter() + .map(|c: &Changeset| c.id) + .collect::<BTreeSet<u64>>(); + if changeset_hashset.is_subset(&ids_parsed) { + break; + } + let event = changeset_data.read_event_into(&mut buf)?; match event { Event::Start(element) => { if let b"changeset" = element.name().as_ref() { // TODO: What do we do in case of an error? - let changeset = Changeset::new_from_element(changeset_data, &element); + let changeset = + Changeset::new_from_element(changeset_data, &element, changeset_list); match changeset { - Ok(changeset) => { - if changeset_list.contains(&changeset.id) { - changesets.push(changeset); - } + Ok(Some(changeset)) => { + changesets.push(changeset); } Err(err) => { error!( @@ -194,8 +214,10 @@ pub fn parse_changeset( &element, err ); } + _ => {} } } + //changeset_data.read_to_end_into(element.name(), &mut buf); } Event::Eof => break, // exits the loop when reaching end of file _ => (), // There are `Event` types not considered here diff --git a/src/osm/osm_data.rs b/src/osm/osm_data.rs @@ -469,6 +469,16 @@ pub enum OSMObject { Relation(Relation), } +impl OSMObject { + pub fn id(&self) -> u64 { + match self { + OSMObject::Node(node) => node.id, + OSMObject::Way(way) => way.id, + OSMObject::Relation(relation) => relation.id, + } + } +} + pub fn convert_objects_to_git( repository: &Repository, committer: &Signature, @@ -542,7 +552,7 @@ pub fn convert_objects_to_git( } } } else if name == QName(b"way") { - let way = Way::new_from_element(&mut data, &e); + let way = Way::new_from_element(&mut data, e); match way { Ok(way) => created_objects.push(OSMObject::Way(way)), Err(err) => { @@ -553,7 +563,7 @@ pub fn convert_objects_to_git( } } } else if name == QName(b"relation") { - let relation = Relation::new_from_element(&mut data, &e); + let relation = Relation::new_from_element(&mut data, e); match relation { Ok(relation) => { created_objects.push(OSMObject::Relation(relation)) @@ -595,7 +605,13 @@ pub fn convert_objects_to_git( OSMObject::Relation(ref relation) => format!("{}.yaml", relation.id), }; let object_file_path = repository_folder.join(object_file_name); - let object_file = std::fs::File::create(object_file_path)?; + + // We need to create the file + let object_file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&object_file_path)?; serde_yaml::to_writer(object_file, &object)?; // Add the object to the list of created objects for the changeset based on the changeset id @@ -627,7 +643,7 @@ pub fn convert_objects_to_git( if let Event::Start(ref e) = event { let name = e.name(); if name == QName(b"node") { - let node = Node::new_from_element(&mut data, &e); + let node = Node::new_from_element(&mut data, e); match node { Ok(node) => deleted_objects.push(OSMObject::Node(node)), Err(err) => { @@ -638,7 +654,7 @@ pub fn convert_objects_to_git( } } } else if name == QName(b"way") { - let way = Way::new_from_element(&mut data, &e); + let way = Way::new_from_element(&mut data, e); match way { Ok(way) => deleted_objects.push(OSMObject::Way(way)), Err(err) => { @@ -649,7 +665,7 @@ pub fn convert_objects_to_git( } } } else if name == QName(b"relation") { - let relation = Relation::new_from_element(&mut data, &e); + let relation = Relation::new_from_element(&mut data, e); match relation { Ok(relation) => { deleted_objects.push(OSMObject::Relation(relation)) @@ -779,7 +795,7 @@ pub fn convert_objects_to_git( if let Event::Start(ref e) = event { let name = e.name(); if name == QName(b"node") { - let node = Node::new_from_element(&mut data, &e); + let node = Node::new_from_element(&mut data, e); match node { Ok(node) => deleted_objects.push(OSMObject::Node(node)), Err(err) => { @@ -790,7 +806,7 @@ pub fn convert_objects_to_git( } } } else if name == QName(b"way") { - let way = Way::new_from_element(&mut data, &e); + let way = Way::new_from_element(&mut data, e); match way { Ok(way) => deleted_objects.push(OSMObject::Way(way)), Err(err) => { @@ -801,7 +817,7 @@ pub fn convert_objects_to_git( } } } else if name == QName(b"relation") { - let relation = Relation::new_from_element(&mut data, &e); + let relation = Relation::new_from_element(&mut data, e); match relation { Ok(relation) => { deleted_objects.push(OSMObject::Relation(relation)) @@ -858,20 +874,6 @@ pub fn convert_objects_to_git( .entry(changeset) .or_insert_with(Vec::new) .push(object.clone()); - // Remove it from the list of created objects if it exists - if let Some(index) = created_or_modified_objects_for_changeset - .get_mut(&changeset) - .and_then(|objects| { - objects - .iter() - .position(|existing_object| *existing_object == object) - }) - { - created_or_modified_objects_for_changeset - .get_mut(&changeset) - .unwrap() - .remove(index); - } } } _ => (), @@ -895,6 +897,18 @@ pub fn convert_objects_to_git( let mut last_highest_id = 0; let mut changeset_path = String::new(); for changeset_file in changeset_files { + // Delete all objects by id that are in deleted_objects_for_changeset from created_or_modified_objects_for_changeset + let deleted_ids: Vec<u64> = deleted_objects_for_changeset + .values() + .flatten() + .map(|object| object.id()) + .collect(); + created_or_modified_objects_for_changeset + .iter_mut() + .for_each(|(_, objects)| { + objects.retain(|object| !deleted_ids.contains(&object.id())); + }); + let changeset_file = changeset_file?; let changeset_file_path = changeset_file.path(); let changeset_file_name = changeset_file_path.file_name().unwrap().to_str().unwrap(); @@ -914,6 +928,8 @@ pub fn convert_objects_to_git( let changesets = parse_changeset(&mut uncompressed_data, &changeset_list)?; + info!("Generating commits for changesets"); + for changeset_id in changeset_list { // Find the changeset within the files of the cache let changeset = find_changesets_in_cache(&changesets, changeset_id)?; @@ -1032,11 +1048,7 @@ fn find_changesets_in_cache( changesets: &[Changeset], changeset_id: u64, ) -> Result<Option<&Changeset>> { - let mut changeset = None; - - if changesets.iter().any(|c| c.id == changeset_id) { - changeset = changesets.iter().find(|c| c.id == changeset_id); - } + let changeset = changesets.iter().find(|c| c.id == changeset_id); Ok(changeset) }