Add tool for bumping images

Skip-Release-Notes

Depends-On: https://review.vexxhost.dev/c/atmosphere/+/555
Change-Id: I500b01ef58b54680e52c04f3c4576b640e07e1be
(cherry picked from commit 7f6414c93f48dca1677c67a19ad4b5de6b233209)
diff --git a/crates/imagebumper/src/repository.rs b/crates/imagebumper/src/repository.rs
new file mode 100644
index 0000000..2f1a274
--- /dev/null
+++ b/crates/imagebumper/src/repository.rs
@@ -0,0 +1,66 @@
+use crate::clients::ClientSet;
+use crate::RepositoryClient;
+use std::error::Error;
+use url::Url;
+
+pub struct Repository<'a> {
+    pub owner: String,
+    pub name: String,
+    client: &'a dyn RepositoryClient,
+}
+
+impl<'a> Repository<'a> {
+    pub fn from_url(clientset: &'a ClientSet, url: &str) -> Result<Self, Box<dyn Error>> {
+        let url = url.trim_end_matches(".git");
+        let parsed_url = Url::parse(url)?;
+        let hostname = parsed_url.host_str().ok_or("Invalid repository URL")?;
+        let parts: Vec<&str> = parsed_url
+            .path_segments()
+            .ok_or("Invalid repository URL")?
+            .collect();
+        if parts.len() < 2 {
+            return Err("Invalid repository URL".into());
+        }
+
+        let client: &dyn RepositoryClient = match hostname {
+            "opendev.org" => &clientset.opendev,
+            "github.com" => &clientset.github,
+            _ => return Err("Unsupported repository host".into()),
+        };
+
+        Ok(Repository {
+            owner: parts[parts.len() - 2].to_string(),
+            name: parts[parts.len() - 1].to_string(),
+            client,
+        })
+    }
+
+    pub async fn get_latest_commit(&self, branch: &str) -> Result<String, Box<dyn Error>> {
+        self.client.get_latest_commit(self, branch).await
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn test_from_url_for_opendev() {
+        let clientset = ClientSet::new();
+        let repo =
+            Repository::from_url(&clientset, "https://opendev.org/openstack/nova.git").unwrap();
+
+        assert_eq!(repo.owner, "openstack");
+        assert_eq!(repo.name, "nova");
+    }
+
+    #[tokio::test]
+    async fn test_from_url_for_github() {
+        let clientset = ClientSet::new();
+        let repo =
+            Repository::from_url(&clientset, "https://github.com/vexxhost/atmosphere.git").unwrap();
+
+        assert_eq!(repo.owner, "vexxhost");
+        assert_eq!(repo.name, "atmosphere");
+    }
+}