GitAutomation.psm1

# 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.

$registeredSsh = $false
$gitCmd = Get-Command -Name 'git.exe' -ErrorAction Ignore
if( $gitCmd )
{
    $sshExePath = Split-Path -Path $gitCmd.Path -Parent
    $sshExePath = Join-Path -Path $sshExePath -ChildPath '..\usr\bin\ssh.exe' -Resolve -ErrorAction Ignore
    if( $sshExePath )
    {
       [Git.Automation.SshExeTransport]::Unregister()
       [Git.Automation.SshExeTransport]::Register($sshExePath)
       $registeredSsh = $true
    }
}

if( -not $registeredSsh )
{
    Write-Warning -Message 'SSH support is disabled. To enable SSH, please install Git for Windows. GitAutomation uses the version of SSH that ships with Git for Windows.'
}

$moduleRoot = $PSScriptRoot
$moduleBinRoot = Join-Path -Path $moduleRoot -ChildPath 'bin'

$oldLibGit2Sharp = 
    [AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.FullName -like 'LibGit2Sharp*' } |
    ForEach-Object { $_.GetName() } |
    Where-Object { $_.Name -eq 'LibGit2Sharp' -and $_.Version -lt [version]'0.26.0' }
if( $oldLibGit2Sharp )
{
    Write-Error -Message ('Unable to load GitAutomation: an older version has already been loaded. Please restart your PowerShell session.') -ErrorAction Stop
}


Join-Path -Path $PSScriptRoot -ChildPath 'Functions' |
    Where-Object { Test-Path -Path $_ -PathType Container } |
    Get-ChildItem -Filter '*.ps1' |
    ForEach-Object { . $_.FullName }



function Add-GitItem
{
    <#
    .SYNOPSIS
    Promotes changes to the Git staging area so they can be saved during the next commit.
 
    .DESCRIPTION
    The `Add-GitItem` function promotes new/untracked and modified files to the Git staging area. When committing changes, by default Git only commits changes that have been staged. Use this function on each file you want to commit before committing. No other files will be committed.
 
    Use the `PassThru` switch to get `IO.FileInfo` and `IO.DirectoryInfo` objects back for each file and directory added, respectively. If the items are already added or were unmodified and not added to the staging area, you'll still get objects back for them.
 
    This function implements the `git add` command.
 
    .LINK
    Save-GitCommit
 
    .EXAMPLE
    Add-GitItem -Path 'C:\Projects\GitAutomation'
 
    Demonstrates how to add all the items under a directory to the next commit to the repository in the current directory.
 
    .EXAMPLE
    Add-GitItem -Path 'C:\Projects\GitAutomation\Functions\Add-GitItem.ps1','C:\Projects\GitAutomation\Tests\Add-GitItem.Tests.ps1'
 
    Demonstrates how to add multiple items and files to the next commit.
 
    .EXAMPLE
    Get-ChildItem '.\GitAutomation\Functions','.\Tests' | Add-GitItem
 
    Demonstrates that you can pipe paths or file system objects to Add-GitItem. When passing directories, all untracked/new or modified files under that directory are added. When passing files, only that file is added.
 
    .EXAMPLE
    Add-GitItem -Path 'C:\Projects\GitAutomation' -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to operate on a repository that isn't the current directory.
 
    .EXAMPLE
    Get-ChildItem | Add-GitItem
 
    Demonstrates that you can pipe `IO.FileInfo` and `IO.DirectoryInfo` objects to `Add-GitItem`. Plain strings are also allowed.
 
    .EXAMPLE
    Add-GitItem -Path 'file1','directory1' -PassThru
 
    Demonstrates how to get `IO.FileInfo` and `IO.DirectoryInfo` objects returned for each file and directory, respectively.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [Alias('FullName')]
        [string[]]
        # The paths to the files/directories to add to the next commit.
        $Path,

        [string]
        # The path to the repository where the files should be added. The default is the current directory as returned by Get-Location.
        $RepoRoot = (Get-Location).ProviderPath,

        [Switch]
        # Return `IO.FileInfo` and/or `IO.DirectoryInfo` objects for each file and/or directory added, respectively.
        $PassThru
    )

    begin
    {
        Set-StrictMode -Version 'Latest'

        $repo = Find-GitRepository -Path $RepoRoot -Verify
    }

    process
    {
        if( -not ((Test-Path -Path 'variable:repo') -and $repo) )
        {
            return
        }

        foreach( $pathItem in $Path )
        {
            if( -not [IO.Path]::IsPathRooted($pathItem) )
            {
                $pathItem = Join-Path -Path $repo.Info.WorkingDirectory -ChildPath $pathItem -Resolve
                if( -not $pathItem )
                {
                    continue
                }
            }

            if( -not $pathItem.StartsWith($repo.Info.WorkingDirectory, [stringcomparison]::InvariantCultureIgnoreCase) )
            {
                Write-Error -Message ('Item ''{0}'' can''t be added because it is not in the repository ''{1}''.' -f $pathItem,$repo.Info.WorkingDirectory)
                continue
            }

            if( -not (Test-Path -Path $pathItem) )
            {
                Write-Error -Message ('Cannot find path ''{0}'' because it does not exist.' -f $pathItem)
                continue
            }

            [LibGit2Sharp.Commands]::Stage($repo, $pathItem, $null)

            if( $PassThru )
            {
                Get-Item -Path $pathItem
            }
        }
    }

    end
    {
        if( ((Test-Path -Path 'variable:repo') -and $repo) )
        {
            $repo.Dispose()
        }
    }
}



function Compare-GitTree
{
    <#
    .SYNOPSIS
    Gets a diff of the file tree changes in a repository between two commits.
 
    .DESCRIPTION
    The `Compare-GitTree` function returns a `LibGit2Sharp.TreeChanges` object representing the file tree changes in a repository between two commits. The tree changes are the names of the files that have been added, removed, modified, and renamed in a git repository.
 
    Pass the name of commits to diff, such as commit hash, branch name, or tag name, to the `ReferenceCommit` and `DifferenceCommit` parameters.
 
    You must specify a commit reference name for the `ReferenceCommit` parameter. The `DifferenceCommit` parameter is optional and defaults to `HEAD`.
 
    This function implements the `git diff --name-only` command.
 
    .EXAMPLE
    Compare-GitTree -ReferenceCommit 'HEAD^'
 
    Demonstrates how to get the diff between the default `HEAD` commit and its parent commit referenced by `HEAD^`.
 
    .EXAMPLE
    Compare-GitTree -RepoRoot 'C:\build\repo' -ReferenceCommit 'tags/1.0' -DifferenceCommit 'tags/2.0'
 
    Demonstrates how to get the diff between the commit tagged with `2.0` and the older commit tagged with `1.0` in the repository located at `C:\build\repo`.
    #>

    [CmdletBinding(DefaultParameterSetName='RepositoryRoot')]
    [OutputType([LibGit2Sharp.TreeChanges])]
    param(
        [Parameter(ParameterSetName='RepositoryRoot')]
        [Alias('RepoRoot')]
        [string]
        # The root path to the repository. Defaults to the current directory.
        $RepositoryRoot = (Get-Location).ProviderPath,

        [Parameter(Mandatory=$true, ParameterSetName='RepositoryObject')]
        [LibGit2Sharp.Repository]
        $RepositoryObject,

        [Parameter(Mandatory=$true)]
        [string]
        # A commit to compare `DifferenceCommit` against, e.g. commit hash, branch name, tag name.
        $ReferenceCommit,

        [string]
        # A commit to compare `ReferenceCommit` against, e.g. commit hash, branch name, tag name. Defaults to `HEAD`.
        $DifferenceCommit = 'HEAD'
    )

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

    if ($RepositoryObject)
    {
        $repo = $RepositoryObject
    }
    else
    {
        $repo = Find-GitRepository -Path $RepositoryRoot -Verify
        if (-not $repo)
        {
            Write-Error -Message ('Unable to get diff between ''{0}'' and ''{1}''. See previous errors for more details.' -f $ReferenceCommit, $DifferenceCommit)
            return
        }
    }

    try
    {
        $oldCommit = $repo.Lookup($ReferenceCommit)
        $newCommit = $repo.Lookup($DifferenceCommit)

        if (-not $oldCommit)
        {
            Write-Error -Message ('Commit ''{0}'' not found in repository ''{1}''.' -f $ReferenceCommit, $repo.Info.WorkingDirectory)
            return
        }
        elseif (-not $newCommit)
        {
            Write-Error -Message ('Commit ''{0}'' not found in repository ''{1}''.' -f $DifferenceCommit, $repo.Info.WorkingDirectory)
            return
        }

        return , [Git.Automation.Diff]::GetTreeChanges($repo, $oldCommit, $newCommit)
    }
    finally
    {
        if (-not $RepositoryObject)
        {
            Invoke-Command -NoNewScope -ScriptBlock {
                $repo.Dispose()
            }
        }
    }
}



function ConvertTo-GitFullPath
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='Path')]
        [string]
        # A path to convert to a full path.
        $Path,

        [Parameter(Mandatory=$true,ParameterSetName='Uri')]
        [uri]
        # A URI to convert to a full path. It can be a local path.
        $Uri
    )

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

    if( $PSCmdlet.ParameterSetName -eq 'Uri' )
    {
        if( $Uri.Scheme )
        {
            return $Uri.AbsoluteUri
        }

        $Path = $Uri.ToString()
    }

    if( [IO.Path]::IsPathRooted($Path) )
    {
        return $Path
    }

    $Path = Join-Path -Path (Get-Location) -ChildPath $Path
    [IO.Path]::GetFullPath($Path)
}


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/GitAutomation' -DestinationPath GitAutomation
    #>

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

        [Parameter(Mandatory=$true)]
        [string]
        # The directory where the repository should be cloned to. Must not exist or be empty.
        $DestinationPath,

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

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

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

    $Source = ConvertTo-GitFullPath -Uri $Source
    $DestinationPath = ConvertTo-GitFullPath -Path $DestinationPath

    $options = New-Object 'libgit2sharp.CloneOptions'
    if( $Credential )
    {
        $gitCredential = New-Object 'LibGit2Sharp.SecureUsernamePasswordCredentials'
        $gitCredential.Username = $Credential.UserName
        $gitCredential.Password = $Credential.Password
        $options.CredentialsProvider = { return $gitCredential }
    }

    $cancelClone = $false
    $options.OnProgress = { 
        param(
            $Output
        )

        Write-Verbose -Message $Output
        return -not $cancelClone
    }

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

        # 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
        }
         
        $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
        return (-not $cancelClone)
    }

    try
    {
        $cloneCompleted = $false
        $gitPath = [LibGit2Sharp.Repository]::Clone($Source, $DestinationPath, $options)
        if( $PassThru -and $gitPath )
        {
            Get-Item -Path $gitPath -Force
        }
        $cloneCompleted = $true
    }
    finally
    {
        if( -not $cloneCompleted )
        {
            $cancelClone = $true
        }
    }
}


function Find-GitRepository
{
    <#
    .SYNOPSIS
    Searches a directory and its parents for a Git repository.
 
    .DESCRIPTION
    The `Find-GitRepository` function searches a directory for a Git repository and returns a `LibGit2Sharp.Repository` object representing that repository. If a repository isn't found, it looks up the directory tree until it finds one (i.e. it looks at the directories parent directory, then that directory's parent, then that directory's parent, etc. until it finds a repository or gets to the root directory. If it doesn't find one, nothing is returned.
 
    With no parameters, looks in the current directory and up its directory tree. If given a path with the `Path` parameter, it looks in that directory then up its directory tree.
 
    The repository object that is returned contains resources that don't get automatically removed from memory by .NET. To avoid memory leaks, you must call its `Dispose()` method when you're done using it.
 
    .OUTPUTS
    LibGit2Sharp.Repository.
 
    .EXAMPLE
    Find-GitRepository
 
    Demonstrates how to find the Git repository of the current directory.
 
    .EXAMPLE
    Find-GitRepository -Path 'C:\Projects\GitAutomation\GitAutomation\bin'
 
    Demonstrates how to find the Git repository that a specific directory is a part of. In this case, a `LibGit2Sharp.Repository` object is returned for the repository at `C:\Projects\GitAutomation`.
    #>

    [CmdletBinding()]
    [OutputType([LibGit2Sharp.Repository])]
    param(
        [string]
        # The path to start searching.
        $Path = (Get-Location).ProviderPath,

        [Switch]
        # Write an error if a repository isn't found. Usually, no error is written and nothing is returned when a repository isn't found.
        $Verify
    )

    Set-StrictMode -Version 'Latest'

    if( -not $Path )
    {
        $Path = (Get-Location).ProviderPath
    }

    $Path = Resolve-Path -Path $Path -ErrorAction Ignore | Select-Object -ExpandProperty 'ProviderPath'
    if( -not $Path )
    {
        Write-Error -Message ('Can''t find a repository in ''{0}'' because it does not exist.' -f $PSBoundParameters['Path']) -ErrorAction $ErrorActionPreference
        return
    }
    
    $startedAt = $Path

    while( $Path -and -not [LibGit2Sharp.Repository]::IsValid($Path) )
    {
        $Path = Split-Path -Parent -Path $Path
    }

    if( -not $Path )
    {
        if( $Verify )
        {
            Write-Error -Message ('Path ''{0}'' not in a Git repository.' -f $startedAt) -ErrorAction $ErrorActionPreference
        }
        return
    }

    return Get-GitRepository -RepoRoot $Path
}



function Get-GitBranch
{
   <#
   .SYNOPSIS
   Gets the branches in a Git repository.
     
   .DESCRIPTION
   The `Get-GitBranch` function returns a list of all the branches in a repository.
     
   Use the `Current` switch to return just the current branch.
 
   It defaults to the current repository. Use the `RepoRoot` parameter to specify an explicit path to another repo.
 
   .EXAMPLE
   Get-GitBranch -RepoRoot 'C:\Projects\GitAutomation' -Current
     
   Returns an object representing the current branch for the specified repo.
 
   .EXAMPLE
   Get-GitBranch
 
   Returns objects for all the branches in the current directory.
   #>

   [CmdletBinding()]
   [OutputType([Git.Automation.BranchInfo])]
    param(
        [string]
        # Specifies which git repository to check. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [Switch]
        # Get the current branch only. Otherwise all branches are returned.
        $Current
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        if( $Current )
        {
            New-Object Git.Automation.BranchInfo $repo.Head
            return
        }
        else
        {
            $repo.Branches | ForEach-Object { New-Object Git.Automation.BranchInfo $_ }
            return
        }

    }
    finally
    {
        $repo.Dispose()
    }
}

    
function Get-GitCommit
{
    <#
    .SYNOPSIS
    Gets the sha-1 ID for specific changes in a Git repository.
 
    .DESCRIPTION
    The `Get-GitCommit` gets all the commits in a repository, from most recent to oldest.
 
    To get a commit for a specific named revision, e.g. HEAD, a branch, a tag), pass the name to the `Revision` parameter.
 
    To get the commit of the current checkout, pass `HEAD` to the `Revision` parameter.
    #>

    [CmdletBinding(DefaultParameterSetName='All')]
    [OutputType([Git.Automation.CommitInfo])]
    param(
        [Parameter(ParameterSetName='All')]
        [switch]
        # Get all the commits in the repository.
        $All,

        [Parameter(Mandatory=$true,ParameterSetName='Lookup')]
        [string]
        # A named revision to get, e.g. `HEAD`, a branch name, tag name, etc.
        # To get the commit of the current checkout, pass `HEAD`.
        $Revision,

        [Parameter(ParameterSetName='CommitFilter')]
        [string]
        # The starting commit from which to generate a list of commits. Defaults to `HEAD`.
        $Since = 'HEAD',

        [Parameter(Mandatory=$true,ParameterSetName='CommitFilter')]
        [string]
        # The commit and its ancestors which will be excluded from the returned commit list which starts at `Since`.
        $Until,

        [Parameter(ParameterSetName='CommitFilter')]
        [switch]
        # Do not include any merge commits in the generated commit list.
        $NoMerges,

        [string]
        # The path to the repository. Defaults to the current directory.
        $RepoRoot
    )

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

    $repo = Find-GitRepository -Path $RepoRoot
    if( -not $repo )
    {
        return
    }
    
    try
    {
        if( $PSCmdlet.ParameterSetName -eq 'All' )
        {
            $filter = New-Object -TypeName 'LibGit2Sharp.CommitFilter'
            $filter.IncludeReachableFrom = $repo.Refs
            $repo.Commits.QueryBy($filter) | ForEach-Object { New-Object -TypeName 'Git.Automation.CommitInfo' -ArgumentList $_ }
            return
        }
        elseif( $PSCmdlet.ParameterSetName -eq 'Lookup' )
        {
            $change = $repo.Lookup($Revision)
            if( $change )
            {
                return New-Object -TypeName 'Git.Automation.CommitInfo' -ArgumentList $change
            }
            else
            {
                Write-Error -Message ('Commit ''{0}'' not found in repository ''{1}''.' -f $Revision,$repo.Info.WorkingDirectory) -ErrorAction $ErrorActionPreference
                return
            }
        }
        elseif( $PSCmdlet.ParameterSetName -eq 'CommitFilter')
        {
            $IncludeFromCommit = $repo.Lookup($Since)
            $ExcludeFromCommit = $repo.Lookup($Until)

            if (-not $IncludeFromCommit)
            {
                Write-Error -Message ('Commit ''{0}'' not found in repository ''{1}''.' -f $Since,$repo.Info.WorkingDirectory) -ErrorAction $ErrorActionPreference
                return
            }
            elseif (-not $ExcludeFromCommit)
            {
                Write-Error -Message ('Commit ''{0}'' not found in repository ''{1}''.' -f $Until,$repo.Info.WorkingDirectory) -ErrorAction $ErrorActionPreference
                return
            }
            elseif ($IncludeFromCommit.Sha -eq $ExcludeFromCommit.Sha)
            {
                Write-Error -Message ('Commit reference ''{0}'' and ''{1}'' refer to the same commit with hash ''{2}''.' -f $Since,$Until,$IncludeFromCommit.Sha)
                return
            }

            $CommitFilter = New-Object -TypeName LibGit2Sharp.CommitFilter
            $CommitFilter.IncludeReachableFrom = $IncludeFromCommit.Sha
            $CommitFilter.ExcludeReachableFrom = $ExcludeFromCommit.Sha

            $filteredCommits = $repo.Commits.QueryBy($CommitFilter)

            if ($NoMerges)
            {
                $filteredCommits = $filteredCommits | Where-Object { $_.Parents.Count -le 1 }
            }

            $filteredCommits | ForEach-Object { New-Object -TypeName 'Git.Automation.CommitInfo' -ArgumentList $_ }
        }
    }
    finally
    {
        $repo.Dispose()
    }
}



function Get-GitConfiguration
{
    <#
    .SYNOPSIS
    Gets Git configuration.
 
    .DESCRIPTION
    The `Get-GitConfiguration` function returns Git's configuration as `LibGit2Sharp.ConfigurationEntry` objects. When run outside a repository, it gets configuration from the user's and system's configuration files. When run from inside a repository, it also returns that repository's settings. The objects returned have the following properties:
 
    * `Key`: the key/name of the setting. This will be the section and name seperated by a dot, e.g. `user.name`.
    * `Value`: the setting's value.
    * `Level`: a `LibGit2Sharp.ConfigurationLevel` enumeration value indicating at what level/scope the setting is defined. `Local` means its set in a repository's `.git\config` file. `Global` means its set in the user's `.gitconfig` file. `Xdg` means it set in the user's `.config\git\config` file. `System` means its set in the global `gitconfig` file. `ProgramData` means it is defined in Git's system-wide `Git\config` file in Windows' `ProgramData` directory.
 
    You can explicitly set the repository whose settings to get with the `RepoRoot` parameter. Settings at higher levels will also be returned.
 
    You can read configuration from a specific file by passing the file's path to the `Path` parameter. When reading from a specific file, settings at higher levels (e.g. global and system) are also returned.
 
    To return a specific configuration setting, pass its name to the `Name` parameter. If a setting with that name doesn't exist, nothing is returned. Sections and setting names should be seperated by periods, e.g. `user.name`.
 
    .EXAMPLE
    Get-GitConfiguration
 
    Demonstrates how to get all Git configuration.
 
    .EXAMPLE
    Get-GitConfiguration -Path 'template.gitconfig'
 
    Demonstrates how to read configuration from a specific git config file. Settings at higher levels (global/user and system) are still returned.
 
    .EXAMPLE
    Get-GitConfiguration -Path 'template.gitconfig' | Where-Object { $_.Level -eq [LibGit2Sharp.ConfigurationLevel]::Local }
 
    Demonstrates how to read configuration from a specific git config file and filter out all settings that didn't come from that file.
 
    .EXAMPLE
    Get-GitConfiguration -Name 'user.email'
 
    Demonstrates how to get a specific setting.
    #>

    [CmdletBinding(DefaultParameterSetName='ByScope')]
    [OutputType([LibGit2Sharp.ConfigurationEntry[string]])]
    param(
        [Parameter(Position=0)]
        [string]
        # The name of the configuration variable to get. By default all configuration settings are returned. The name should be the section and name seperated by a dot, e.g. `user.name`.
        $Name,

        [Parameter(Mandatory=$true,ParameterSetName='ByPath')]
        [string]
        # The path to a specific file from which to read configuration. If this file doesn't exist, it is created.
        $Path,

        [Parameter(ParameterSetName='ByScope')]
        [string]
        # The path to the repository whose configuration variables to get. Defaults to the repository the current directory is in.
        $RepoRoot
    )

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

    if( $PSCmdlet.ParameterSetName -eq 'ByPath' )
    {
        if( -not (Test-Path -Path $Path -PathType Leaf) )
        {
            New-Item -Path $Path -ItemType 'File' -Force | Write-Verbose
        }

        $Path = Resolve-Path -Path $Path | Select-Object -ExpandProperty 'ProviderPath'

        $config = [LibGit2Sharp.Configuration]::BuildFrom($Path)
        try
        {
            if( -not $Name )
            {
                return $config
            }

            [Git.Automation.ConfigurationExtensions]::GetString( $config, $Name, 'Local' )
        }
        finally
        {
            $config.Dispose()
        }
        return
    }

    $pathParam = @{}
    if( $RepoRoot )
    {
        $pathParam['Path'] = $RepoRoot
    }
    
    $value = $null
        
    $repo = Find-GitRepository @pathParam -Verify -ErrorAction Ignore
    if( $repo )
    {
        try
        {
            if( -not $Name )
            {
                return $repo.Config
            }

            $value = [Git.Automation.ConfigurationExtensions]::GetString( $repo.Config, $Name )
        }
        finally
        {
            $repo.Dispose()
        }
    }
    
    if( -not $value )
    {
        $config = [LibGit2Sharp.Configuration]::BuildFrom([nullstring]::Value,[nullstring]::Value)
        try
        {
            if( -not $Name )
            {
                return $config
            }

            $value = [Git.Automation.ConfigurationExtensions]::GetString( $config, $Name )
        }
        finally
        {
            $config.Dispose()
        }    
    }

    return $value
}



function Get-GitRepository
{
    <#
    .SYNOPSIS
    Gets an object representing a Git repository.
 
    .DESCRIPTION
    The `Get-GitRepository` function gets a `LibGit2Sharp.Repository` object representing a Git repository. By default, it gets the current directory's repository. You can get an object for a specific repository using the `RepoRoot` parameter. If the `RepoRoot` path doesn't point to the root of a Git repository, or, if not using the `RepoRoot` parameter and the current directory isn't the root of a Git repository, you'll get an error.
 
    The repository object contains resources that don't get automatically removed from memory by .NET. To avoid memory leaks, you must call its `Dispose()` method when you're done using it.
 
    .EXAMPLE
    Get-GitRepository
 
    Demonstrates how to get a `LibGit2Sharp.Repository` object for the repository in the current directory.
 
    .EXAMPLE
    Get-GitRepository -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to get a `LibGit2Sharp.Repository` object for a specific repository.
    #>

    [CmdletBinding()]
    [OutputType([LibGit2Sharp.Repository])]
    param(
        [string]
        # The root to the repository to get. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath
    )

    Set-StrictMode -Version 'Latest'

    $RepoRoot = Resolve-Path -Path $RepoRoot -ErrorAction Ignore | Select-Object -ExpandProperty 'ProviderPath'
    if( -not $RepoRoot )
    {
        Write-Error -Message ('Repository ''{0}'' does not exist.' -f $PSBoundParameters['RepoRoot'])
        return
    }

    try
    {
        New-Object 'LibGit2Sharp.Repository' ($RepoRoot)
    }
    catch
    {
        Write-Error -ErrorRecord $_
    }
}


function Get-GitRepositoryStatus
{
    <#
    .SYNOPSIS
    Gets information about all added, untracked, and modified files in a repository.
 
    .DESCRIPTION
    The `Get-GitRepositoryStatus` commands gets information about which files in your working directory are new, untracked, or modified, including files that have been staged for the next commit. It gets information about each uncommitted change in your repository.
 
    Ignored items are not returned unless you provide the `IncludeIgnored` switch.
 
    You can get status for specific files and directories with the Path parameter. If you provide a `RepoRoot` parameter to work with a specific repository, the values of the `Path` parameter should be relative to the root of that repository. With no `RepoRoot` parameter, the paths in the `Path` parameter are treated as relative to the current directory. Wildcards are supported and are passed directly to Git to evaluate (i.e. use Git wildcard syntax not PowerShell's).
 
    The `LibGit2Sharp.StatusEntry` objects returned have several extended type data members added. You should use these members instead of using the object's `State` property.
 
     * `IsStaged`: `$true` if the item has been staged for the next commit; `$false` otherwise.
     * `IsAdded`: returns `$true` if the item is new in the working directory or has been staged for the next commit; `$false` otherwise.
     * `IsConflicted`: returns `$true` if the item was merged and currently has conflicts; `$false` otherwise.
     * `IsDeleted`: returns `$true` if the item was deleted from the working directory or has been staged for removal in the next commit; `$false` otherwise.
     * `IsIgnored`: returns `$true` if the item is ignored; `$false` otherwise. You'll only see ignored items if you use the `IncludeIgnored` switch.
     * `IsModified`: returns `$true` if the item is modified; `$false` otherwise.
     * `IsRenamed`: returns `$true` if the item was renamed; `$false` otherwise.
     * `IsTypeChanged`: returns `$true` if the item's type was changed; `$false` otherwise.
     * `IsUnchanged`: returns `$true` if the item is unchanged; `$false` otherwise.
     * `IsUnreadable`: returns `$true` if the item is unreadable; `$false` otherwise.
     * `IsUntracked`: returns `$true` if the item is untracked (i.e. hasn't been staged or added to the repository); `$false` otherwise.
 
    When displayed in a table (the default), the first column will show characters that indicate the state of each item, e.g.
 
        State FilePath
        ----- --------
         a GitAutomation\Formats\LibGit2Sharp.StatusEntry.ps1xml
         a GitAutomation\Functions\Get-GitRepositoryStatus.ps1
          m GitAutomation\GitAutomation.psd1
         a GitAutomation\Types\LibGit2Sharp.StatusEntry.types.ps1xml
         a Tests\Get-GitRepositoryStatus.Tests.ps1
 
    The state will display:
 
     * `i` if the item is ignored (i.e. `IsIgnored` returns `$true`)
     * `a` if the item is untracked or staged for the next commit (i.e. `IsAdded` returns `$true`)
     * `m` if the item was modified (i.e. `IsModified` returns `$true`)
     * `d` if the item was deleted (i.e. `IsDeleted` returns `$true`)
     * `r` if the item was renamed (i.e. `IsRenamed` returns `$true`)
     * `t` if the item's type was changed (i.e. `IsTypeChanged` returns `$true`)
     * `?` if the item can't be read (i.e. `IsUnreadable` returns `$true`)
     * `!` if the item was merged with conflicts (i.e. `IsConflicted` return `$true`)
 
    If no state characters are shown, the file is unchanged (i.e. `IsUnchanged` return `$true`).
 
    This function implements the `git status` command.
 
 
    .EXAMPLE
    Get-GitRepositoryStatus
 
    Demonstrates how to get the status of any uncommitted changes for the repository in the current directory.
 
    .EXAMPLE
    Get-GitRepositoryStatus -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to get the status of any uncommitted changes for the repository at a specific location.
 
    .EXAMPLE
    Get-GitRepositoryStatus -Path 'build.ps1','*.cs'
 
    Demonstrates how to get the status for specific files at or under the current directory using the Path parameter. In this case, only modified files named `build.ps1` or that match the wildcard `*.cs` under the current directory will be returned.
 
    .EXAMPLE
    Get-GitRepositoryStatus -Path 'build.ps1','*.cs' -RepoRoot 'C:\Projects\GitAutomation`
 
    Demonstrates how to get the status for specific files under the root of a specific repository. In this case, only modified files named `build.ps1` or that match the wildcard `*.cs` under `C:\Projects\GitAutomation` will be returned.
    #>

    [CmdletBinding()]
    [OutputType([LibGit2Sharp.StatusEntry])]
    param(
        [Parameter(Position=0)]
        [string[]]
        # The path to specific files and/or directories whose status to get. Git-style wildcards are supported.
        #
        # If no `RepoRoot` parameter is provided, these paths are evaluated as relative to the current directory. If a `RepoRoot` parameter is provided, these paths are evaluated as relative to the root of that repository.
        $Path,

        [Switch]
        # Return ignored files and directories. The default is to not return them.
        $IncludeIgnored,

        [string]
        # The path to the repository whose status to get.
        $RepoRoot = (Get-Location).ProviderPath
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        $statusOptions = New-Object -TypeName 'LibGit2Sharp.StatusOptions'

        if( $IncludeIgnored )
        {
            $statusOptions.RecurseIgnoredDirs = $true
        }

        $currentLocation = (Get-Location).ProviderPath
        if( -not $currentLocation.EndsWith([IO.Path]::DirectorySeparatorChar) )
        {
            $currentLocation = '{0}{1}' -f $currentLocation,[IO.Path]::DirectorySeparatorChar
        }

        if( -not $currentLocation.StartsWith($repo.Info.WorkingDirectory) )
        {
            Push-Location -Path $repo.Info.WorkingDirectory -StackName 'Get-GitRepositoryStatus'
        }

        $repoRootRegex = $repo.Info.WorkingDirectory.TrimEnd([IO.Path]::DirectorySeparatorChar)
        $repoRootRegex = [regex]::Escape($repoRootRegex)
        $repoRootRegex = '^{0}\\?' -f $repoRootRegex

        try
        {
            if( $Path )
            {
                $statusOptions.PathSpec = $Path | 
                                                ForEach-Object {
                                                
                                                    $pathItem = $_
                                                
                                                    if( [IO.Path]::IsPathRooted($_) )
                                                    {
                                                        return $_
                                                    }

                                                    $fullPath = Join-Path -Path (Get-Location).ProviderPath -ChildPath $_
                                                    try
                                                    {
                                                        return [IO.Path]::GetFullPath($fullPath)
                                                    }
                                                    catch
                                                    {
                                                        return $pathItem
                                                    }
                                                } |
                                                ForEach-Object { $_ -replace $repoRootRegex,'' } |
                                                ForEach-Object { $_ -replace ([regex]::Escape([IO.Path]::DirectorySeparatorChar)),'/' }
            }

            $repo.RetrieveStatus($statusOptions) |
                Where-Object { 
                    if( $IncludeIgnored )
                    {
                        return $true
                    }

                    return -not $_.IsIgnored
                }
        }
        finally
        {
            Pop-Location -StackName 'Get-GitRepositorySTatus' -ErrorAction Ignore
        }
    }
    finally
    {
        $repo.Dispose()
    }
}



function Get-GitTag{
    <#
    .SYNOPSIS
    Gets the tags in a Git repository.
 
    .DESCRIPTION
    The `Get-GitTag` function returns a list of all the tags in a Git repository.
 
    To get a specific tag, pass its name to the `Name` parameter. Wildcard characters are supported in the tag name. Only tags that match the wildcard pattern will be returned.
 
    .EXAMPLE
    Get-GitTag
 
    Demonstrates how to get all the tags in a repository.
 
    .EXAMPLE
    Get-GitTag -Name 'LastSuccessfulBuild'
 
    Demonstrates how to return a specific tag. If no tag matches, then `$null` is returned.
 
    .EXAMPLE
    Get-GitTag -Name '13.8.*' -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to return all tags that match a wildcard in the given repository.'
    #>



   [CmdletBinding()]
   [OutputType([Git.Automation.TagInfo])]
    param(
        [string]
        # Specifies which git repository to check. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [string]
        # The name of the tag to return. Wildcards accepted.
        $Name
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        $repo.Tags | ForEach-Object {
            New-Object Git.Automation.TagInfo $_
            } |
            Where-Object {
                if( $PSBoundParameters.ContainsKey('Name') )
                {
                    return $_.Name -like $Name
                }
                return $true
            }
    }
    finally
    {
        $repo.Dispose()
    }
}


function Merge-GitCommit
{
    <#
    .SYNOPSIS
    Merges a commit into the current branch.
 
    .DESCRIPTION
    The `Merge-GitCommit` function merges a commit into the current branch. The commit can be identified with its ID, by a tag name, or branch name. It returns a `Git.Automation.MergeResult` object, which has two properties:
 
    * `Status`: the status of the merge. It will be one of the following values:
      * `Conflicts`: when there are conflicts with the merge.
      * `FastForward`: when the merge resulted in a fast-forward.
      * `NonFastForward`: when a merge commit was created.
      * `UpToDate`: when nothing needed to be merged.
    * `Commit`: the merge commit (if one was created).
 
    If there are conflicts, the conflicts are left in place. You can use your preferred merge tool to resolve the conflicts and then commit. If this script is running non-interactively, you probably don't want any conflict markers hanging out in your local files. Use the "-NonInteractive" switch to prevent conflict files from remaining.
 
    By default, the function operates on the Git repository in the current directory. Use the `RepoRoot` parameter to target a different repository.
 
    .EXAMPLE
    Merge-GitCommit -Revision 'develop'
 
    Demonstrates how to merge a branch into the current branch.
    #>

    [CmdletBinding()]
    [OutputType([Git.Automation.MergeResult])]
    param(
        [string]
        # The path to the repository where the files should be added. The default is the current directory as returned by Get-Location.
        $RepoRoot = (Get-Location).ProviderPath,

        [Parameter(Mandatory=$true)]
        [string]
        # The revision to merge into the current commit (i.e. HEAD). A revision can be a specific commit ID/sha (short or long), branch name, tag name, etc. Run git help gitrevisions or go to https://git-scm.com/docs/gitrevisions for full documentation on Git's revision syntax.
        $Revision = "HEAD",

        [string]
        [ValidateSet('No','Only')]
        # Controls whether or not to do a fast-forward merge. By default, "Merge-GitCommit" will try to do a fast-forward merge if it can. If it can't it will create a new merge commit. Options are:
        #
        # * `No`: Don't do a fast-forward merge. Always create a merge commit.
        # * `Only`: Only do a fast-forward merge. No new merge commit is created.
        $FastForward,

        [Switch]
        # The merge is happening non-interactively. If there are any conflicts, the working directory will be left in the state it was in before the merge, i.e. there will be no conflict markers left in any files.
        $NonInteractive
    )

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

    $repo = Get-GitRepository -RepoRoot $RepoRoot
    if( -not $repo )
    {
        return
    }

    [LibGit2Sharp.MergeOptions]$mergeOptions = New-Object -TypeName 'LibGit2Sharp.MergeOptions'
    $mergeOptions.CommitOnSuccess = $true
    $mergeOptions.FailOnConflict = $false
    if( $NonInteractive )
    {
        $mergeOptions.FailOnConflict = $true
    }
    $mergeOptions.FindRenames = $true
    if( $FastForward -eq 'No' )
    {
        $mergeOptions.FastForwardStrategy = [LibGit2Sharp.FastForwardStrategy]::NoFastForward
    }
    elseif( $FastForward -eq 'Only' )
    {
        $mergeOptions.FastForwardStrategy = [LibGit2Sharp.FastForwardStrategy]::FastForwardOnly
    }

    $signature = $repo.Config.BuildSignature((Get-Date))
    try
    {
        $result = $repo.Merge($Revision,$signature,$mergeOptions)
        New-Object -TypeName 'Git.Automation.MergeResult' -ArgumentList $result
    }
    catch
    {
        Write-Error -Exception $_.Exception
    }
    finally
    {
        $repo.Dispose()
    }
}


function New-GitBranch
{
    <#
    .SYNOPSIS
 
    Creates a new branch in a Git repository.
 
    .DESCRIPTION
 
    The `New-GitBranch` creates a new branch in a Git repository and then switches to (i.e. checks out) that branch.
 
    It defaults to the current repository. Use the `RepoRoot` parameter to specify an explicit path to another repo.
 
    This function implements the `git branch <branchname> <startpoint>` and `git checkout <branchname>` commands.
 
    .EXAMPLE
 
    New-GitBranch -RepoRoot 'C:\Projects\GitAutomation' -Name 'develop'
 
    Demonstrates how to create a new branch named 'develop' in the specified repository.
 
    .EXAMPLE
 
    New-GitBranch -Name 'develop
 
    Demonstrates how to create a new branch named 'develop' in the current directory.
    #>

    [CmdletBinding()]
    param(
        [string]
        # Specifies which git repository to add a branch to. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the new branch.
        $Name,

        [string]
        # The revision where the branch should be started/created. A revision can be a specific commit ID/sha (short or long), branch name, tag name, etc. Run git help gitrevisions or go to https://git-scm.com/docs/gitrevisions for full documentation on Git's revision syntax.
        $Revision = "HEAD"
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        if( Test-GitBranch -RepoRoot $RepoRoot -Name $Name )
        {
            Write-Warning ('Branch {0} already exists in repository {1}' -f $Name, $RepoRoot)
            return
        }

        $newBranch = $repo.Branches.Add($Name, $Revision)
        $checkoutOptions = New-Object LibGit2Sharp.CheckoutOptions
        [LibGit2Sharp.Commands]::Checkout($repo, $newBranch, $checkoutOptions)
    }
    catch [LibGit2Sharp.LibGit2SharpException]
    {
        Write-Error ("Could not create branch '{0}' from invalid starting point: '{1}'" -f $Name, $Revision)
    }
    finally
    {
        $repo.Dispose()
    }
}


function New-GitRepository
{
    <#
    .SYNOPSIS
    Creates a new Git repository.
 
    .DESCRIPTION
    The `New-GitRepository` function creates a new Git repository. Set the `Path` parameter to the directory where the repository should be created. If the path does not exist, it is created and that directory becomes the new repository's root. If the path does exist, it also becomes the root of a new repository. If the path exists and it is already a repository, nothing happens.
 
    To create a bare repository (i.e. a repository that doesn't have a working directory) use the `Bare` switch.
 
    This function implements the `git init` command.
 
    .OUTPUTS
    Git.Automation.RepositoryInfo.
 
    .EXAMPLE
    New-GitRepository -Path 'C:\Projects\MyCoolNewRepo'
 
    Demonstrates how to create a new Git repository. In this case, a new repository is created in `C:\Projects\MyCoolNewRepo'.
 
    .EXAMPLE
    New-GitRepository -Path 'C:\Projects\MyCoolNewRepo' -Bare
 
    Demonstrates how to create a repository that doesn't have a working directory. Git calls these "Bare" repositories.
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    [OutputType([Git.Automation.RepositoryInfo])]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The path to the repository to create.
        $Path,

        [Switch]
        $Bare
    )

    Set-StrictMode -Version 'Latest'

    if( -not [IO.Path]::IsPathRooted($Path) )
    {
        $Path = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path
        $Path = [IO.Path]::GetFullPath($Path)
    }

    $whatIfMessage = 'create Git repository at ''{0}''' -f $Path
    if( -not $PSCmdlet.ShouldProcess($whatIfMessage, $whatIfMessage, 'New-GitRepository' ) )
    {
        return
    }

    $repoPath = [LibGit2Sharp.Repository]::Init($Path, $Bare.IsPresent)
    $repo = New-Object 'LibGit2Sharp.Repository' $repoPath
    try
    {
        return New-Object 'Git.Automation.RepositoryInfo' $repo.Info
    }
    finally
    {
        $repo.Dispose()
    }
}


function New-GitSignature
{
    <#
    .SYNOPSIS
    Creates an author signature object used to identify who created a commit.
 
    .DESCRIPTION
    The `New-GitSignature` object creates `LibGit2Sharp.Signature` objects. These objects are added when committing changes to identify the author of the commit and when the commit was made.
 
    With no parameters, this function reads author metadata from the "user.name" and "user.email" user level or system level configuration. If there is no user or system-level "user.name" or "user.email" setting, you'll get an error and nothing will be returned.
 
    To use explicit author information, pass the author's name and email address to the "Name" and "EmailAddress" parameters.
 
    .EXAMPLE
    New-GitSignature
 
    Demonstrates how to get create a Git author signature from the current user's user-level and system-level Git configuration files.
 
    .EXAMPLE
    New-GitSignature -Name 'Jock Nealy' -EmailAddress 'email@example.com'
 
    Demonstrates how to create a Git author signature using an explicit name and email address.
    #>

    [CmdletBinding(DefaultParameterSetName='FromConfiguration')]
    [OutputType([LibGit2Sharp.Signature])]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='FromParameter')]
        [string]
        # The author's name, i.e. GivenName Surname.
        $Name,

        [Parameter(Mandatory=$true,ParameterSetName='FromParameter')]
        [string]
        # The author's email address.
        $EmailAddress,

        [Parameter(Mandatory=$true,ParameterSetName='FromRepositoryConfiguration')]
        [string]
        $RepoRoot
    )

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

    function Get-Signature
    {
        param(
            [LibGit2Sharp.Configuration]
            $Configuration
        )

        $signature = $Configuration.BuildSignature([DateTimeOffset]::Now)
        if( -not $signature )
        {
            Write-Error -Message ('Failed to build author signature from Git configuration files. Please pass custom author information to the "Name" and "EmailAddress" parameters or set author information in Git''s user-level configuration files by running these commands:
  
    git config --global user.name "GIVEN_NAME SURNAME"
    git config --global user.email "email@example.com"
 '
) -ErrorAction $ErrorActionPreference
            return
        }
        return $signature
    }

    if( $PSCmdlet.ParameterSetName -eq 'FromRepositoryConfiguration' )
    {
        $repo = Get-GitRepository -RepoRoot $RepoRoot
        if( -not $repo )
        {
            return 
        }

        try
        {
            return Get-Signature -Configuration $repo.Config
        }
        finally
        {
            $repo.Dispose()
        }
    }

    if( $PSCmdlet.ParameterSetName -eq 'FromConfiguration' )
    {
        $blankGitConfigPath = Join-Path -Path $moduleBinRoot -ChildPath 'gitconfig' -Resolve
        [LibGit2Sharp.Configuration]$config = [LibGit2Sharp.Configuration]::BuildFrom($blankGitConfigPath)

        try
        {
            return Get-Signature -Configuration $config
        }
        finally
        {
            $config.Dispose()
        }
    }

    New-Object -TypeName 'LibGit2Sharp.Signature' -ArgumentList $Name,$EmailAddress,([DateTimeOffset]::Now)
}


function New-GitTag{
    <#
    .SYNOPSIS
    Creates a new tag in a Git repository.
     
    .DESCRIPTION
    The `New-GitTag` function creates a tag in a Git repository.
     
    A tag is a name that references/points to a specific commit in the repository. By default, the tag points to the commit checked out in the working directory. To point to a specific commit, pass the commit ID to the `Target` parameter.
 
    If the tag already exists, this function will fail. If you want to update an existing tag to point to a different commit, use the `Force` switch.
 
    This function implements the `git tag <tagname> <target>` command.
     
    .EXAMPLE
    New-GitTag -Name 'BranchBaseline'
     
    Creates a new tag, `BranchBaseline`, for the HEAD of the current directory.
     
    .EXAMPLE
    New-GitTag -Name 'BranchBaseline' -Target 'branch'
     
    Demonstrates how to create a tag pointing to the head of a branch.
     
    .EXAMPLE
    New-GitTag -Name 'BranchBaseline' -Force
     
    Demonstrates how to change the target a tag points to, to the current HEAD.
    #>


    [CmdletBinding()]
    param(
        [string]
        # Specifies which git repository to add the tag to. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the tag.
        $Name,

        [string]
        # The revision the tag should point to/reference. A revision can be a specific commit ID/sha (short or long), branch name, tag name, etc. Run git help gitrevisions or go to https://git-scm.com/docs/gitrevisions for full documentation on Git's revision syntax.
        $Revision = "HEAD",

        [Switch]
        # Overwrite existing tag to point at new target
        $Force
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        if( -not $Force -and (Test-GitTag -RepoRoot $RepoRoot -Name $Name) )
        {
            Write-Error ("Tag '{0}' already exists. Please use a different tag name." -f $Name)
            return
        }

        $validTarget = $repo.Lookup($Revision)
        if( -not $validTarget )
        {
            Write-Error ("No valid git object identified by '{0}' exists in the repository." -f $Revision)
            return
        }

        $allowOverwrite = $false
        if( $Force )
        {
            $allowOverwrite = $true
        }
        
        $repo.Tags.Add($Name, $Revision, $allowOverwrite)
    }
    finally
    {
        $repo.Dispose()
    }

}


function Receive-GitCommit
{
    <#
    .SYNOPSIS
    Downloads all branches (and their commits) from remote repositories.
 
    .DESCRIPTION
    The `Recieve-GitCommit` function gets all the commits on all branches from all remote repositories and brings them into your repository.
 
    It defaults to the repository in the current directory. Pass the path to a different repository to the `RepoRoot` parameter.
 
    This function implements the `git fetch` command.
 
    .EXAMPLE
    Receive-GitCommit
 
    Demonstrates how to get all branches from a remote repository.
    #>

    [CmdletBinding()]
    param(
        [string]
        # The repository to fetch updates for. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

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

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

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    $options = New-Object 'libgit2sharp.FetchOptions'
    if( $Credential )
    {
        $gitCredential = New-Object 'LibGit2Sharp.SecureUsernamePasswordCredentials'
        $gitCredential.Username = $Credential.UserName
        $gitCredential.Password = $Credential.Password
        $options.CredentialsProvider = { return $gitCredential }
    }

    try
    {
        foreach( $remote in $repo.Network.Remotes )
        {
            [string[]]$refspecs = $remote.FetchRefSpecs | Select-Object -ExpandProperty 'Specification'
            [LibGit2Sharp.Commands]::Fetch($repo, $remote.Name, $refspecs, $options, $null)
        } 
    }
    finally
    {
        $repo.Dispose()
    }

}


function Remove-GitConfiguration
{
    <#
    .SYNOPSIS
    Removes/unsets a Git configuration value.
 
    .DESCRIPTION
    The `Remove-GitConfiguration` function removes/unsets a Git configuration value. Pass the name of the setting to the `Name` parameter (sections and names should be seperated by a dot, e.g. `user.name`). Pass the scope at which you want the configuration removed to the `Scope` parameter. Values are:
 
    * `Local`: the setting will be removed from the repository in the current working directory's `.git\config` file.
    * `Global`: the setting will be removed from the user's `.gitconfig` file.
    * `Xdg`: the setting will be removed from the user's `.config\git\config` file.
    * `System`: the setting will be removed from Git's global `gitconfig` file.
    * `ProgramData` the setting will be removed from the `Git\config` file in Windows' `ProgramData` directory.
 
    To work on a specific repository, pass its path to the `RepoRoot` directory.
 
    To operate on a specific file, pass its path to the `Path` directory.
 
    If the setting doesn't exist, nothing happens.
 
    .EXAMPLE
    Remove-GitConfiguration -Name 'user.name' -Scope Global
 
    Demonstrates how to removes a setting from a given scope. In this case, the `user.name` setting will be removed from the user's `.gitconfig` file.
 
    .EXAMPLE
    Remove-GitConfiguration -Name 'user.name' -Scope Local -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to remove a setting from a specific repository. In this case, the `user.name` setting is removed from the `.git\config` file in the `C:\Projects\GitAutomation` repository.
 
    .EXAMPLE
    Remove-GitConfiguration -Name 'user.name' -Path 'C:\Projects\GitAutomation\template.gitconfig'
 
    Demonstrates how to remove a setting from a specific file. In this case, the `user.name` setting is removed from the `C:\Projects\GitAutomation\template.gitconfig` file.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]
        # The name of the configuration variable to remove.
        $Name,

        [Parameter(ParameterSetName='ByScope')]
        [LibGit2Sharp.ConfigurationLevel]
        # Where to remove the configuration value. Local means the value will be removed from the repository in the current working directory. Global means remove from the current user's `.gitconfig` file. Xdg means remove from the user's `.config\git\config` file. System means remove from Git's system-wide configuration file. `ProgramData` means remove from the Git's config file in the `Git` directory in Windows' ProgramData directory. The default is `Local`.
        $Scope = ([LibGit2Sharp.ConfigurationLevel]::Local),

        [Parameter(Mandatory=$true,ParameterSetName='ByPath')]
        [string]
        # The path to a specific file where a configuration value should be removed.
        $Path,

        [Parameter(ParameterSetName='ByScope')]
        [string]
        # The path to the repository whose configuration variables to remove. Defaults to the repository the current directory is in.
        $RepoRoot = (Get-Location).Path
    )

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

    if( $PSCmdlet.ParameterSetName -eq 'ByPath' )
    {
        if( -not (Test-Path -Path $Path -PathType Leaf) )
        {
            return
        }

        $Path = Resolve-Path -Path $Path | Select-Object -ExpandProperty 'ProviderPath'

        $config = [LibGit2Sharp.Configuration]::BuildFrom($Path)
        try
        {
            $config.Unset( $Name, [LibGit2Sharp.ConfigurationLevel]::Local )
        }
        finally
        {
            $config.Dispose()
        }
        return
    }

    $pathParam = @{}
    if( $RepoRoot )
    {
        $pathParam['Path'] = $RepoRoot
    }

    Write-Verbose -Message ('Removing configuration "{0}".' -f $Name)

    if( $Scope -eq [LibGit2Sharp.ConfigurationLevel]::Local )
    {
        $repo = Find-GitRepository @pathParam -Verify -ErrorAction Ignore
        if( -not $repo )
        {
            Write-Error -Message ('There is no Git repository at "{0}". Unable to unset "{1}".' -f $RepoRoot,$Name)
            return
        }

        try
        {
            $repo.Config.Unset($Name,$Scope)
        }
        finally
        {
            $repo.Dispose()
        }
        return
    }

    $config = [LibGit2Sharp.Configuration]::BuildFrom([nullstring]::Value,[nullstring]::Value)
    try
    {
        $config.Unset($Name,$Scope)
    }
    finally
    {
        $config.Dispose()
    }

}



function Remove-GitItem
{
    <#
    .SYNOPSIS
    Function to Remove files from both working directory and in the repository
 
    .DESCRIPTION
    This function will delete the files from the working directory and stage the files to be deleted in the next commit. Multiple filepaths can be passed at once.
 
    .EXAMPLE
    Remove-GitItem -RepoRoot $repoRoot -Path 'file.ps1'
 
    .Example
    Remove-GitItem -Path 'file.ps1'
 
    .Example
    Get-ChildItem '.\GitAutomation\Functions','.\Tests' | Remove-GitItem
 
    #>


    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [String[]]
        # The paths to the files/directories to remove in the next commit.
        $Path,

        [string]
        # The path to the repository where the files should be removed. The default is the current directory as returned by Get-Location.
        $RepoRoot = (Get-Location).ProviderPath
    )
    
    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $repo = Find-GitRepository -Path $RepoRoot -Verify

    if( -not $repo )
    {
        return
    }

    foreach( $pathItem in $Path )
    {
        if( -not [IO.Path]::IsPathRooted($pathItem) )
        {
            $pathItem = Join-Path -Path $repo.Info.WorkingDirectory -ChildPath $pathItem
        }
        [LibGit2Sharp.Commands]::Remove($repo, $pathItem, $true, $null)
    }
    $repo.Dispose()
}


function Save-GitCommit
{
    <#
    .SYNOPSIS
    Commits changes to a Git repository.
 
    .DESCRIPTION
    The `Save-GitCommit` function commits changes to a Git repository. Those changes must be staged first with `git add` or the `GitAutomation` module's `Add-GitItem` function. If there are no changes staged, nothing happens, and nothing is returned.
 
    You are required to pass a commit message with the `Message` parameter. This module is intended to be used by non-interactive repository automation scripts, so opening in an editor is not supported.
 
    Implements the `git commit` command.
 
    .OUTPUTS
    Git.Automation.CommitInfo
 
    .LINK
    Add-GitItem
 
    .EXAMPLE
    Save-GitCommit -Message 'Creating Save-GitCommit function.'
 
    Demonstrates how to commit staged changes in a Git repository. In this example, the repository is assumed to be in the current directory.
 
    .EXAMPLE
    Save-GitCommit -Message 'Creating Save-GitCommit function.' -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to commit changes to a repository other than the current directory.
 
    .EXAMPLE
    Save-GitCommit -Message 'Creating Save-GitCommit function.' -Signature (New-GitSignature -Name 'Name' -EmailAddress 'email@example.com')
 
    Demonstrates how to set custom author metadata. In this case, the commit will be from user "Name" whose email address is "email@example.com".
    #>

    [CmdletBinding()]
    [OutputType([Git.Automation.CommitInfo])]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The commit message.
        $Message,

        [string]
        # The repository where to commit staged changes. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [LibGit2Sharp.Signature]
        # Author metadata. If not provided, it is pulled from configuration. To create an author/signature object,
        #
        # New-GitSignature -name 'Name' -EmailAddress 'email@example.com'
        #
        $Signature
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        $commitOptions = New-Object 'LibGit2Sharp.CommitOptions'
        $commitOptions.AllowEmptyCommit = $false
        if( -not $Signature )
        {
            $Signature = New-GitSignature -RepoRoot $RepoRoot -ErrorAction Ignore
            if( -not $Signature )
            {
                Write-Error -Message ('Failed to build author signature from Git configuration files. Pass an author signature to the "Signature" parameter (use the "New-GitSignature" function to create an author signature) or set author information in Git''s user-level configuration files by running these commands:
  
    git config --global user.name "GIVEN_NAME SURNAME"
    git config --global user.email "email@example.com"
 '
)
                return
            }
        }

        $repo.Commit( $Message, $Signature, $Signature, $commitOptions ) |
            ForEach-Object { New-Object 'Git.Automation.CommitInfo' $_ } 
    }
    catch [LibGit2Sharp.EmptyCommitException]
    {
        $Global:Error.RemoveAt(0)
        Write-Warning -Message ('Nothing to commit. Git only commits changes that are staged. To stage changes, use the Add-GitItem function or the `git add` command.')
    }
    catch 
    {
        Write-Error -ErrorRecord $_
    }
    finally
    {
        $repo.Dispose()
    }

}


function Send-GitBranch
{
    <#
    .SYNOPSIS
    Pushes the current branch to a remote repository, merging in changes from the remote branch, if necessary.
 
    .DESCRIPTION
    The `Send-GitBranch` function sends the changes in the current branch to a remote repository. If there are any new changes for that branch on the remote repository, they are pulled in and merged with the local branch using the `Sync-GitBranch` function.
 
    Use the `MergeStrategy` argument to control how new changes are merged into your branch. The default is to use the `merge.ff` Git setting, which is to fast-forward when possible, merge otherwise.
 
    The `Retry` parameter controls how many pull/merge/push attempts to make. The default is "5".
 
    Returns a `Git.Automation.SendBranchResult`. To see if the push succeeded, check the `LastPushResult` property, which is a `Git.Automation.PushResult` enumeration. A value of `Ok` means the push succeeded. Other values are `Failed` or `Rejected`.
 
    The result object contains lists for every merge and push operation this function attempts. Merge results are in a `MergeResult` object, from first attempt to most recent attempt. Push results are in a `PushResult` property, from first attempt to most recent attempt.
 
    The most recent merge result is available as the `LastMergeResult` property. The most recent push result is available as the `LastPushResult` property.
 
    This command implements the `git push` command, and, if there are new changes in the remote repository, the `git pull` command.
 
    .LINK
    Sync-GitBranch
 
    .LINK
    Send-GitCommit
 
    .EXAMPLE
    Send-GitBranch
 
    Demonstrates how to push changes to a remote repository.
    #>

    [CmdletBinding()]
    [OutputType([Git.Automation.SendBranchResult])]
    param(
        [string]
        $RepoRoot = (Get-Location).ProviderPath,

        [ValidateSet('FastForward','Merge')]
        [string]
        # What to do when merging remote changes into your local branch. By default, will use your configured `merge.ff` configuration options. Set to `Merge` to always create a merge commit. Use `FastForward` to only allow fast-forward "merges" (i.e. move the remote branch to point to your local branch head if there are no new changes on the remote branch). When automating, the safest option is `Merge`. If you choose `FastForward` and the remote branch has new changes on it, this function will fail.
        $MergeStrategy,

        [int]
        # The number of times to retry the push. Default is 5.
        $Retry = 5
    )

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

    $mergeStrategyParam = @{ }
    if( $MergeStrategy )
    {
        $mergeStrategyParam['MergeStrategy'] = $MergeStrategy
    }
    
    $result = New-Object -TypeName 'Git.Automation.SendBranchResult'

    try
    {
        $tryNum = 0
        do
        {
            $syncResult = Sync-GitBranch -RepoRoot $RepoRoot @mergeStrategyParam
            if( -not $syncResult )
            {
                return
            }

            $result.MergeResult.Add($syncResult)

            if( $syncResult.Status -eq [LibGit2Sharp.MergeStatus]::Conflicts )
            {
                Write-Error -Message ('There are merge conflicts pulling remote changes into local branch.')
                return
            }

            $pushResult = Send-GitCommit -RepoRoot $RepoRoot
            $result.PushResult.Add($pushResult)
        }
        while( $tryNum++ -lt $Retry -and $pushResult -ne [Git.Automation.PushResult]::Ok )
    }
    finally
    {
        Write-Output -InputObject $result -NoEnumerate
    }
}



function Send-GitCommit
{
    <#
    .SYNOPSIS
    Pushes commits from the current Git repository to its remote source repository.
 
    .DESCRIPTION
    The `Send-GitCommit` function sends all commits on the current branch of the local Git repository to its upstream remote repository. If you are pushing a new branch, use the `SetUpstream` switch to ensure Git tracks the new remote branch as a copy of the local branch.
     
    If the repository requires authentication, pass the username/password via the `Credential` parameter.
 
    Returns a `Git.Automation.PushResult` that represents the result of the push. One of:
 
    * `Ok`: the push succeeded
    * `Failed`: the push failed.
    * `Rejected`: the push failed because there are changes on the branch that aren't present in the local repository. They should get pulled into the local repository and the push attempted again.
 
    This function implements the `git push` command.
 
    .EXAMPLE
    Send-GitCommit
 
    Pushes commits from the repository at the current location to its upstream remote repository
 
    .EXAMPLE
    Send-GitCommit -RepoRoot 'C:\Build\TestGitRepo' -Credential $PsCredential
 
    Pushes commits from the repository located at 'C:\Build\TestGitRepo' to its remote using authentication
    #>

    [CmdletBinding()]
    [OutputType([Git.Automation.PushResult])]
    param(
        [string]
        # Specifies the location of the repository to synchronize. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,
        
        [pscredential]
        # The credentials to use to connect to the source repository.
        $Credential,

        [Switch]
        # Add tracking information for any new branches pushed so Git sees the local branch and remote branch as the same.
        $SetUpstream
    )
    
    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    
    try
    {
        [LibGit2Sharp.Branch]$currentBranch = $repo.Branches | Where-Object { $_.IsCurrentRepositoryHead -eq $true }

        $result = Send-GitObject -RefSpec $currentBranch.CanonicalName -RepoRoot $RepoRoot -Credential $Credential

        if( -not $SetUpstream -or $result -ne [Git.Automation.PushResult]::Ok )
        {
            return $result
        }

        # Setup tracking with the new remote branch.
        [void]$repo.Branches.Update($currentBranch, {
            param(
                [LibGit2Sharp.BranchUpdater]
                $Updater
            )

            $updater.Remote = 'origin'
            $updater.UpstreamBranch = $currentBranch.CanonicalName
        });
        
        return $result
    }
    finally
    {
        $repo.Dispose()
    }
}



function Send-GitObject
{
    <#
    .SYNOPSIS
    Sends Git refs and object to a remote repository.
 
    .DESCRIPTION
    The `Send-GitObject` functions sends objects from a local repository to a remote repository. You specify what refs and objects to send with the `Revision` parameter.
 
    This command implements the `git push` command.
 
    .EXAMPLE
    Send-GitObject -Revision 'refs/heads/master'
 
    Demonstrates how to push the commits on a specific branch to the default remote repository.
 
    .EXAMPLE
    Send-GitObject -Revision 'refs/heads/master' -RemoteName 'upstream'
 
    Demonstrates how to push an object (in this case, the master branch) to a specific remote repository, in this case the remote named "upstream".
 
    .EXAMPLE
    Send-GitObject -Revision 'refs/tags/4.45.6'
 
    Demonstrates how to push a tag to the default remote repository.
 
    .EXAMPLE
    Send-GitObject -Tags
 
    Demonstrates how to push all tags to the default remote repository.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='ByRefSpec')]
        [string[]]
        # The refs that should be pushed to the remote repository.
        $RefSpec,

        [Parameter(Mandatory=$true,ParameterSetname='Tags')]
        [Switch]
        # Push all tags to the remote repository.
        $Tags,

        [string]
        # The name of the remote repository to send the changes to. The default is `origin`.
        $RemoteName = 'origin',

        [string]
        # The path to the local repository from which to push changes. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,
        
        [pscredential]
        # The credentials to use to connect to the source repository.
        $Credential
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $repo = Find-GitRepository -Path $RepoRoot -Verify
    
    $pushOptions = New-Object -TypeName 'LibGit2Sharp.PushOptions'
    if( $Credential )
    {
        $gitCredential = New-Object -TypeName 'LibGit2Sharp.SecureUsernamePasswordCredentials'
        $gitCredential.Username = $Credential.UserName
        $gitCredential.Password = $Credential.Password
        $pushOptions.CredentialsProvider = { return $gitCredential }
    }

    $remote = $repo.Network.Remotes | Where-Object { $_.Name -eq $RemoteName }
    if( -not $remote )
    {
        Write-Error -Message ('A remote named "{0}" does not exist.' -f $RemoteName)
        return [Git.Automation.PushResult]::Failed
    }

    if( $Tags )
    {
        $RefSpec = $repo.Tags | ForEach-Object { $_.CanonicalName }
    }

    try
    {
        $repo.Network.Push($remote, $RefSpec, $pushOptions)
        return [Git.Automation.PushResult]::Ok
    }
    catch
    {
        Write-Error -ErrorRecord $_
        
        switch ( $_.FullyQualifiedErrorId )
        {
            'NonFastForwardException' { return [Git.Automation.PushResult]::Rejected }
            'LibGit2SharpException' { return [Git.Automation.PushResult]::Failed }
            'BareRepositoryException' { return [Git.Automation.PushResult]::Failed }
            default { return [Git.Automation.PushResult]::Failed }
        }
    }
    finally
    {
        $repo.Dispose()
    }

}



function Set-GitConfiguration
{
    <#
    .SYNOPSIS
    Sets Git configuration options
 
    .DESCRIPTION
    The `Set-GitConfiguration` function sets Git configuration variables. These variables change Git's behavior. Git has hundreds of variables and we can't document them here. Some are shared between Git commands. Some variables are only used by specific commands. The `git help config` help topic lists most of them.
 
    By default, this function sets options for the current repository, or a specific repository using the `RepoRoot` parameter. To set options for the current user across all repositories, use the `-Global` switch. If running in an elevated process, `Set-GitConfiguration` will look in `$env:HOME` and `$env:USERPROFILE` (in that order) for a .gitconfig file. If it can't find one, it will create one in `$env:HOME`. If the `HOME` environment variable isn't defined, it will create a `.gitconfig` file in the `$env:USERPROFILE` directory.
     
    If running in a non-elevated process, `Set-GitConfiguration` will look in `$env:HOME`, `$env:HOMEDRIVE$env:HOMEPATH`, and `$env:USERPROFILE` (in that order) and use the first `.gitconfig` file it finds. If it can't find a `.gitconfig` file, it will create a `.gitconfig` in the `$env:HOME` directory. If the `HOME` environment variable isn't defined, it will create the `.gitconfig` file in the `$env:HOMEDRIVE$env:HOMEPATH` directory.
 
    To set the configuration in a specific file, use the `Path` parameter. If the file doesn't exist, it is created.
 
    This function implements the `git config` command.
 
    .EXAMPLE
    Set-GitConfiguration -Name 'core.autocrlf' -Value 'false'
 
    Demonstrates how to set the `core.autocrlf` setting to `false` for the repository in the current directory.
 
    .EXAMPLE
    Set-GitConfiguration -Name 'core.autocrlf' -Value 'false' -Global
 
    Demonstrates how to set a configuration variable so that it applies across all a user's repositories by using the `-Global` switch.
 
    .EXAMPLE
    Set-GitConfiguration -Name 'core.autocrlf' -Value 'false' -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to set a configuration variable for a specific repository. In this case, the configuration for the repository at `C:\Projects\GitAutomation` will be updated.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]
        # The name of the configuration variable to set.
        $Name,

        [Parameter(Mandatory=$true,Position=1)]
        [string]
        # The value of the configuration variable.
        $Value,

        [Parameter(ParameterSetName='ByScope')]
        [LibGit2Sharp.ConfigurationLevel]
        # Where to set the configuration value. Local means the value will be set for a specific repository. Global means set for the current user. System means set for all users on the current computer. The default is `Local`.
        $Scope = ([LibGit2Sharp.ConfigurationLevel]::Local),

        [Parameter(Mandatory=$true,ParameterSetName='ByPath')]
        [string]
        # The path to a specific file whose configuration to update.
        $Path,

        [Parameter(ParameterSetName='ByScope')]
        [string]
        # The path to the repository whose configuration variables to set. Defaults to the repository the current directory is in.
        $RepoRoot
    )

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

    if( $PSCmdlet.ParameterSetName -eq 'ByPath' )
    {
        if( -not (Test-Path -Path $Path -PathType Leaf) )
        {
            New-Item -Path $Path -ItemType 'File' -Force | Write-Verbose
        }

        $Path = Resolve-Path -Path $Path | Select-Object -ExpandProperty 'ProviderPath'

        $config = [LibGit2Sharp.Configuration]::BuildFrom($Path)
        try
        {
            $config.Set( $Name, $Value, 'Local' )
        }
        finally
        {
            $config.Dispose()
        }
        return
    }

    $pathParam = @{}
    if( $RepoRoot )
    {
        $pathParam['Path'] = $RepoRoot
    }

    if( $Scope -eq [LibGit2Sharp.ConfigurationLevel]::Local )
    {
        $repo = Find-GitRepository @pathParam -Verify
        if( -not $repo )
        {
            return
        }

        try
        {
            $repo.Config.Set($Name,$Value,$Scope)
        }
        finally
        {
            $repo.Dispose()
        }
    }
    else
    {
        Update-GitConfigurationSearchPath -Scope $Scope

        $configFileName = 'config'
        $configFileNames = @{
                                [LibGit2Sharp.ConfigurationLevel]::System = 'gitconfig'
                                [LibGit2Sharp.ConfigurationLevel]::Global = '.gitconfig'
                            }
        if( $configFileNames.ContainsKey($Scope) )
        {
            $configFileName = $configFileNames[$Scope]
        }

        if( $Scope -eq [LibGit2Sharp.ConfigurationLevel]::Xdg )
        {
            $xdgConfigPath = Join-Path -Path $env:HOME -ChildPath '.config\git\config'
            if( -not (Test-Path -Path $xdgConfigPath -PathType Leaf) )
            {
                New-Item -Path $xdgConfigPath -ItemType 'File' -Force | Out-Null
            }
        }

        # LibGit2 only creates config files explicitly.
        [string[]]$searchPaths = [LibGit2Sharp.GlobalSettings]::GetConfigSearchPaths($Scope) | Join-Path -ChildPath $configFileName
        if( $searchPaths )
        {
            $scopeConfigFiles = $searchPaths | Where-Object { Test-Path -Path $_ -PathType Leaf }
            if( -not $scopeConfigFiles )
            {
                New-Item -Path $searchPaths[0] -ItemType 'File' -Force | Write-Verbose
            }
        }

        $config = [LibGit2Sharp.Configuration]::BuildFrom([nullstring]::Value,[nullstring]::Value)
        try
        {
            $config.Set($Name,$Value,$Scope)
        }
        finally
        {
            $config.Dispose()
        }
    }
}



function Sync-GitBranch
{
    <#
    .SYNOPSIS
    Updates the current branch so it is in sync with its remote branch.
 
    .DESCRIPTION
    The `Sync-GitBranch` function merges in commits from the current branch's remote branch. It pulls in these commits from the remote repository. If there are new commits in the remote branch, they are merged into your current branch and a new commit is created. If there are no new commits in the remote branch, the remote branch is updated to point to the head of your current branch. This is called a "fast forward" merge.
     
    This function's default behavior is controlled by Git's `merge.ff` setting. If unset or set to `true`, it behaves as described above. You can also use the `MergeStrategy` parameter to control how you want remote commits to get merged into your branch.
     
    If the `merge.ff` setting is `only`, or you pass `FastForward` to the `MergeStrategy` parameter, this function will only do a fast-forward merge. If there are new commits in the remote branch, a fast-forward merge is impossible and this function will fail.
     
    If the `merge.ff` setting is `false`, or you pass `Merge` to the `MergeStrategy` parameter, the function will always create a merge commit, even if there are no new commits on the remote branch.
 
    Returns a `LibGit2Sharp.MergeResult` object, which has two properties:
     
    * `Commit`: the merge commit created, if any.
    * `Status`: the status of the merge. One of:
        * `UpToDate`: there were no new changes on the remote branch to bring in. In this case, `Commit` will be empty.
        * `FastForward`: the merge was fast-forwarded. In this case, `Commit` will be emtpy.
        * `NonFastForward`: a new merge commit was created. In this case, `Commit` will be the commit object created`.
        * `Conflicts`: merging in the remote branch resulted in merge conflicts. You'll need to do extra processing to resolve the conflicts.
 
    If the function needs to create a merge commit, but the `merge.ff` option is `only` or the `MergeStrategy` parameter is `FastForward`, the function will write an error and return `$null`.
 
    If there are conflicts made during the merge, this function won't write an error. You need to check the return object to ensure there are no conflicts.
 
    If the current branch isn't tracking a remote branch, this function will look for a remote branch with the same name, and create tracking information. If there is no remote branch with the same name, this function will write an error and return `$null`.
 
    By default, this function works on the repository in the current directory. Use the `RepoRoot` parameter to specify an explicit repository.
 
    This function implements the `git pull` command.
 
    .EXAMPLE
    Sync-GitBranch
 
    Demonstrates the simplest way to get your current branch up-to-date with its remote branch.
 
    .EXAMPLE
    Sync-GitBranch -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to pull remotes commits for a repository that isn't in the current directory.
    #>

    [CmdletBinding()]
    [OutputType([LibGit2Sharp.MergeResult])]
    param(
        [string]
        # The repository to fetch updates for. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [ValidateSet('FastForward','Merge')]
        [string]
        # What to do when merging remote changes into your local branch. By default, will use your configured `merge.ff` configuration options. Set to `Merge` to always create a merge commit. Use `FastForward` to only allow fast-forward "merges" (i.e. move the remote branch to point to your local branch head if there are no new changes on the remote branch). When automating, the safest option is `Merge`. If you choose `FastForward` and the remote branch has new changes on it, this function will fail.
        $MergeStrategy
    )

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

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        $branch = $repo.Branches | Where-Object { $_.IsCurrentRepositoryHead }
        if( -not $branch )
        {
            Write-Error -Message ('Repository in "{0}" isn''t on a branch. Use "Update-GitRepository" to update to a branch.' -f $RepoRoot)
            return
        }

        if( -not $branch.IsTracking )
        {
            [LibGit2Sharp.Branch]$remoteBranch = $repo.Branches | Where-Object { $_.UpstreamBranchCanonicalName -eq $branch.CanonicalName }
            if( -not $remoteBranch )
            {
                Write-Error -Message ('Branch "{0}" in repository "{1}" isn''t tracking a remote branch and we''re unable to find a remote branch named "{0}".' -f $branch.FriendlyName,$RepoRoot)
                return
            }
        
            [void]$repo.Branches.Update($branch, {
                param(
                    [LibGit2Sharp.BranchUpdater]
                    $Updater
                )
        
                $Updater.TrackedBranch = $remoteBranch.CanonicalName
            })
        }

        $pullOptions = New-Object LibGit2Sharp.PullOptions
        $mergeOptions = New-Object LibGit2Sharp.MergeOptions
        $mergeOptions.FastForwardStrategy = [LibGit2Sharp.FastForwardStrategy]::Default
        if( $MergeStrategy -eq 'FastForward' )
        {
            $mergeOptions.FastForwardStrategy = [LibGit2Sharp.FastForwardStrategy]::FastForwardOnly
        }
        elseif( $MergeStrategy -eq 'Merge' )
        {
            $mergeOptions.FastForwardStrategy = [LibGit2Sharp.FastForwardStrategy]::NoFastForward
        }
        $pullOptions.MergeOptions = $mergeOptions
        $signature = New-GitSignature -RepoRoot $RepoRoot
        try
        {
            [LibGit2Sharp.Commands]::Pull($repo, $signature, $pullOptions)
        }
        catch
        {
            Write-Error -ErrorRecord $_
        }
    }
    finally
    {
        $repo.Dispose()
    }

}


function Test-GitBranch
{
    <#
    .SYNOPSIS
    Checks if a branch exists in a Git repository.
 
    .DESCRIPTION
    The `Test-GitBranch` command tests if a branch exists in a Git repository. It returns $true if a branch exists; $false otherwise.
     
    Pass the branch name to test to the `Name` parameter
 
    .EXAMPLE
    Test-GitBranch -Name 'develop'
 
    Demonstrates how to check if the 'develop' branch exists in the current directory.
 
    .EXAMPLE
    Test-GitBranch -RepoRoot 'C:\Projects\GitAutomation' -Name 'develop'
 
    Demonstrates how to check if the 'develop' branch exists in a specific repository.
    #>

    [CmdletBinding()]
    param(
        [string]
        # Specifies which git repository to check. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the branch.
        $Name
    )

    Set-StrictMode -Version 'Latest'

    $branch = Get-GitBranch -RepoRoot $RepoRoot | Where-Object { $_.Name -ceq $Name }
    if( $branch )
    {
        return $true
    }
    else
    {
        return $false
    }
    
}

    
function Test-GitCommit
{
    <#
    .SYNOPSIS
    Tests if a Git revision exists.
 
    .DESCRIPTION
    The `Test-GitCommit` function tests if a commit exists. You pass the revision you want to check to the `Revision` parameter and the repository in the current working directory is checked. If a commit exists, the function returns `$true`. Otherwise, it returns `$false`.
 
    To test for a commit in a specific repository, pass the path to that repository to the `RepoRoot` parameter.
 
    .EXAMPLE
    Test-GitCommit -Revision 'feature/test-gitcommit'
 
    Demonstrates how to check if a branch exists. In this example, if the branch `feature/test-gitcommit` exists, `$true` is returned.
 
    .EXAMPLE
    Test-GitCommit -Revision 'deadbee'
 
    Demonstrates how to check if a commit exists using its partial SHA hash.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # A revision to test, e.g. a branch name, partial commit SHA hash, full commit SHA hash, tag name, etc.
        #
        # See https://git-scm.com/docs/gitrevisions for documentation on how to specify Git revisions.
        $Revision,

        [string]
        # The path to the repository. Defaults to the current directory.
        $RepoRoot
    )

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

    if( (Get-GitCommit -Revision $Revision -RepoRoot $RepoRoot -ErrorAction Ignore) )
    {
        return $true
    }

    return $false
}


function Test-GitRemoteUri
{
    <#
    .SYNOPSIS
    Tests if the uri leads to a git repository
 
    .DESCRIPTION
    The `Test-GitRemoteUri` tries to list remote references for the specified uri. A uri that is not a git repo will throw a LibGit2SharpException.
 
    This function is similar to `git ls-remote` but returns a bool based on if there is any output
 
    .EXAMPLE
    Test-GitRemoteUri -Uri 'ssh://git@stash.portal.webmd.com:7999/whs/blah.git'
 
    Demonstrates how to check if there is a repo at the specified uri
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The uri to test
        $Uri
    )

    Set-StrictMode -Version 'Latest'

    try
    {
        [LibGit2Sharp.Repository]::ListRemoteReferences($Uri) | Out-Null
    }
    catch [LibGit2Sharp.LibGit2SharpException]
    {
        return $false
    }
    return $true
}


function Test-GitTag
{
    <#
    .SYNOPSIS
    Tests if a tag exists in a Git repository.
 
    .DESCRIPTION
    The `Test-GitTag function tests if a tag exists in a Git repository.
 
    If a tag exists, returns $true; otherwise $false. Pass the name of the tag to check for to the `Name` parameter.
 
    .EXAMPLE
    Test-GitTag -Name 'Hello'
 
    Demonstrates how to check if the tag 'Hello' exists in the current directory.
    #>


    [CmdletBinding()]
    param(
        [string]
        # Specifies which git repository to check. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the tag to check for.
        $Name
    )

    Set-StrictMode -Version 'Latest'

    $tag = Get-GitTag -RepoRoot $RepoRoot -Name $Name |
                Where-Object { $_.Name -eq $Name }

    return ($tag -ne $null)
}


function Test-GitUncommittedChange
{
     <#
    .SYNOPSIS
    Tests for uncommitted changes in a git repository.
 
    .DESCRIPTION
    The `Test-GitUncommittedChange` function checks for any uncommited changes in a git repository.
 
    It defaults to the current repository and only the current branch. Use the `RepoRoot` parameter to specify an explicit path to another repo.
 
    Implements the `git diff --exit-code` command ( No output if no uncommitted changes, otherwise output diff )
 
    .EXAMPLE
    Test-GitUncommittedChange -RepoRoot 'C:\Projects\GitAutomation'
 
    Demonstrates how to check for uncommitted changes in a repository that isn't the current directory.
    #>


    [CmdletBinding()]
    param(
        [string]
        # The repository to check for uncommitted changes. Defaults to current directory
        $RepoRoot = (Get-Location).ProviderPath
    )

    Set-StrictMode -Version 'Latest'

    if( Get-GitRepositoryStatus -RepoRoot $RepoRoot )
    {
        return $true
    }
 
    return $false   
}


function Update-GitConfigurationSearchPath
{
    [CmdletBinding()]
    param(
        [LibGit2Sharp.ConfigurationLevel]
        # The scope of the configuration. Nothing is updated unless `Global` is used.
        $Scope
    )

    Set-StrictMode -Version 'Latest'

    if( $Scope -ne [LibGit2Sharp.ConfigurationLevel]::Global )
    {
        return
    }

    if( -not (Test-Path -Path 'env:HOME') )
    {
        return
    }

    $homePath = Get-Item -Path 'env:HOME' | Select-Object -ExpandProperty 'Value'
    $homePath = $homePath -replace '\\','/'

    [string[]]$searchPaths = [LibGit2Sharp.GlobalSettings]::GetConfigSearchPaths($Scope)
    if( $searchPaths[0] -eq $homePath )
    {
        return
    }

    $searchList = New-Object -TypeName 'Collections.Generic.List[string]' 
    $searchList.Add($homePath)
    $searchList.AddRange($searchPaths)

    [LibGit2Sharp.GlobalSettings]::SetConfigSearchPaths($Scope, $searchList.ToArray())
}


function Update-GitRepository
{
    <#
    .SYNOPSIS
    Updates the working directory of a Git repository to a specific commit.
 
    .DESCRIPTION
    The `Update-GitRepository` function updates a Git repository to a specific commit, i.e. it checks out a specific commit.
 
    The default target is "HEAD". Use the `Revision` parameter to specifiy a different branch, tag, commit, etc. If you specify a branch name, and there isn't a local branch by that name, but there is a remote branch, this function creates a new local branch that tracks the remote branch.
 
    It defaults to the current repository. Use the `RepoRoot` parameter to specify an explicit path to another repo.
 
    Use the `Force` switch to remove any uncommitted/unstaged changes during the checkout. Otherwise, the update will fail.
 
    This function implements the `git checkout <target>` command.
 
    .EXAMPLE
    Update-GitRepository -RepoRoot 'C:\Projects\GitAutomation' -Revision 'feature/ticket'
 
    Demonstrates how to checkout the 'feature/ticket' branch of the given repository.
 
    .EXAMPLE
    Update-GitRepository -RepoRoot 'C:\Projects\GitAutomation' -Revision 'refs/tags/tag1'
 
    Demonstrates how to create a detached head at the tag 'tag1'.
 
    .EXAMPLE
    Update-GitRepository -RepoRoot 'C:\Projects\GitAutomation' -Revision 'develop' -Force
 
    Demonstrates how to remove any uncommitted changes during the checkout by using the `Force` switch.
    #>


    [CmdletBinding()]
    param(
        [string]
        # Specifies which git repository to update. Defaults to the current directory.
        $RepoRoot = (Get-Location).ProviderPath,

        [string]
        # The revision checkout, i.e. update the repository to. A revision can be a specific commit ID/sha (short or long), branch name, tag name, etc. Run git help gitrevisions or go to https://git-scm.com/docs/gitrevisions for full documentation on Git's revision syntax.
        $Revision = "HEAD",

        [Switch]
        # Remove any uncommitted changes when checking out/updating to `Revision`.
        $Force
    )

    Set-StrictMode -Version 'Latest'

    $repo = Find-GitRepository -Path $RepoRoot -Verify
    if( -not $repo )
    {
        return
    }

    try
    {
        $checkoutOptions = New-Object -TypeName 'LibGit2Sharp.CheckoutOptions'
        if( $Force )
        {
            $checkoutOptions.CheckoutModifiers = [LibGit2Sharp.CheckoutModifiers]::Force
        }

        $branch = $repo.Branches[$Revision]
        if( -not $branch )
        {
            [LibGit2Sharp.Branch]$remoteBranch = $repo.Branches | Where-Object { $_.UpstreamBranchCanonicalName -eq ('refs/heads/{0}' -f $Revision) }
            if( $remoteBranch )
            {
                $branch = $repo.Branches.Add($Revision, $remoteBranch.Tip.Sha)
                $repo.Branches.Update($branch, {
                    param(
                        [LibGit2Sharp.BranchUpdater]
                        $Updater
                    )

                    $Updater.TrackedBranch = $remoteBranch.CanonicalName
                })
            }
        }

        [LibGit2Sharp.Commands]::Checkout($repo, $Revision, $checkoutOptions)
    }
    finally
    {
        $repo.Dispose()
    }
}


function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [Management.Automation.SessionState]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        $SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }

}