Functions/Sync-HgRepository.ps1


function Sync-HgRepository
{
    <#
    .SYNOPSIS
    Synchronizes the current Mercurial repository with its remote source repository.
 
    .DESCRIPTION
    The process of pushing your changes to a remote repository usually goes something like this:
 
        PS> hg commit -m "My change are done! Woot!"
        PS> hg push
        abort: push creates new remote head addbadbeddad
        PS> hg pull
        PS> hg merge
        PS> hg commit -m "Merge"
        PS> hg push
 
    And sometimes you have to repeat the pull/merge/commit/push cycle multiple times. This script combines all these operations. As long as a merge doesn't make any new, local modifications, the pull/merge/commit/push cycle will repeat until the push succeeds.
 
    The script attempts to push. If the push fails because there are incoming changes, it pulls those changes down, merges them, commits the merge (if there were no conflicts or merged files), then tries to push again. It repeats until the push succeeds or fails and there are no incoming changes.
 
    If the merge results in any conflicts or merged files, the script stops so you can resolve the conflicts and review the merged files. Once you commit the merge, you can re-run Sync-HgRepository.ps1.
 
    If a pull brings in multiple heads in another branch, the script will stop and you�ll have to go to that branch, merge those heads, then re-run Sync-HgRepository.ps1.
 
    Returns the output of the the Mercurial push commands it runs. If the synchronization succeeds, sets the `$PsHg?` variable to `$true`. Otherwise, it sets it to `$false`.
 
    ALIASES
      syhg, hgsync
 
    .OUTPUTS
    PsHg.SyncResult
     
    .EXAMPLE
    PS> Sync-HgRepository
 
    The local repository in the current directory will be synchronized with its remote repository.
 
    .EXAMPLE
    PS> Sync-HgRepository -RepoRoot C:\Projects\PsHg
 
    The repository at `C:\Projects\PsHg` will be synchronized with its remote repository.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Switch]
        # The push is creating a new branch. Adds the `--new-branch` argument to the `hg push` command.
        $NewBranch,

        [string]
        # The bookmark to synchronize. Only this boookmark and changesets before it are pushed.
        $Bookmark,

        [Switch]
        # Forces the push, even if it will create multiple heads. You usually don't want to do this. Adds the `-f` argument to the `hg push` command.
        $Force,
        
        [Switch]
        # The synchronization is running in non-interactive mode. When set, ensures Mercurial doesn't prompt for anything. If Mercurial does need to prompt, it will fail instead.
        $NonInteractive,

        [string]
        # Specifies an explicit repository to push to.
        $Destination,
        
        [string]
        # The path to the repository that should by synced. Defaults to the current directory.
        $RepoRoot = (Resolve-Path .)
    )
    
    $rRepoRoot = Resolve-HgRoot -Path $RepoRoot

    if( -not $rRepoRoot )
    {
        return
    }
    
    Write-Verbose ('Working in {0}.' -f $rRepoRoot)

    Push-Location $rRepoRoot

    try
    {
        $currentBranch = Get-HgBranch -Current

        $revision = $currentBranch.Name
        if( $Bookmark )
        {
            $revision = $Bookmark
        }
        
        $sendArgs = @{ }
        $receiveArgs = @{ }

        if( $NewBranch )
        {
            $sendArgs.NewBranch = $true
        }
        
        if ( $Bookmark )
        {
            $receiveArgs.Bookmark = $Bookmark
            $sendArgs.Bookmark = $Bookmark
        }

        if ( $Destination )
        {
            $sendArgs.Destination = $Destination
            $receiveArgs.Source = $Destination
        }

        $forceArg = ''
        if ( $Force )
        {
            $forceArg = '-f'
        }

        do
        {
            if( -not $Bookmark -and (Test-HgHead -Revision $currentBranch.Name) -and -not $Force )
            {
                Write-Error ('Unable to push: found multiple local heads. Run `hg heads .` to view, `hg merge` to merge.')
                break
            }
            
            Write-Verbose ('Pushing changesets')
            $sendArgs | Format-Table | Out-String | Write-Verbose
            $errCount = $Global:Error.Count
            Send-HgChangeset @sendArgs -ErrorAction SilentlyContinue -NonInteractive:$NonInteractive

            if( $PsHg? )
            {
                break
            }
            
            if( -not (Test-HgIncomingChangeset -Revision $revision -Source $Destination) )
            {
                $currentErrCount = $Global:Error.Count
                if( $currentErrCount -gt $errCount )
                {
                    $errorsToReport = New-Object 'Collections.ArrayList'
                    for( $idx = ($currentErrCount - $errCount - 1); $idx -ge 0; --$idx )
                    {
                        [void]$errorsToReport.Add( $Global:Error[$idx] )
                        $Global:Error.RemoveAt($idx)
                    }

                    foreach( $err in $errorsToReport )
                    {
                        Write-Error -ErrorRecord $err
                    }
                }
                else
                {
                    Write-Error ('Unable to push: an unknown error occurred. LastExitCode: {0}' -f $LastExitCode)
                }
                break
            }
            else
            {
                # Remove the push errors, because we're handling them.
                while( $Global:Error.Count -gt $errCount )
                {
                    $Global:Error.RemoveAt( 0 )
                }
            }
            
            $incoming = Get-HgIncomingChangeset -Revision $revision -Source $Destination |
                            Select-Object -Last 1
            
            Write-Verbose ('Incoming changes. Updating to revision {0}.' -f $incoming.Node)
            $receiveArgs.Revision = $incoming.Node

            Receive-HgChangeset @receiveArgs
            if( -not $PsHg? )
            {
                Write-Verbose ('Pull failed.')
                break
            }
            
            Write-Verbose ('Merging with revision {0}.' -f $incoming.Node)
            Merge-HgChangeset -Revision $incoming.Node -NonInteractive:$NonInteractive
            
            $mergeResult = Get-HgMergeResult
            if( -not $mergeResult )
            {
                $Script:PsHg? = $false
                break
            }

            if( $mergeResult.FilesMerged -gt 0 )
            {
                Write-Warning ('{0} files merged. Please review the merge, then commit and re-run synchronization.' -f $mergeResult.FilesMerged)
                $Script:PsHg? = $false
                break
            }

            if( $mergeResult.FilesUnresolved -gt 0 )
            {
                Write-Error ('{0} files unresolved.' -f $mergeResult.FilesUnresolved)
                $Script:PsHg? = $false
                break
            }

            if( -not $Destination )
            {
                $Destination = @( 'default-push', 'default' ) | ForEach-Object { hg showconfig ('paths.{0}' -f $_) } | Select-Object -First 1
            }
            
            if( $Destination )
            {
                $destinationMsg = ' with {0}' -f $Destination
            }

            $merge = Save-HgChangeset -Message ('Automated merge{0} by {1}\{2} on {3}.' -f $destinationMsg,$env:userdomain,$env:username,$env:computername)
            if( -not $merge )
            {
                Write-Verbose ('Saving merge failed.')
                break
            }
            $merge
        }
        while( $true )
        
    }
    finally
    {
        Pop-Location
    }   
}

Set-Alias -Name 'hgsync' -Value 'Sync-HgRepository'
Set-Alias -Name 'syhg' -Value 'Sync-HgRepository'