Implement git commit and push using credentials stored in credential manager

This commit is contained in:
Robin Gottschalk 2025-02-02 16:52:28 +01:00
parent 467d86f53d
commit aa9209b7d6
5 changed files with 182 additions and 11 deletions

91
Cargo.lock generated
View File

@ -64,6 +64,21 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" 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]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.9" version = "0.3.9"
@ -153,6 +168,12 @@ version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.9.0" version = "1.9.0"
@ -249,6 +270,20 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@ -416,7 +451,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading 0.7.4", "libloading 0.8.6",
] ]
[[package]] [[package]]
@ -856,6 +891,29 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@ -1077,6 +1135,17 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "kqueue" name = "kqueue"
version = "1.0.8" version = "1.0.8"
@ -1356,6 +1425,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 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]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.7.3" version = "0.7.3"
@ -1584,7 +1662,9 @@ dependencies = [
name = "obsidian-git-backup" name = "obsidian-git-backup"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"git2", "git2",
"keyring",
"notify", "notify",
"tray-icon", "tray-icon",
"winit", "winit",
@ -2498,6 +2578,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"

View File

@ -8,3 +8,5 @@ winit = "0.30.8"
notify = "8.0.0" notify = "8.0.0"
tray-icon = "0.19.2" tray-icon = "0.19.2"
git2 = "0.20.0" git2 = "0.20.0"
chrono = "0.4.39"
keyring = { version = "3.6.1", features = ["windows-native"] }

View File

@ -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] 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) - [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 - [ ] Some error management
- [x] Tray Menu - [x] Tray Menu
- [x] Exit - [x] Exit

View File

@ -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<dyn std::error::Error>> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
println!("Checking for changes..."); // Open the repository at the provided path
// TODO log changed files first 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()); 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("<unknown>");
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")
} }

View File

@ -1,5 +1,4 @@
use notify::{Event, RecursiveMode, Watcher}; use notify::{Event, RecursiveMode, Watcher};
use std::path::Path; use std::path::Path;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tray_icon::{ use tray_icon::{
@ -16,8 +15,8 @@ use winit::{
mod filesync; mod filesync;
mod gitpush; mod gitpush;
const SOURCE_VAULT: &str = "D:\\test\\obsidian-vault-source"; // syncthing vault folder const SOURCE_VAULT: &str = "X:\\test\\obsidian-vault-source"; // syncthing vault folder
const REPO_VAULT: &str = "D:\\test\\obsidian-vault-test"; // git repo folder const REPO_VAULT: &str = "X:\\test\\obsidian-vault-test"; // git repo folder
#[derive(Debug)] #[derive(Debug)]
enum UserEvent { enum UserEvent {
@ -70,7 +69,8 @@ impl ApplicationHandler<UserEvent> for App {
println!("---> Backup triggered by menu"); println!("---> Backup triggered by menu");
// Handle push now menu item // Handle push now menu item
self.last_change_time = None; 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() => { id if id == self.menu_exit_id.as_ref().unwrap() => {
println!("---> Exit triggert by menu"); println!("---> Exit triggert by menu");
@ -101,7 +101,8 @@ impl ApplicationHandler<UserEvent> for App {
if now.duration_since(last_change_time) > self.duration_threshold { if now.duration_since(last_change_time) > self.duration_threshold {
println!("---> Backup triggered by file change"); println!("---> Backup triggered by file change");
self.last_change_time = None; 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")) filesync::sync_directory(source_dir, repo_dir, Some(".git"))
.expect("Directories could not be synced"); .expect("Directories could not be synced");
println!("Sync took {:?}", start.elapsed()); println!("Sync took {:?}", start.elapsed());
gitpush::backup_changes(); gitpush::backup_changes(repo_dir).expect("Changes could not be pushed");
// Create event loop // Create event loop
let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap(); let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap();