From aa9209b7d63c1d3d73150db823716840caea8a6f Mon Sep 17 00:00:00 2001 From: Robin Gottschalk Date: Sun, 2 Feb 2025 16:52:28 +0100 Subject: [PATCH] Implement git commit and push using credentials stored in credential manager --- Cargo.lock | 91 +++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 ++ README.md | 2 +- src/gitpush.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 13 ++++---- 5 files changed, 182 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e673469..2744ac0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -153,6 +168,12 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.9.0" @@ -249,6 +270,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "combine" version = "4.6.7" @@ -416,7 +451,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.6", ] [[package]] @@ -856,6 +891,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1077,6 +1135,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833" +dependencies = [ + "byteorder", + "log", + "windows-sys 0.59.0", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1356,6 +1425,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -1584,7 +1662,9 @@ dependencies = [ name = "obsidian-git-backup" version = "0.1.0" dependencies = [ + "chrono", "git2", + "keyring", "notify", "tray-icon", "winit", @@ -2498,6 +2578,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 12508fc..c31f49d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,5 @@ winit = "0.30.8" notify = "8.0.0" tray-icon = "0.19.2" git2 = "0.20.0" +chrono = "0.4.39" +keyring = { version = "3.6.1", features = ["windows-native"] } diff --git a/README.md b/README.md index 0678354..03a5475 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This script automates the process of backing up your Obsidian vault to a Git rep - [x] Trigger backup after file changes with delay - [x] Maintain git repo in a seperate folder to not have the repo synced by syncthing (copy changed files over) -- [ ] Push changes to remote repository +- [x] Push changes to remote repository - [ ] Some error management - [x] Tray Menu - [x] Exit diff --git a/src/gitpush.rs b/src/gitpush.rs index 2f665a1..5f334d8 100644 --- a/src/gitpush.rs +++ b/src/gitpush.rs @@ -1,8 +1,87 @@ -pub fn backup_changes() { +use chrono::Local; +use git2::{Cred, IndexAddOption, PushOptions, RemoteCallbacks, Repository, StatusOptions}; +use keyring::Entry; +use std::path::Path; + +pub fn backup_changes(repo_path: &Path) -> Result<(), Box> { let start = std::time::Instant::now(); - println!("Checking for changes..."); - // TODO log changed files first + // Open the repository at the provided path + let repo = Repository::open(repo_path)?; + // Gather the repository statuses, including untracked files. + let mut status_opts = StatusOptions::new(); + status_opts.include_untracked(true); + let statuses = repo.statuses(Some(&mut status_opts))?; + + // If no changes are detected, exit early + if statuses.is_empty() { + println!("No changes detected"); + println!("Backup took {:?}", start.elapsed()); + return Ok(()); + } + + // Print out all changes. + println!("Detected changes:"); + for entry in statuses.iter() { + let path = entry.path().unwrap_or(""); + let status = entry.status(); + println!(" - {}: {:?}", path, status); + } + + // Add all changes to the index + let mut index = repo.index()?; + index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None)?; + index.write()?; + + // Write the index to a tree + let tree_id = index.write_tree()?; + let tree = repo.find_tree(tree_id)?; + + // Use the repository signature for both author and committer + let signature = repo.signature()?; + + // Determine the parent commit + let parent_commit = repo.head()?.peel_to_commit()?; + + // Generate commit message with current timestamp + let commit_message = format!("{} Autobackup", Local::now().format("%Y-%m-%d %H:%M:%S")); + + // Create commit + repo.commit( + Some("HEAD"), + &signature, + &signature, + &commit_message, + &tree, + &[&parent_commit], + )?; + + // Set up remote push with credentials callback + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(move |_url, username, _allowed_types| { + let user = username.unwrap_or("git"); + let token = get_git_token(); + Cred::userpass_plaintext(user, &token) + }); + + // Push changes + let mut push_options = PushOptions::new(); + push_options.remote_callbacks(callbacks); + let mut remote = repo.find_remote("origin")?; + remote.push(&["refs/heads/main"], Some(&mut push_options))?; + + println!("Changes committed and pushed successfully."); println!("Backup took {:?}", start.elapsed()); + Ok(()) +} + +fn get_git_token() -> String { + // Get token from Windows Credential Manager + let service = "obsidian-git-backup"; + let username = "git"; + let entry = Entry::new(service, username).unwrap(); + entry + .get_password() + .expect("Git access token could not be retrieved from Windows Credential Manager") } diff --git a/src/main.rs b/src/main.rs index 05a6d3d..cb647b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use notify::{Event, RecursiveMode, Watcher}; - use std::path::Path; use std::time::{Duration, Instant}; use tray_icon::{ @@ -16,8 +15,8 @@ use winit::{ mod filesync; mod gitpush; -const SOURCE_VAULT: &str = "D:\\test\\obsidian-vault-source"; // syncthing vault folder -const REPO_VAULT: &str = "D:\\test\\obsidian-vault-test"; // git repo folder +const SOURCE_VAULT: &str = "X:\\test\\obsidian-vault-source"; // syncthing vault folder +const REPO_VAULT: &str = "X:\\test\\obsidian-vault-test"; // git repo folder #[derive(Debug)] enum UserEvent { @@ -70,7 +69,8 @@ impl ApplicationHandler for App { println!("---> Backup triggered by menu"); // Handle push now menu item self.last_change_time = None; - gitpush::backup_changes(); + let repo_dir = Path::new(REPO_VAULT); + gitpush::backup_changes(repo_dir).expect("Changes could not be pushed"); } id if id == self.menu_exit_id.as_ref().unwrap() => { println!("---> Exit triggert by menu"); @@ -101,7 +101,8 @@ impl ApplicationHandler for App { if now.duration_since(last_change_time) > self.duration_threshold { println!("---> Backup triggered by file change"); self.last_change_time = None; - gitpush::backup_changes(); + let repo_dir = Path::new(REPO_VAULT); + gitpush::backup_changes(repo_dir).expect("Changes could not be pushed"); } } @@ -126,7 +127,7 @@ fn main() { filesync::sync_directory(source_dir, repo_dir, Some(".git")) .expect("Directories could not be synced"); println!("Sync took {:?}", start.elapsed()); - gitpush::backup_changes(); + gitpush::backup_changes(repo_dir).expect("Changes could not be pushed"); // Create event loop let event_loop = EventLoop::::with_user_event().build().unwrap();