Functions/Copy-GitRepository.ps1

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

using namespace System.Management.Automation

function Copy-GitRepository {
    <#
    .SYNOPSIS
    Clones a Git repository.

    .DESCRIPTION
    The `Copy-GitRepository` function clones a Git repository from the URL specified by `Uri` to the path specified by `DestinationPath` and checks out the `master` branch. If the repository requires authentication, pass the username/password via the `Credential` parameter.

    To clone a local repository, pass a file system path to the `Uri` parameter.

    .EXAMPLE
    Copy-GitRepository -Uri 'https://github.com/webmd-health-services/PowerGit' -DestinationPath PowerGit
    #>

    param(
        # The URI or path to the source repository to clone.
        [Parameter(Mandatory, Position = 0)]
        [string] $Source,

        # The directory where the repository should be cloned to. Must not exist or be empty.
        [string] $DestinationPath,

        # The credentials to use to connect to the source repository.
        [pscredential] $Credential,

        # Returns a `System.IO.DirectoryInfo` object for the new copy's `.git` directory.
        [Switch] $PassThru
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $Source = ConvertTo-GitFullPath -Uri $Source
    if (-not $DestinationPath) {
        $DestinationPath = Join-Path $PWD (Split-Path $Source -LeafBase)
    }
    $DestinationPath = ConvertTo-GitFullPath -Path $DestinationPath

    $options = [libgit2sharp.CloneOptions]::new()
    $credentialsProviderCalled = $false
    $options.CredentialsProvider = {
        param([string]$Url, [string]$UsernameForUrl, [LibGit2Sharp.SupportedCredentialTypes]$Types)
        Write-Verbose "Credentials required"
        if ($credentialsProviderCalled) {
            $Credential = Get-Credential -Title "Wrong credentials provided for $Url"
        }
        Set-Variable -Name credentialsProviderCalled -Value $true -Scope 1
        if (-not $Credential) {
            $Credential = Get-Credential -Title "Authentication required for $Url"
        }
        $gitCredential = [LibGit2Sharp.SecureUsernamePasswordCredentials]::new()
        $gitCredential.Username = $Credential.UserName
        $gitCredential.Password = $Credential.Password
        return $gitCredential
    }

    $cancel = $false
    $options.OnProgress = {
        param([string] $serverProgressOutput)
        try {
            if ($ProgressPreference -ne 'SilentlyContinue') {
                if ($serverProgressOutput -match '^(.+):\s+(\d+)% \((\d+/\d+)\)') {
                    # Compressing objects: 0% (1/123)
                    # Counting objects: 3% (11/550)
                    if ($ProgressPreference -ne 'SilentlyContinue') {
                        Write-Progress `
                            -Activity $Matches[1] `
                            -PercentComplete $Matches[2] `
                            -Status $Matches[3]
                    }
                } elseif ($serverProgressOutput -match '^(.+)(?::)?\s+(\d+)') {
                    # Enumerating objects: 576, done.
                    # Counting objects 4
                    if ($ProgressPreference -ne 'SilentlyContinue') {
                        Write-Progress `
                            -Activity $Matches[1] `
                            -PercentComplete -1 `
                            -Status $Matches[2]
                    }
                } elseif (-not [string]::IsNullOrWhiteSpace($serverProgressOutput)) {
                    Write-Information $serverProgressOutput
                }
            }
            Write-Verbose "OnProgress returning $(-not $cancel -and -not $PSCmdlet.Stopping)"
            return -not $cancel -and -not $PSCmdlet.Stopping
        } catch [PipelineStoppedException] {
            return $false
        }
    }

    $lastUpdated = Get-Date
    $options.OnTransferProgress = {
        param([LibGit2Sharp.TransferProgress] $TransferProgress)
        try {

            # Only update progress every 1/10th of a second, otherwise updating the progress takes longer than the clone
            if (((Get-Date) - $lastUpdated).TotalMilliseconds -lt 100) {
                return $true
            }

            if ($ProgressPreference -ne 'SilentlyContinue' -and $TransferProgress.TotalObjects -ne 0) {
                $numBytes = $TransferProgress.ReceivedBytes
                if ($numBytes -lt 1kb) {
                    $unit = 'B'
                } elseif ($numBytes -lt 1mb) {
                    $unit = 'KB'
                    $numBytes = $numBytes / 1kb
                } elseif ($numBytes -lt 1gb) {
                    $unit = 'MB'
                    $numBytes = $numBytes / 1mb
                } elseif ($numBytes -lt 1tb) {
                    $unit = 'GB'
                    $numBytes = $numBytes / 1gb
                } elseif ($numBytes -lt 1pb) {
                    $unit = 'TB'
                    $numBytes = $numBytes / 1tb
                } else {
                    $unit = 'PB'
                    $numBytes = $numBytes / 1pb
                }

                Write-Progress -Activity ('Cloning {0} -> {1}' -f $Source, $DestinationPath) `
                    -Status ('{0}/{1} objects, {2:n0} {3}' -f $TransferProgress.ReceivedObjects, $TransferProgress.TotalObjects, $numBytes, $unit) `
                    -PercentComplete (($TransferProgress.ReceivedObjects / $TransferProgress.TotalObjects) * 100)
                Set-Variable -Name 'lastUpdated' -Value (Get-Date) -Scope 1
                Write-Verbose "OnTransferProgress returning $(-not $cancel -and -not $PSCmdlet.Stopping)"
            }
            return -not $cancel -and -not $PSCmdlet.Stopping
        } catch [PipelineStoppedException] {
            return $false
        }
    }

    try {
        Write-Verbose "Cloning $Source to $DestinationPath"
        $gitPath = [LibGit2Sharp.Repository]::Clone($Source, $DestinationPath, $options)
        if ($PassThru -and $gitPath) {
            Get-Item -Path $gitPath -Force
        }
    } finally {
        $cancel = $true
    }
}