From 1439b682d6073c3039c72e8876f07d9d204a57f6 Mon Sep 17 00:00:00 2001 From: Ben Miller Date: Thu, 19 Jun 2025 01:06:29 -0600 Subject: [PATCH] foam_sync.ps1 (LIFEBALANCE) 2025-06-19T01:06:29Z --- .gitignore | 2 + foam_sync.ps1 | 445 ++++++++++++++++++++++++++------------------------ 2 files changed, 235 insertions(+), 212 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b02e5b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# .gitignore +.logs/ diff --git a/foam_sync.ps1 b/foam_sync.ps1 index ec28a13..1a07b6f 100644 --- a/foam_sync.ps1 +++ b/foam_sync.ps1 @@ -35,259 +35,280 @@ $frequencyMinutes = 2 # How often the Scheduled Task should attempt to run this script $executionTimeLimitBufferSeconds = 30 # Buffer: task stops if it runs longer than (frequency - buffer) -# --- 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 Interactive -# Define a very long duration (e.g., 40 years) -$practicallyIndefiniteDuration = New-TimeSpan -Days (365 * 40 + 10) # Approx 40 years, accounting for leap years -# Trigger configuration -$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $frequencyMinutes) -RepetitionDuration $practicallyIndefiniteDuration - -# Action configuration: -# We use an indirection technique to ensure the window is truly hidden. -# The scheduled task launches an initial PowerShell. -# This initial PowerShell then uses Start-Process to launch the *actual* script in a new, hidden PowerShell process. - -# 1. Innermost PowerShell's -File argument: Path to the script, with internal double quotes escaped as "" -# e.g., \"C:\path\to your script.ps1\" -$scriptPathForInnermostFileParam = $scriptPath.Replace('"', '""') -$innermostFileArg = "\`"$scriptPathForInnermostFileParam\`"" - -# 2. Argument string for the innermost PowerShell instance (the one executing the actual script) -# e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\" -$innermostPSArgs = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File $innermostFileArg" - -# 3. The innermostPSArgs string needs its single quotes escaped (as '') because it will be wrapped in single quotes for Start-Process's -ArgumentList -$innermostPSArgsEscapedForStartProcess = $innermostPSArgs.Replace("'", "''") - -# 4. Command string that the first PowerShell instance (launched by Task Scheduler) will execute using Start-Process. -# e.g., Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden -$commandToRunViaStartProcess = "Start-Process -FilePath powershell.exe -ArgumentList '$innermostPSArgsEscapedForStartProcess' -WindowStyle Hidden" - -# 5. Argument string FOR THE TASK SCHEDULER to pass to the first powershell.exe. -# The $commandToRunViaStartProcess is the value for -Command, and needs to be quoted for registration. -# e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden" -$actionArgumentForRegistration = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \`"$commandToRunViaStartProcess\`"" - -# 6. Expected argument string WHEN RETRIEVED by Get-ScheduledTask. -# Task Scheduler/PowerShell often strips the outermost quotes from the -Command value when retrieved. -$expectedRetrievedActionArgument = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $commandToRunViaStartProcess" - -$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgumentForRegistration - -# Task settings -# Calculate a realistic execution time limit based on the frequency -$executionTimeLimitTotalSeconds = ($frequencyMinutes * 60) - $executionTimeLimitBufferSeconds -if ($executionTimeLimitTotalSeconds -lt 30) { - # Ensure a minimum sensible execution time (e.g., 30 seconds) - $executionTimeLimitTotalSeconds = 30 +# --- Log File Setup --- +# Log inside the repository, in a .logs subfolder. Ensure this is in .gitignore +$scriptDirForLog = $PSScriptRoot # Use PSScriptRoot for robustness in determining script's dir +$logDir = Join-Path -Path $scriptDirForLog -ChildPath ".logs" +if (-not (Test-Path -Path $logDir)) { + New-Item -ItemType Directory -Path $logDir -Force | Out-Null } -$taskExecutionTimeLimit = New-TimeSpan -Seconds $executionTimeLimitTotalSeconds -$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit $taskExecutionTimeLimit +$logFilePath = Join-Path -Path $logDir -ChildPath "foam_sync.log" + +Start-Transcript -Path $logFilePath -Append -IncludeInvocationHeader -Force -# Check and configure the scheduled task try { - $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + # Wrap main script logic in try for Stop-Transcript in finally - if ($existingTask) { - Write-Host "Scheduled task '$taskName' already exists. Checking configuration..." - $currentTrigger = $existingTask.Triggers[0] - $currentAction = $existingTask.Actions[0] - # You could also compare $existingTask.Principal.UserId with $taskPrincipal.UserId - # and $existingTask.Description with $taskDescription if strict matching is needed for those. + # --- Script Setup --- + $scriptPath = $MyInvocation.MyCommand.Path + $scriptDir = Split-Path -Path $scriptPath -Parent # This is the Git repository root + $scriptName = (Get-Item $scriptPath).Name - # Check Trigger - $triggerMatches = $false - if ($currentTrigger -is [Microsoft.Management.Infrastructure.CimInstance] ` - -and $currentTrigger.RepetitionInterval.TotalMinutes -eq $frequencyMinutes ` - -and $currentTrigger.RepetitionDuration -eq $practicallyIndefiniteDuration) { - $triggerMatches = $true - } + Write-Host "Script: $scriptName at $scriptPath" + Write-Host "Repository directory: $scriptDir" + Write-Host "Sync frequency: Every $frequencyMinutes minutes" + Write-Host "Log file: $logFilePath" - # Check Action - $actionMatches = $false - if ($currentAction -is [Microsoft.Management.Infrastructure.CimInstance] ` - -and $currentAction.Execute -eq "powershell.exe" ` - -and $currentAction.Argument -eq $expectedRetrievedActionArgument) { - $actionMatches = $true - } + $taskDescription = "Periodically synchronizes the Git repository at $scriptDir using $scriptName." - # Check Principal (example, can be expanded) - $principalMatches = ($existingTask.Principal.UserId -eq $taskPrincipal.UserId) + # Run as the user who executes this script. + $taskPrincipal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive + # Define a very long duration (e.g., 40 years) + $practicallyIndefiniteDuration = New-TimeSpan -Days (365 * 40 + 10) # Approx 40 years, accounting for leap years + # Trigger configuration + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $frequencyMinutes) -RepetitionDuration $practicallyIndefiniteDuration - # Check Settings (specifically ExecutionTimeLimit for this change) - $settingsMatch = $false - if ($existingTask.Settings.ExecutionTimeLimit -eq $taskExecutionTimeLimit) { - $settingsMatch = $true - } + # Action configuration: + # We use an indirection technique to ensure the window is truly hidden. + # The scheduled task launches an initial PowerShell. + # This initial PowerShell then uses Start-Process to launch the *actual* script in a new, hidden PowerShell process. - if ($triggerMatches -and $actionMatches -and $principalMatches -and $settingsMatch) { - Write-Host "Scheduled task '$taskName' is already correctly configured." + # 1. Innermost PowerShell's -File argument: Path to the script, with internal double quotes escaped as "" + # e.g., \"C:\path\to your script.ps1\" + $scriptPathForInnermostFileParam = $scriptPath.Replace('"', '""') + $innermostFileArg = "\`"$scriptPathForInnermostFileParam\`"" + + # 2. Argument string for the innermost PowerShell instance (the one executing the actual script) + # e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\" + $innermostPSArgs = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File $innermostFileArg" + + # 3. The innermostPSArgs string needs its single quotes escaped (as '') because it will be wrapped in single quotes for Start-Process's -ArgumentList + $innermostPSArgsEscapedForStartProcess = $innermostPSArgs.Replace("'", "''") + + # 4. Command string that the first PowerShell instance (launched by Task Scheduler) will execute using Start-Process. + # e.g., Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden + $commandToRunViaStartProcess = "Start-Process -FilePath powershell.exe -ArgumentList '$innermostPSArgsEscapedForStartProcess' -WindowStyle Hidden" + + # 5. Argument string FOR THE TASK SCHEDULER to pass to the first powershell.exe. + # The $commandToRunViaStartProcess is the value for -Command, and needs to be quoted for registration. + # e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden" + $actionArgumentForRegistration = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \`"$commandToRunViaStartProcess\`"" + + # 6. Expected argument string WHEN RETRIEVED by Get-ScheduledTask. + # Task Scheduler/PowerShell often strips the outermost quotes from the -Command value when retrieved. + $expectedRetrievedActionArgument = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $commandToRunViaStartProcess" + + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgumentForRegistration + + # Task settings + # Calculate a realistic execution time limit based on the frequency + $executionTimeLimitTotalSeconds = ($frequencyMinutes * 60) - $executionTimeLimitBufferSeconds + if ($executionTimeLimitTotalSeconds -lt 30) { + # Ensure a minimum sensible execution time (e.g., 30 seconds) + $executionTimeLimitTotalSeconds = 30 + } + $taskExecutionTimeLimit = New-TimeSpan -Seconds $executionTimeLimitTotalSeconds + $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit $taskExecutionTimeLimit + + # Check and configure the scheduled task + try { + $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + + if ($existingTask) { + Write-Host "Scheduled task '$taskName' already exists. Checking configuration..." + $currentTrigger = $existingTask.Triggers[0] + $currentAction = $existingTask.Actions[0] + # You could also compare $existingTask.Principal.UserId with $taskPrincipal.UserId + # and $existingTask.Description with $taskDescription if strict matching is needed for those. + + # Check Trigger + $triggerMatches = $false + if ($currentTrigger -is [Microsoft.Management.Infrastructure.CimInstance] ` + -and $currentTrigger.RepetitionInterval.TotalMinutes -eq $frequencyMinutes ` + -and $currentTrigger.RepetitionDuration -eq $practicallyIndefiniteDuration) { + $triggerMatches = $true + } + + # Check Action + $actionMatches = $false + if ($currentAction -is [Microsoft.Management.Infrastructure.CimInstance] ` + -and $currentAction.Execute -eq "powershell.exe" ` + -and $currentAction.Argument -eq $expectedRetrievedActionArgument) { + $actionMatches = $true + } + + # Check Principal (example, can be expanded) + $principalMatches = ($existingTask.Principal.UserId -eq $taskPrincipal.UserId) + + # Check Settings (specifically ExecutionTimeLimit for this change) + $settingsMatch = $false + if ($existingTask.Settings.ExecutionTimeLimit -eq $taskExecutionTimeLimit) { + $settingsMatch = $true + } + + if ($triggerMatches -and $actionMatches -and $principalMatches -and $settingsMatch) { + Write-Host "Scheduled task '$taskName' is already correctly configured." + } + else { + Write-Host "Scheduled task '$taskName' configuration differs. Attempting to update in-place..." + try { + Set-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Principal $taskPrincipal -ErrorAction Stop + Write-Host "Scheduled task '$taskName' updated successfully." + } + catch { + Write-Warning "Failed to update scheduled task '$taskName' in-place. Error: $($_.Exception.Message)" + Write-Warning "The task remains in its previous state. Manual intervention may be required or re-run with Administrator privileges." + # We intentionally DO NOT unregister here to avoid the scenario you described. + } + } } else { - Write-Host "Scheduled task '$taskName' configuration differs. Attempting to update in-place..." + Write-Host "Creating scheduled task '$taskName'..." try { - Set-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Principal $taskPrincipal -ErrorAction Stop - Write-Host "Scheduled task '$taskName' updated successfully." + Register-ScheduledTask -TaskName $taskName -Description $taskDescription -Principal $taskPrincipal -Trigger $trigger -Action $action -Settings $settings -ErrorAction Stop + Write-Host "Scheduled task '$taskName' created successfully." } catch { - Write-Warning "Failed to update scheduled task '$taskName' in-place. Error: $($_.Exception.Message)" - Write-Warning "The task remains in its previous state. Manual intervention may be required or re-run with Administrator privileges." - # We intentionally DO NOT unregister here to avoid the scenario you described. + Write-Warning "Failed to create scheduled task '$taskName'. Error: $($_.Exception.Message)" + Write-Warning "You may need to run this script as Administrator." } } } - else { - Write-Host "Creating scheduled task '$taskName'..." - try { - Register-ScheduledTask -TaskName $taskName -Description $taskDescription -Principal $taskPrincipal -Trigger $trigger -Action $action -Settings $settings -ErrorAction Stop - Write-Host "Scheduled task '$taskName' created successfully." - } - catch { - Write-Warning "Failed to create scheduled task '$taskName'. Error: $($_.Exception.Message)" - Write-Warning "You may need to run this script as Administrator." - } + catch { + # This outer catch is for unexpected errors, e.g., if Get-ScheduledTask had -ErrorAction Stop + Write-Warning "An unexpected error occurred during scheduled task setup for '$taskName'. Error: $($_.Exception.Message)" } -} -catch { - # This outer catch is for unexpected errors, e.g., if Get-ScheduledTask had -ErrorAction Stop - Write-Warning "An unexpected error occurred during scheduled task setup for '$taskName'. Error: $($_.Exception.Message)" -} -# --- 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 -} + # --- 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 + Write-Host "Staging all changes with 'git add .'" + git add . if ($LASTEXITCODE -ne 0) { - Write-Warning "'git commit' command failed with exit code $LASTEXITCODE." + 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 "Commit created successfully." + Write-Host "No relevant changes detected to commit." + Write-Host "Resetting staging area with 'git reset HEAD --quiet'." + git reset HEAD --quiet } -} -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 -} + 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 } + $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 } + $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" + 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 -} + 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 + $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 } + $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 + 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..." + Write-Host "Proceeding with sync logic..." -git merge-base --is-ancestor $localCommit $remoteCommit -$localIsAncestorOfRemote = ($LASTEXITCODE -eq 0) + git merge-base --is-ancestor $localCommit $remoteCommit + $localIsAncestorOfRemote = ($LASTEXITCODE -eq 0) -git merge-base --is-ancestor $remoteCommit $localCommit -$remoteIsAncestorOfLocal = ($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 + 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 is newer or same age. Pulling with rebase, strategy 'ours'..." - git pull --rebase -X ours origin main + 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 } } - 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 +} +finally { + Stop-Transcript } - -Write-Host "Synchronization process completed successfully." -exit 0