Files
homefoam/foam_sync.ps1
2025-06-18 23:55:52 -06:00

237 lines
10 KiB
PowerShell

#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