From 147e7cdf8c8f21a4ba5bda3b40fa881a372b3daa Mon Sep 17 00:00:00 2001 From: Ben Miller Date: Wed, 18 Jun 2025 23:55:52 -0600 Subject: [PATCH] foam_sync.ps1 (LIFEBALANCE) 2025-06-18T23:55:52Z --- .vscode/settings.json | 2 +- foam_sync.ps1 | 236 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 foam_sync.ps1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c2d074..92a8d64 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,5 @@ "markdown.styles": [ ".vscode/custom-tag-style.css" ], - "gitdoc.enabled": true + "gitdoc.enabled": false } diff --git a/foam_sync.ps1 b/foam_sync.ps1 new file mode 100644 index 0000000..12c42e5 --- /dev/null +++ b/foam_sync.ps1 @@ -0,0 +1,236 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + A PowerShell script to periodically synchronize a Git repository, similar to the foam_sync.sh bash script. + It stages all changes, commits if there are any, and then syncs with the 'origin/main' remote. + It also attempts to set up a Windows Scheduled Task to run itself periodically. + +.DESCRIPTION + This script performs the following actions: + 1. Configures a Scheduled Task to run this script at a defined frequency. + - The first time this script is run, it might require Administrator privileges to register the task. + 2. Navigates to the script's directory (assumed to be the Git repository root). + 3. Stages all changes using 'git add .'. + 4. Checks for actual staged changes using 'git diff --staged --quiet'. + 5. If changes exist, commits them with a timestamped message. + 6. If no meaningful changes were staged, resets the staging area. + 7. Updates the local knowledge of the remote 'origin'. + 8. Compares local HEAD with 'origin/main'. + 9. If they differ, it sleeps for a random interval to avoid race conditions. + 10. Determines if local is ahead, remote is ahead, or they have diverged. + 11. Performs 'git pull --rebase' or 'git push' accordingly. + 12. In case of divergence, it attempts a rebase, preferring the newer commit (based on timestamp) + or 'ours' if timestamps are equal. Then pushes. + +.NOTES + Author: Gemini Code Assist (Translated from bash) + Version: 1.0 + Prerequisites: Git must be installed and in the system PATH. + Running for the first time: You may need to run this script as an Administrator + to allow the Scheduled Task to be registered. +#> + +# --- Configuration --- +$frequencyMinutes = 2 # How often the Scheduled Task should attempt to run this script + +# --- Script Setup --- +$scriptPath = $MyInvocation.MyCommand.Path +$scriptDir = Split-Path -Path $scriptPath -Parent +$scriptName = (Get-Item $scriptPath).Name +Write-Host "Script: $scriptName at $scriptPath" +Write-Host "Repository directory: $scriptDir" +Write-Host "Sync frequency: Every $frequencyMinutes minutes" + +# --- Scheduled Task Setup --- +$taskName = "FoamGitSync" +$taskDescription = "Periodically synchronizes the Git repository at $scriptDir using $scriptName." + +# Run as the user who executes this script. +$taskPrincipal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType InteractiveOrPassword + +# Trigger configuration +$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $frequencyMinutes) -RepetitionDuration ([TimeSpan]::MaxValue) + +# Action configuration: run this script +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`"" + +# Task settings +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 1) ` + -StopIfGoingOnBatteries:$false # Explicitly ensure it doesn't stop + +# Check and configure the scheduled task +try { + $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + + $needsUpdateOrCreation = $true + if ($existingTask) { + Write-Host "Scheduled task '$taskName' already exists. Checking configuration..." + $currentTrigger = $existingTask.Triggers[0] + $currentAction = $existingTask.Actions[0] + + $triggerMatches = $false + if ($currentTrigger -is [Microsoft.Management.Infrastructure.CimInstance] ` + -and $currentTrigger.RepetitionInterval.TotalMinutes -eq $frequencyMinutes) { + $triggerMatches = $true + } + + $actionMatches = $false + if ($currentAction -is [Microsoft.Management.Infrastructure.CimInstance] ` + -and $currentAction.Execute -eq "powershell.exe" ` + -and $currentAction.Argument -eq ("-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`"")) { + $actionMatches = $true + } + + if ($triggerMatches -and $actionMatches) { + Write-Host "Scheduled task '$taskName' is already correctly configured." + $needsUpdateOrCreation = $false + } + else { + Write-Host "Scheduled task '$taskName' configuration differs. It will be updated." + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + } + } + + if ($needsUpdateOrCreation) { + if ($existingTask -and -not ($triggerMatches -and $actionMatches)) { + Write-Host "Updating scheduled task '$taskName'..." + } + else { + Write-Host "Creating scheduled task '$taskName'..." + } + Register-ScheduledTask -TaskName $taskName -Description $taskDescription -Principal $taskPrincipal -Trigger $trigger -Action $action -Settings $settings -ErrorAction Stop + Write-Host "Scheduled task '$taskName' registered/updated successfully." + } +} +catch { + Write-Warning "Failed to register or update scheduled task '$taskName'. Error: $($_.Exception.Message)" + Write-Warning "You may need to run this script as Administrator once to register the scheduled task." +} + +# --- Git Operations --- +Write-Host "Navigating to repository: $scriptDir" +try { + Set-Location -Path $scriptDir -ErrorAction Stop +} +catch { + Write-Error "Unable to find repository at $scriptDir. Exiting script." + exit 1 +} + +Write-Host "Staging all changes with 'git add .'" +git add . +if ($LASTEXITCODE -ne 0) { + Write-Warning "'git add .' command failed with exit code $LASTEXITCODE." +} + +Write-Host "Checking for staged changes with 'git diff --staged --quiet'..." +git diff --staged --quiet +$changesStaged = ($LASTEXITCODE -ne 0) + +if ($changesStaged) { + Write-Host "Staged changes detected. Creating commit..." + $commitMessage = "$scriptName ($($env:COMPUTERNAME)) $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')" + Write-Host "Commit message: $commitMessage" + git commit -m $commitMessage + if ($LASTEXITCODE -ne 0) { + Write-Warning "'git commit' command failed with exit code $LASTEXITCODE." + } + else { + Write-Host "Commit created successfully." + } +} +else { + Write-Host "No relevant changes detected to commit." + Write-Host "Resetting staging area with 'git reset HEAD --quiet'." + git reset HEAD --quiet +} + +Write-Host "Updating remote 'origin' with 'git remote update origin --prune'..." +git remote update origin --prune +if ($LASTEXITCODE -ne 0) { + Write-Error "Unable to update remote 'origin'. Exiting script." + exit 1 +} + +$localCommit = (git rev-parse HEAD 2>$null).Trim() +if ($LASTEXITCODE -ne 0 -or -not $localCommit) { Write-Error "Failed to get local HEAD commit. Exiting script."; exit 1 } + +$remoteBranch = "origin/main" +$remoteCommit = (git rev-parse $remoteBranch 2>$null).Trim() +if ($LASTEXITCODE -ne 0 -or -not $remoteCommit) { Write-Error "Failed to get remote '$remoteBranch' commit. Exiting script."; exit 1 } + +Write-Host "Local HEAD commit: $localCommit" +Write-Host "Remote '$remoteBranch' commit: $remoteCommit" + +if ($localCommit -eq $remoteCommit) { + Write-Host "Local and remote are already in sync." + exit 0 +} + +$sleepyTime = Get-Random -Minimum 1 -Maximum 15 +Write-Host "Local and remote differ. Sleeping for $sleepyTime seconds..." +Start-Sleep -Seconds $sleepyTime + +$localCommitAfterSleep = (git rev-parse HEAD 2>$null).Trim() +if ($LASTEXITCODE -ne 0 -or -not $localCommitAfterSleep) { Write-Error "Failed to re-get local HEAD commit. Exiting script."; exit 1 } +$remoteCommitAfterSleep = (git rev-parse $remoteBranch 2>$null).Trim() +if ($LASTEXITCODE -ne 0 -or -not $remoteCommitAfterSleep) { Write-Error "Failed to re-get remote '$remoteBranch' commit. Exiting script."; exit 1 } + +if ($localCommitAfterSleep -eq $remoteCommitAfterSleep) { + Write-Host "Local and remote became synchronized during sleep." + exit 0 +} +$localCommit = $localCommitAfterSleep +$remoteCommit = $remoteCommitAfterSleep + +Write-Host "Proceeding with sync logic..." + +git merge-base --is-ancestor $localCommit $remoteCommit +$localIsAncestorOfRemote = ($LASTEXITCODE -eq 0) + +git merge-base --is-ancestor $remoteCommit $localCommit +$remoteIsAncestorOfLocal = ($LASTEXITCODE -eq 0) + +if ($localIsAncestorOfRemote) { + Write-Host "Remote '$remoteBranch' is ahead. Pulling with rebase..." + git pull --rebase origin main + if ($LASTEXITCODE -ne 0) { Write-Error "'git pull --rebase' failed. Manual intervention may be required."; exit 1 } +} +elseif ($remoteIsAncestorOfLocal) { + Write-Host "Local HEAD is ahead. Pushing..." + git push origin main + if ($LASTEXITCODE -ne 0) { Write-Error "'git push' failed. Manual intervention may be required."; exit 1 } +} +else { + Write-Host "Local HEAD and remote '$remoteBranch' have diverged." + $remoteTimestampStr = (git log --pretty=format:"%at" -n 1 $remoteBranch 2>$null).Trim() + if ($LASTEXITCODE -ne 0 -or -not $remoteTimestampStr) { Write-Error "Failed to get remote commit timestamp for '$remoteBranch'. Exiting script."; exit 1 } + $remoteTimestamp = [long]$remoteTimestampStr + + $localTimestampStr = (git log --pretty=format:"%at" -n 1 HEAD 2>$null).Trim() + if ($LASTEXITCODE -ne 0 -or -not $localTimestampStr) { Write-Error "Failed to get local commit timestamp for HEAD. Exiting script."; exit 1 } + $localTimestamp = [long]$localTimestampStr + + # It's good practice to check if conversion was successful, though [long] will error on failure. + + Write-Host "Local timestamp: $localTimestamp, Remote timestamp: $remoteTimestamp" + + if ($remoteTimestamp -gt $localTimestamp) { + Write-Host "Remote is newer. Pulling with rebase, strategy 'theirs'..." + git pull --rebase -X theirs origin main + } + else { + Write-Host "Local is newer or same age. Pulling with rebase, strategy 'ours'..." + git pull --rebase -X ours origin main + } + + if ($LASTEXITCODE -ne 0) { Write-Error "Rebase during divergence failed. Manual intervention may be required."; exit 1 } + + Write-Host "Pushing changes after rebase..." + git push origin main + if ($LASTEXITCODE -ne 0) { Write-Error "'git push' after rebase failed. Manual intervention may be required."; exit 1 } +} + +Write-Host "Synchronization process completed successfully." +exit 0