posh-git-workflow.psm1

# TODO: delete branch

[PSCustomObject]$global:PoshGitWorkflow = @{
    PullRequestUrlConfigKey = 'workflow.pullrequesturl';
    ReleaseNameArgumentCompleter = { GetReleaseBranches | Where {NameStartsWith $_ $args[2]} | ForEach-Object { ToBranchCompletionResult $_} };
    FeatureNameArgumentCompleter = { GetFeatureBranches | Where {NameStartsWith $_ $args[2]} | ForEach-Object { ToBranchCompletionResult $_} };
    ReleaseFixNameArgumentCompleter = { GetReleaseFixBranches | Where {NameStartsWith $_ $args[2]} | ForEach-Object { ToBranchCompletionResult $_} };
    LocalFeatureNameArgumentCompleter = { GetFeatureBranches | Where {$_.IsLocal -and (NameStartsWith $_ $args[2])} | ForEach-Object { ToBranchCompletionResult $_} };
    LocalReleaseFixNameArgumentCompleter = { GetReleaseFixBranches | Where {$_.IsLocal -and (NameStartsWith $_ $args[2])} | ForEach-Object { ToBranchCompletionResult $_} };
};

function Sync-GitFork {
    <#
    .SYNOPSIS
    Syncs fork with upstream (original) repository.
     
    .DESCRIPTION
    Fetches changes from upstream, rebases master onto upstream/master, pushes master to origin and prunes stale remote tracking refs.
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    Param()

    trap {
        return;
    }

    SyncFork
}

function Get-GitFeature {
    <#
    .SYNOPSIS
    Returns feature branches.
 
    .LINK
    Get-GitRelease
 
    .LINK
    Get-GitReleaseFix
    #>

    [CmdletBinding()]
    param ()
    
    trap {
        return;
    }

    GetFeatureBranches | Select Name,ShortRefName,RefName,IsHead;
}

function Get-GitRelease {
    <#
    .SYNOPSIS
    Returns release branches.
 
    .LINK
    Get-GitReleaseFix
 
    .LINK
    Get-GitFeature
    #>

    [CmdletBinding()]
    param ()
    
    trap {
        return;
    }

    GetReleaseBranches | Select Name,ShortRefName,RefName,IsHead;
}

function Get-GitReleaseFix {
    <#
    .SYNOPSIS
    Returns release fix branches.
 
    .LINK
    Get-GitRelease
 
    .LINK
    Get-GitFeature
    #>

    [CmdletBinding()]
    param ()
    
    trap {
        return;
    }

    GetReleaseFixBranches | Select Name,ShortRefName,RefName,IsHead;
}

function Get-Hotfixes {
    [CmdletBinding()]
    param ()
    
    trap {
        return;
    }

    GethotfixBranches | Select Name,ShortRefName,RefName,IsHead;
}

function New-GitFeature {
    <#
    .SYNOPSIS
    Creates new feature branch.
     
    .DESCRIPTION
    Syncs fork and creates branch with the specified name prefixed with 'feature/' on latest master.
     
    .PARAMETER Name
    Branch name without 'feature/' prefix.
     
    .EXAMPLE
    New-GitFeature cool-stuff
 
    .LINK
    Sync-GitFork
 
    .LINK
    New-GitRelease
 
    .LINK
    New-GitReleaseFix
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$true, 
                   Position=0, 
                   ParameterSetName="Name",
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    trap {
        return;
    }

    SyncFork
    
    ExecuteGitCommand 'git checkout' "-B feature/$Name" '--progress'
}


function New-GitRelease {
    <#
    .SYNOPSIS
    Creates new release branch.
     
    .DESCRIPTION
    Syncs fork, creates branch with the specified name prefixed with 'release/' on latest master, pushes it to upstream and removes local ref.
     
    .PARAMETER Name
    Branch name without 'release/' prefix.
     
    .EXAMPLE
    New-GitRelease v1.1
 
    .LINK
    Sync-GitFork
 
    .LINK
    New-GitReleaseFix
 
    .LINK
    New-GitFeature
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$true, 
                   Position=0, 
                   HelpMessage="Release name (version)")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    trap {
        return;
    }

    SyncFork
    
    ExecuteGitCommand 'git checkout' "-B release/$Name" '--progress'
    ExecuteGitCommand 'git push' '--set-upstream upstream';
    ExecuteGitCommand 'git checkout' 'master' '--progress'
    ExecuteGitCommand 'git branch' "-D release/$Name";
}


function New-GitReleaseFix {
    <#
    .SYNOPSIS
    Creates new release fix branch.
     
    .DESCRIPTION
    Syncs fork and creates branch with the specified name on top of selected release branch. New branch name is constructed in the following manner: release/release-name/branch-name. Release name is optional if there's only one release branch.
     
    .PARAMETER Name
    Branch name without release branch prefix
 
    .PARAMETER ReleaseName
    Release branch name without release prefix
     
    .EXAMPLE
    New-GitReleaseFix bug-fix
 
    .LINK
    Sync-GitFork
 
    .LINK
    New-GitFeature
 
    .LINK
    New-GitRelease
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$true, 
                   Position=0, 
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({ & $PoshGitWorkflow.ReleaseNameArgumentCompleter })]
        [string]
        $Name,
        [Parameter(Mandatory=$false, 
                   Position=1, 
                   HelpMessage="Release branch name")]
        [string]
        $ReleaseName
    )

    SyncFork

    $releaseBranches = @(GetReleaseBranches);

    if ($releaseBranches.Count -eq 0) {
        Write-Error 'No release branches found.'
        return;
    }

    $releaseBranch = $null;
    if ($ReleaseName -eq '') {
        $currentBranch = $releaseBranches | Where {$_.IsHead -eq $True} | Select -First 1;

        if ($currentBranch -ne $null)
        {
            $releaseBranch = $currentBranch;
        }

        if ($releaseBranch -eq $null -and $releaseBranches.Count -eq 1)
        {
            $releaseBranch = $releaseBranches[0];
        }

        if ($releaseBranch -eq $null)
        {
            # TODO: Show prompt
            #$choices = @(
            # New-Object "System.Management.Automation.Host.ChoiceDescription" "&Apple", "Apple"
            # New-Object "System.Management.Automation.Host.ChoiceDescription" "&Banana", "Banana"
            # New-Object "System.Management.Automation.Host.ChoiceDescription" "&Orange", "Orange"
            #)
            #
            ## Single-choice prompt
            #$host.UI.PromptForChoice("Choose a fruit", "You may choose one", $choices, 1)
            Write-Error 'Please specify release branch name.'
            return;
        }
    }
    else {
        $releaseBranch = $releaseBranches | Where {$_.Name -eq $ReleaseName} | Select -First 1;

        if ($releaseBranch -eq $null) {
            Write-Error "Release branch $ReleaseName not found."
            return;
        }
    }

    $releaseRefName = $releaseBranch.ShortRefName;
    $ReleaseName = $releaseBranch.Name;
    ExecuteGitCommand 'git checkout' "-b release/$ReleaseName/$Name --no-track $releaseRefName" '--progress'
}

function Push-GitFeature {
    <#
    .SYNOPSIS
    Pushes feature branch to origin.
     
    .DESCRIPTION
    Pushes specified feature branch to origin. If no branch name specified the current feature branch will be pushed.
     
    .PARAMETER Name
    Branch name without feature prefix.
 
    .EXAMPLE
    Push-GitFeature cool-stuff
 
    .LINK
    Push-GitReleaseFix
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$false, 
                   Position=0, 
                   ParameterSetName="Name",
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({ & $PoshGitWorkflow.LocalFeatureNameArgumentCompleter })]
        [string]
        $Name
    )

    trap {
        return;
    }

    $branch = GetFeatureBranches | Where {$_.IsLocal -and (HasNameOrCurrent $_ $Name)};
    PushBranch $branch;
}

function Push-GitReleaseFix {
    <#
    .SYNOPSIS
    Pushes release fix branch to origin.
     
    .DESCRIPTION
    Pushes specified release fix branch to origin. If no branch name specified the current release fix branch will be pushed.
     
    .PARAMETER Name
    Branch name without release branch prefix.
 
    .EXAMPLE
    Push-GitReleaseFix bug-fix
 
    .LINK
    Push-GitFeature
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$false, 
                   Position=0, 
                   ParameterSetName="Name",
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({ & $PoshGitWorkflow.LocalReleaseFixNameArgumentCompleter })]
        [string]
        $Name
    )

    trap {
        return;
    }

    $branch = GetReleaseFixBranches | Where {$_.IsLocal -and (HasNameOrCurrent $_ $Name)};
    PushBranch $branch;
}

function Complete-GitFeature {
    <#
    .SYNOPSIS
    Pushes feature branch and submits pull request.
     
    .DESCRIPTION
    Pushes feature branch to origin and submits pull request (if configured). If no branch name specified the current feature branch will be used.
     
    .PARAMETER Name
    Branch name without feature prefix.
 
    .EXAMPLE
    Complete-GitFeature cool-stuff
 
    .LINK
    Set-GitPullRequestUrl
 
    .LINK
    Complete-GitReleaseFix
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$false, 
                   Position=0, 
                   ParameterSetName="Name",
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({ & $PoshGitWorkflow.FeatureNameArgumentCompleter })]
        [string]
        $Name
    )

    trap {
        "Error: $_";
        return;
    }

    $branch = GetFeatureBranches | Where {HasNameOrCurrent $_ $Name};
    PushBranch $branch;
    SubmitPullRequest $branch;    
}

function Complete-GitReleaseFix {
    <#
    .SYNOPSIS
    Pushes release fix branch and submits pull request.
     
    .DESCRIPTION
    Pushes release fix branch to origin and submits pull request (if configured). If no branch name specified the current release fix branch will be used.
     
    .PARAMETER Name
    Branch name without release branch prefix.
 
    .EXAMPLE
    Complete-GitReleaseFix bug-fix
 
    .LINK
    Set-GitPullRequestUrl
 
    .LINK
    Complete-GitFeature
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$false, 
                   Position=0, 
                   ParameterSetName="Name",
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({ & $PoshGitWorkflow.ReleaseFixNameArgumentCompleter })]
        [string]
        $Name
    )

    trap {
        "Error: $_";
        return;
    }

    $branch = GetReleaseFixBranches | Where {HasNameOrCurrent $_ $Name};
    PushBranch $branch;
    SubmitPullRequest $branch;    
}

function Complete-GitRelease {
    <#
    .SYNOPSIS
    Merges release branch to master.
     
    .DESCRIPTION
    Merges release branch to master, creates tag with merged branch name and pushes changes to upstream.
     
    .PARAMETER Name
    Branch name without release prefix.
 
    .PARAMETER Message
    Release tag message.
 
    .EXAMPLE
    Complete-GitRelease v1.1 -Message 'Awesome release'
 
    .LINK
    Complete-GitReleaseFix
    #>

    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$false, 
                   Position=0, 
                   HelpMessage="Branch name")]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({ & $PoshGitWorkflow.ReleaseNameArgumentCompleter })]
        [string]
        $Name,
        [Parameter(Mandatory=$false, 
                   Position=1, 
                   HelpMessage="Tag message")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Message
    )

    trap {
        return;
    }

    SyncFork

    $branches = @(GetReleaseBranches);
    $branch = $branches | Where {HasNameOrCurrent $_ $Name};

    if ([System.String]::IsNullOrWhitespace($Name) -and $branch -eq $null -and $branches.Length -eq 1) {
        $branch = $branches[0];
    }

    if ($branch -eq $null) {
        if ([System.String]::IsNullOrWhitespace($Name)) {
            Write-Error "No release branches found";
        }
        else {
            Write-Error "Branch 'release/$Name' not found";
        }
        return;
    }

    $Name = $branch.Name;

    $commitId = ExecuteGitCommand 'git rev-parse' $branch.ShortRefName;

    if ($branch.IsLocal) {
        ExecuteGitCommand 'git branch' "-D release/$Name";
    }

    $messageParameter = '';
    if (-not [System.String]::IsNullOrWhitespace($Message)) {
        $messageParameter = "--message='$Message'";
    }

    ExecuteGitCommand 'git merge' "--no-ff -m `"Merge 'release/$Name'`" $commitId";

    if (HasMergeConflicts) {
        return;
    }

    ExecuteGitCommand 'git tag' "$messageParameter release/$Name $commitId"
    ExecuteGitCommand 'git push' "upstream --delete release/$Name";
    ExecuteGitCommand 'git push' '--set-upstream upstream';
    ExecuteGitCommand 'git branch' '--set-upstream-to origin'
    ExecuteGitCommand 'git push' "upstream --tags"
}

function Set-GitPullRequestUrl {
    <#
    .SYNOPSIS
    Sets submit pull request page URL in local git config.
     
    .DESCRIPTION
    Creates or updates local git config section 'workflow' and sets 'pullrequesturl' key value to the specified URL.
     
    .PARAMETER Url
    Submit pull request page URL.
     
    The URL can be in a form of a template with this placeholders:
    {0} - sourceRef
    {1} - targetRef
     
    GitHub URL example:
    https://github.com/<repo-owner-id>/<original-repo-id>/compare/{1}...<my-github-user-id>:{0}
 
    Visual Studio Online URL example:
    https://<my-account-id>.visualstudio.com/_git/<my-repo-id>/pullrequestcreate?sourceRef={0}&targetRef={1}&sourceRepositoryId=<my-fork-GUID>&targetRepositoryId=<main-repo-GUID>
     
    .EXAMPLE
    Set-GitPullRequestUrl 'https://github.com/octocat/Spoon-Knife/compare/master...my-github-user-id:master'
 
    .EXAMPLE
    Set-GitPullRequestUrl 'https://my-account.visualstudio.com/_git/my-repo/pullrequestcreate?sourceRef={0}&targetRef={1}&sourceRepositoryId=my-fork-repo-GUID&targetRepositoryId=main-repo-GUID'
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false, 
                   Position=0, 
                   ParameterSetName="Url",
                   HelpMessage="Pull request URL")]
        [System.Uri] $Url
    )
    
    trap {
        return;
    }

    SetGitConfigValue $PoshGitWorkflow.PullRequestUrlConfigKey $Url;
}

function SubmitPullRequest {
    [CmdletBinding()]
    param ($branch)
    
    $pullRequestUrl = GetGitConfigValue $PoshGitWorkflow.PullRequestUrlConfigKey;

    if ([System.String]::IsNullOrWhitespace($pullRequestUrl)) {
        return;
    }

    [System.Reflection.Assembly]::LoadWithPartialName('System.Web') | out-null;
    $sourceRef = $branch.ShortLocalRefName;
    $targetRef = 'master';

    if ($branch.IsSubBranch -and -not $branch.IsOther) {
        $targetRef = $branch.ShortLocalParentRefName;
    }

    $sourceRef = [System.Web.HttpUtility]::UrlEncode($sourceRef);
    $targetRef = [System.Web.HttpUtility]::UrlEncode($targetRef);

    # "https://rebtel.visualstudio.com/_git/Backend/pullrequestcreate?sourceRef={0}&targetRef={1}&sourceRepositoryId=9b139ae4-3a6c-4946-9837-a14c15243bf3&targetRepositoryId=28e641a9-a204-46c4-b22a-303204901fe9"
    $pullRequestUrl = [System.String]::Format($pullRequestUrl, $sourceRef, $targetRef);

    (New-Object -Com Shell.Application).Open($pullRequestUrl);
}

function SyncFork {
    [CmdletBinding(SupportsShouldProcess=$false)]
    Param()

    if (HasMergeConflicts) {
        throw 'Merge conflicts detected.';
    }
    
    $popStash = $false;

    if (IsDirty) {
        ExecuteGitCommand 'git stash save --include-untracked';
        $popStash = $true;        
    }

    ExecuteGitCommand 'git fetch --prune' 'upstream' '--progress --verbose'
    ExecuteGitCommand 'git checkout' 'master' '--progress'
    ExecuteGitCommand 'git merge --ff-only' 'upstream/master' '--verbose'
    ExecuteGitCommand 'git push' 'origin master' '--progress --verbose'

    PruneRemoteBranches 'origin'
    PruneRemoteBranches 'upstream'

    if ($popStash) {
        ExecuteGitCommand 'git stash pop';
    }

    if (HasMergeConflicts) {
        throw 'Merge conflicts detected.';
    }
}

function PruneRemoteBranches {
    [CmdletBinding()]
    param ([string]$remote)
    
    $result = ExecuteGitCommand 'git remote prune' $remote;
    $staleBranches = [System.Collections.ArrayList]::new();

    $prunedBranchPrefix = '* [pruned] ';

    foreach ($line in $result) {
        $line = $line.Trim();

        if ($line.StartsWith($prunedBranchPrefix)) {
            # prune output looks like this: * [would prune] origin/feature/branch-name
            $branchName = $line.Substring($prunedBranchPrefix.Length + $remote.Length + 1);
            $staleBranches.Add($branchName);
        }
    }

    if ($staleBranches.Count -eq 0) {
        return;
    }

    $localBranchesToDelete = '';
    GetAllBranches | Where {$_.IsLocal -and $staleBranches.Contains($_.ShortRefName)} | ForEach-Object {$localBranchesToDelete += $_.ShortRefName + ' '};

    if ($localBranchesToDelete.Length -eq 0) {
        return;
    }

    ExecuteGitCommand 'git branch -D' $localBranchesToDelete '--verbose'
}

function PushBranch {
    [CmdletBinding(SupportsShouldProcess=$false)]
    param($branch)

    if ($branch -eq $null) {
        Write-Error "Branch $Name could not be found";
        throw 'Branch does not exist.';
    }

    ExecuteGitCommand 'git push --set-upstream origin' $branch.ShortRefName '--verbose --progress'
}

function IsDirty {
    $changes = git status --porcelain;

    return $changes -ne $null -and $changes.Length -ne 0;
}

function HasMergeConflicts {
    $changes = git status --porcelain;

    if ($changes -eq $null -or $changes.Length -eq 0) {
        return $false;
    }

    $conflictStatuses = 'DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU';

    return ($changes | Where {$conflictStatuses.Contains($_.Substring(0, 2))}).Length -gt 0;
}

function HasNameOrCurrent {
    param($branch, $name)

    if ([System.String]::IsNullOrEmpty($name) -and $branch.IsHead) {
        return $branch
    }

    if ($branch.Name -eq $name) {
        return $branch;
    }

    return $null;
}

function GetReleaseBranches () {
    GetAllBranches | Where {$_.IsRelease -and -not $_.IsSubBranch}
}

function GetReleaseFixBranches () {
    GetAllBranches | Where {$_.IsRelease -and $_.IsSubBranch}
}

function GetHotfixBranches () {
    GetAllBranches | Where {$_.IsHotfix -and -not $_.IsSubBranch}
}

function GetHotfixFixBranches () {
    GetAllBranches | Where {$_.IsHotfix -and $_.IsSubBranch}
}

function GetFeatureBranches () {
    GetAllBranches | Where {$_.IsFeature}
}

function GetAllBranches () {
    $trackedRefs = @{};
    $branches = @{};

    git branch --list --all --format='%(refname)>>%(refname:short)>>%(upstream)>>%(HEAD)' | 
        ForEach-Object {
            $values = $_ -split '>>';
        
            $refName = $values[0];
            $shortRefName = $values[1];
            $upstream = $values[2];
            $isHead = $values[3] -eq '*';

            # don't parse manually, use git:
            # git branch --all --format '%(upstream) %(upstream:short) %(upstream:track) %(upstream:trackshort) %(upstream:remotename) %(upstream:lstrip=3)'
            # gives:
            # refs/remotes/origin/feature/vsts-itp origin/feature/vsts-itp [ahead 8] > origin feature/vsts-itp
            $refParts = $refName -split '/';
        
            $isTracking = [System.String]::IsNullOrEmpty($upstream) -ne $true;
            if ($isTracking)
            {
                $trackedRefs[$upstream] = '';
            }

            $isLocal = $refParts[1] -eq 'heads';

            # local - 2: refs/heads/feature/branch_name
            # remote - 3: refs/remotes/origin/feature/branch_name
            $typePartIndex = 3;
            if ($isLocal)
            {
                $typePartIndex = 2;
            }

            $isFeature = $false;
            $isRelease = $false;
            $isHotfix = $false;

            $type = $refParts[$typePartIndex];
            $isFeature = $type -eq 'feature';
            $isRelease = $type -eq 'release';
            $isHotfix = $type -eq 'hotfix';

            $isOther = ($isFeature -or $isRelease -or $isHotfix) -ne $true;

            $name = $shortRefName;

            if ($isOther -eq $false)
            {
                $nameparts = @($refParts | Select -skip ($typePartIndex + 1));
                $name = [System.String]::Join('/', $nameparts);
            }
            
            $localRefParts = @($refParts | Select -Skip ($typePartIndex));

            $isSubBranch = $false;
            if ($localRefParts.Length -gt 2) {
                $isSubBranch = $true;
            }

            $shortLocalRefName = [System.String]::Join('/', $localRefParts);

            if ($localRefParts.Length -gt 1) {
                $shortLocalParentRefName = [System.String]::Join('/', @($localRefParts | Select -SkipLast 1));
            }
        
            $branch = [PSCustomObject]@{
                RefName = $refName;
                ShortRefName = $shortRefName;
                ShortLocalRefName = $shortLocalRefName;
                ShortLocalParentRefName = $shortLocalParentRefName;
                Upstream = $upstream;
                Name = $name;
                IsHead = $isHead;
                IsLocal = $isLocal;
                IsFeature = $isFeature;
                IsRelease = $isRelease;
                Ishotfix = $isHotfix;
                IsOther = $isOther;
                IsSubBranch = $isSubBranch;
                Type = $type;
            };
        
            $branches[$branch.RefName] = $branch;
        };

    foreach($ref in $trackedRefs.Keys)
    {
        $branches.Remove($ref);
    }

    $branches.Values | sort RefName;
}

function NameStartsWith {
    param($branch, $value)

    return $branch.Name.StartsWith($value, $true, [System.Globalization.CultureInfo]::InvariantCulture);
}

function GetStorageFolder () {
    $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath);
    $applicationDataFolder = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ApplicationData);
    return $applicationDataFolder | Join-Path -ChildPath $moduleName;
}

function ToBranchCompletionResult($branch) {
    return [System.Management.Automation.CompletionResult]::new($branch.Name, $branch.Name, 'ParameterValue', $branch.RefName);
}

function GetGitConfigValue {
    [CmdletBinding()]
    param (
        [string] $key
    )
    
    ExecuteGitCommand 'git config --get' $key;
}

function SetGitConfigValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [string] $key,
        [Parameter(Mandatory=$false, Position=1)]
        [string] $value
    )
    
    if ([System.String]::IsNullOrEmpty($value)) {
        ExecuteGitCommand 'git config --unset' $key;    
    }
    else {
        ExecuteGitCommand 'git config' "$key '$value'";
    }
}

function ExecuteGitCommand {
    [CmdletBinding(SupportsShouldProcess=$false)]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Command,

        [Parameter(Mandatory=$false, Position=1)]
        [ValidateNotNullOrEmpty()]
        [string]$Parameters = '',

        [Parameter(Mandatory=$false, Position=2)]
        [ValidateNotNullOrEmpty()]
        [string]$VerboseParameters = '')

    if ($VerbosePreference -ne 'SilentlyContinue') {
        $Command = $Command + " " + $VerboseParameters;
    }

    $Command = $Command + " " + $Parameters;

    Write-Host -ForegroundColor Yellow $Command
    iex $Command

    if ($LASTEXITCODE -ne 0) {
        throw 'Failed'
    }
}