VSTS.psm1

function New-VstsSession {
    param([Parameter()]$AccountName, 
          [Parameter(Mandatory=$true)]$User, 
          [Parameter(Mandatory=$true)]$Token,
          [Parameter()][string]$Collection = 'DefaultCollection',
          [Parameter()][string]$Server = 'visualstudio.com',
          [Parameter()][ValidateSet('HTTP', 'HTTPS')]$Scheme = 'HTTPS'
          )

    [PSCustomObject]@{
        AccountName = $AccountName
        User = $User
        Token = $Token
        Collection = $Collection
        Server = $Server
        Scheme = $Scheme
    }
}

function Invoke-VstsEndpoint {
    param(
          [Parameter(Mandatory=$true)]$Session, 
          [Hashtable]$QueryStringParameters, 
          [string]$Project,
          [Uri]$Path, 
          [string]$ApiVersion='1.0', 
          [ValidateSet('GET', 'PUT', 'POST', 'DELETE', 'PATCH')]$Method='GET',
          [string]$Body)

    $queryString = [System.Web.HttpUtility]::ParseQueryString([string]::Empty)
   
    if ($QueryStringParameters -ne $null)
    {
        foreach($parameter in $QueryStringParameters.GetEnumerator())
        {
            $queryString[$parameter.Key] = $parameter.Value
        }
    }

    $queryString["api-version"] = $ApiVersion
    $queryString = $queryString.ToString();

    $authorization = Get-VstsAuthorization -User $Session.User -Token $Session.Token
    if ([String]::IsNullOrEmpty($Session.AccountName))
    {
        $UriBuilder = New-Object System.UriBuilder -ArgumentList "$($Session.Scheme)://$($Session.Server)"
    }
    else
    {
        $UriBuilder = New-Object System.UriBuilder -ArgumentList "$($Session.Scheme)://$($Session.AccountName).visualstudio.com"
    }
    $Collection = $Session.Collection
    
    $UriBuilder.Query = $queryString
    if ([String]::IsNullOrEmpty($Project))
    {
        $UriBuilder.Path = "$Collection/_apis/$Path"
    }
    else 
    {
        $UriBuilder.Path = "$Collection/$Project/_apis/$Path"
    }

    $Uri = $UriBuilder.Uri

    Write-Verbose "Invoke URI [$uri]"

    $ContentType = 'application/json'
    if ($Method -eq 'PUT' -or $Method -eq 'POST' -or $Method -eq 'PATCH')
    {
        if ($Method -eq 'PATCH')
        {
            $ContentType = 'application/json-patch+json'
        }

        Invoke-RestMethod $Uri -Method $Method -ContentType $ContentType -Headers @{Authorization=$authorization} -Body $Body
    }
    else
    {
        Invoke-RestMethod $Uri -Method $Method -ContentType $ContentType -Headers @{Authorization=$authorization} 
    }
}

function Get-VstsAuthorization {
<#
    .SYNOPSIS
        Generates a VSTS authorization header value from a username and Personal Access Token.
#>

    param($user, $token)

    $Value = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $token)))
    ("Basic {0}" -f $value)
}

function Get-VstsProject {
<#
    .SYNOPSIS
        Get projects in a VSTS account.
#>

    param(
        [Parameter(Mandatory, ParameterSetname='Account')]$AccountName, 
        [Parameter(Mandatory, ParameterSetname='Account')]$User, 
        [Parameter(Mandatory, ParameterSetname='Account')]$Token, 
        [Parameter(Mandatory, ParameterSetname='Session')]$Session, 
        [string]$Name)
    
    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    $Value = Invoke-VstsEndpoint -Session $Session -Path 'projects' 

    if ($PSBoundParameters.ContainsKey("Name"))
    {
        $Value.Value | Where Name -eq $Name
    }
    else
    {
        $Value.Value 
    }
}

function Wait-VSTSProject {
    param([Parameter(Mandatory)]$Session, 
          [Parameter(Mandatory)]$Name, 
          $Attempts = 30, 
          [Switch]$Exists)

    $Retries = 0
    do {
        #Takes a few seconds for the project to be created
        Start-Sleep -Seconds 2

        $TeamProject = Get-VSTSProject -Session $Session -Name $Name

        $Retries++
    } while ((($TeamProject -eq $null -and $Exists) -or ($TeamProject -ne $null -and -not $Exists)) -and $Retries -le $Attempts)

    if (($TeamProject -eq $null -and $Exists) -or ($TeamProject -ne $null -and -not $Exists) ) 
    {
        throw "Failed to create team project!" 
    }
}

function New-VstsProject 
{
    <#
        .SYNOPSIS
            Creates a new project in a VSTS account
    #>

    param(
    [Parameter(Mandatory, ParameterSetname='Account')]$AccountName, 
    [Parameter(Mandatory, ParameterSetname='Account')]$User, 
    [Parameter(Mandatory, ParameterSetname='Account')]$Token, 
    [Parameter(Mandatory, ParameterSetname='Session')]$Session, 
    [Parameter(Mandatory)]$Name, 
    [Parameter()]$Description, 
    [Parameter()][ValidateSet('Git')]$SourceControlType = 'Git',
    [Parameter()]$TemplateTypeId = '6b724908-ef14-45cf-84f8-768b5384da45',
    [Parameter()]$TemplateTypeName = 'Agile',
    [Switch]$Wait)

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    if ($PSBoundParameters.ContainsKey('TemplateTypeName'))
    {
        $TemplateTypeId = Get-VstsProcess -Session $Session | Where Name -EQ $TemplateTypeName | Select -ExpandProperty Id
        if ($TemplateTypeId -eq $null)
        {
            throw "Template $TemplateTypeName not found."
        }
    }

    $Body = @{
        name = $Name
        description = $Description
        capabilities = @{
            versioncontrol = @{
                sourceControlType = $SourceControlType
            }
            processTemplate = @{
                templateTypeId = $TemplateTypeId
            }
        }
    } | ConvertTo-Json

    Invoke-VstsEndpoint -Session $Session -Path 'projects' -Method POST -Body $Body

    if ($Wait)
    {
        Wait-VSTSProject -Session $Session -Name $Name -Exists
    }
}

function Remove-VSTSProject {
    <#
        .SYNOPSIS
            Deletes a project from the specified VSTS account.
    #>

    param(
        [Parameter(Mandatory, ParameterSetname='Account')]$AccountName, 
        [Parameter(Mandatory, ParameterSetname='Account')]$User, 
        [Parameter(Mandatory, ParameterSetname='Account')]$Token, 
        [Parameter(Mandatory, ParameterSetname='Session')]$Session,  
        [Parameter(Mandatory)]$Name,
        [Parameter()][Switch]$Wait)

        if ($PSCmdlet.ParameterSetName -eq 'Account')
        {
            $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
        }

        $Id = Get-VstsProject -Session $Session -Name $Name | Select -ExpandProperty Id

        if ($Id -eq $null)
        {
            throw "Project $Name not found in $AccountName."
        }

        Invoke-VstsEndpoint -Session $Session -Path "projects/$Id" -Method DELETE

        if ($Wait)
        {
            Wait-VSTSProject -Session $Session -Name $Name
        }
}

function Get-VstsWorkItem {
<#
    .SYNOPSIS
        Get work items from VSTS
#>

    param(
    [Parameter(Mandatory, ParameterSetname='Account')]$AccountName, 
    [Parameter(Mandatory, ParameterSetname='Account')]$User, 
    [Parameter(Mandatory, ParameterSetname='Account')]$Token, 
    [Parameter(Mandatory, ParameterSetname='Session')]$Session, 
    [Parameter(Mandatory)]$Id)

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    Invoke-VstsEndpoint -Session $Session -Path 'wit/workitems' -QueryStringParameters @{ids = $id}
}

function New-VstsWorkItem {
<#
    .SYNOPSIS
        Create new work items in VSTS
#>

    param(
    [Parameter(Mandatory, ParameterSetname='Account')]
    $AccountName, 
    [Parameter(Mandatory, ParameterSetname='Account')]
    $User, 
    [Parameter(Mandatory, ParameterSetname='Account')]
    $Token, 
    [Parameter(Mandatory, ParameterSetname='Session')]
    $Session, 
    [Parameter(Mandatory)]
    $Project,
    [Parameter()]
    [Hashtable]
    $PropertyHashtable, 
    [Parameter(Mandatory)]
    [string]
    $WorkItemType
    )

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    if ($PropertyHashtable -ne $null)
    {
        $Fields = foreach($kvp in $PropertyHashtable.GetEnumerator())
        {
            [PSCustomObject]@{
                op = 'add'
                path = '/fields/' + $kvp.Key
                value = $kvp.value
            }
        }

        $Body = $Fields | ConvertTo-Json
    }
    else
    {
        $Body = [String]::Empty
    }

    Invoke-VstsEndpoint -Session $Session -Path "wit/workitems/`$$($WorkItemType)" -Method PATCH -Project $Project -Body $Body
}

function Get-VstsWorkItemQuery {
    <#
    .SYNOPSIS
        Returns a list of work item queries from the specified folder.
    #>

    param(
    [Parameter(Mandatory, ParameterSetname='Account')]
    $AccountName, 
    [Parameter(Mandatory, ParameterSetname='Account')]
    $User, 
    [Parameter(Mandatory, ParameterSetname='Account')]
    $Token, 
    [Parameter(Mandatory, ParameterSetname='Session')]
    $Session, 
    [Parameter(Mandatory=$true)]$Project, 
    $FolderPath)

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    $Result = Invoke-VstsEndpoint -Session $Session -Project $Project -Path 'wit/queries' -QueryStringParameters @{depth=1}

    foreach($value in $Result.Value)
    {
        if ($Value.isFolder -and $Value.hasChildren)
        {
            Write-Verbose "$Value.Name"
            foreach($child in $value.Children)
            {
                if (-not $child.isFolder)
                {
                    $child
                }
            }
        }
    } 
}

function New-VstsGitRepository {
    <#
        .SYNOPSIS
            Creates a new Git repository in the specified team project.
    #>

    param(
    [Parameter(Mandatory, ParameterSetname='Account')]
    $AccountName, 
    [Parameter(Mandatory, ParameterSetname='Account')]
    $User, 
    [Parameter(Mandatory, ParameterSetname='Account')]
    $Token, 
    [Parameter(Mandatory, ParameterSetname='Session')]
    $Session, 
    [Parameter(Mandatory=$true)]
    $Project,
    [Parameter(Mandatory=$true)]
    $RepositoryName)  

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    if (-not (Test-Guid $Project))
    {
        $Project = Get-VstsProject -Session $Session -Name $Project | Select -ExpandProperty Id
    }

    $Body = @{
        Name = $RepositoryName
        Project = @{
            Id = $Project
        }
    } | ConvertTo-Json

    Invoke-VstsEndpoint -Session $Session -Method POST -Path 'git/repositories' -Body $Body
}

function Get-VstsGitRepository {
    <#
        .SYNOPSIS
            Gets Git repositories in the specified team project.
    #>

        param(
        [Parameter(Mandatory, ParameterSetname='Account')]
        $AccountName, 
        [Parameter(Mandatory, ParameterSetname='Account')]
        $User, 
        [Parameter(Mandatory, ParameterSetname='Account')]
        $Token, 
        [Parameter(Mandatory, ParameterSetname='Session')]
        $Session, 
        [Parameter(Mandatory=$true)]$Project)

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

     $Result = Invoke-VstsEndpoint -Session $Session -Project $Project -Path 'git/repositories' -QueryStringParameters @{depth=1}
     $Result.Value              
}

function Get-VstsCodePolicy {
    <#
        .SYNOPSIS
            Get code policies for the specified project.
    #>


    param(
        [Parameter(Mandatory, ParameterSetname='Account')]
        $AccountName, 
        [Parameter(Mandatory, ParameterSetname='Account')]
        $User, 
        [Parameter(Mandatory, ParameterSetname='Account')]
        $Token, 
        [Parameter(Mandatory, ParameterSetname='Session')]
        $Session, 
        [Parameter(Mandatory=$true)]$Project)

        
    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }
              
     $Result = Invoke-VstsEndpoint -Session $Session -Project $Project -Path 'policy/configurations' -ApiVersion '2.0-preview.1'
     $Result.Value     
}

function New-VstsCodePolicy {
    <#
        .SYNOPSIS
            Creates a new Code Policy configuration for the specified project.
    #>


    param(
        [Parameter(Mandatory, ParameterSetname='Account')]
        $AccountName, 
        [Parameter(Mandatory, ParameterSetname='Account')]
        $User, 
        [Parameter(Mandatory, ParameterSetname='Account')]
        $Token, 
        [Parameter(Mandatory, ParameterSetname='Session')]
        $Session, 
        [Parameter(Mandatory=$true)]
        $Project,
        [Guid]
        $RepositoryId = [Guid]::Empty,
        [int]
        $MinimumReviewers,
        [string[]]
        $Branches)

    $RepoId = $null
    if ($RepositoryId -ne [Guid]::Empty)
    {
        $RepoId = $RepositoryId.ToString()   
    }

    $scopes = foreach($branch in $Branches)
    {
        @{
            repositoryId = $RepoId
            refName = "refs/heads/$branch"
            matchKind = "exact"
        }
    }

    $Policy = @{
        isEnabled = $true
        isBlocking = $false
        type = @{
            id = 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd'
        }
        settings = @{
            minimumApproverCount = $MinimumReviewers
            creatorVoteCounts = $false
            scope = @($scopes)
        }
    } | ConvertTo-Json -Depth 10

    if ($PSCmdlet.ParameterSetName -eq 'Account')
    {
        $Session = New-VSTSSession -AccountName $AccountName -User $User -Token $Token
    }

    Invoke-VstsEndpoint -Session $Session -Project $Project -ApiVersion '2.0-preview.1' -Body $Policy -Method POST
}

function Get-VstsProcess {
    <#
        .SYNOPSIS
            Gets team project processes.
    #>


    param(
        [Parameter(Mandatory)]
        $Session)

     $Result = Invoke-VstsEndpoint -Session $Session -Path 'process/processes'
     $Result.Value     
}

function Get-VstsBuild {
    <#
        .SYNOPSIS
            Gets team project builds.
    #>


    param(
        [Parameter(Mandatory)]
        $Session,
        [Parameter(Mandatory)]
        $Project)

     $Result = Invoke-VstsEndpoint -Session $Session -Path 'build/builds' -Project $Project -ApiVersion '2.0'
     $Result.Value     
}

function Get-VstsBuildDefinition {
    <#
        .SYNOPSIS
            Gets team project build definitions.
    #>


    param(
        [Parameter(Mandatory)]
        $Session,
        [Parameter(Mandatory)]
        $Project)

     $Result = Invoke-VstsEndpoint -Session $Session -Path 'build/definitions' -Project $Project -ApiVersion '2.0'
     $Result.Value     
}

function Test-Guid {
    param([Parameter(Mandatory)]$Input)

    $Guid = [Guid]::Empty
    [Guid]::TryParse($Input, [ref]$Guid)
}

function New-VstsBuildDefinition {
    <#
        .SYNOPSIS
            Gets build definitions for the specified project.
    #>


    param(
        [Parameter(Mandatory)]
        $Session,
        [Parameter(Mandatory=$true)]
        $Project,
        [Parameter(Mandatory=$true)]
        $Name,
        [Parameter()]
        $DisplayName = $Name,
        [Parameter()]
        $Comment,
        [Parameter(Mandatory=$true)]
        $Queue,
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Repository 
    )

    if (-not (Test-Guid -Input $Queue))
    {
        $Queue = Get-VstsBuildQueue -Session $Session | Where Name -EQ $Queue | Select -ExpandProperty Id
    }

    $Body = @{
      name =  $Name
      type = "build"
      quality = "definition"
      queue = @{
        id = $Queue
      }
      build = @(
        @{
          enabled = $true
          continueOnError = $false
          alwaysRun = $false
          displayName = $DisplayName
          task = @{
            id = "71a9a2d3-a98a-4caa-96ab-affca411ecda"
            versionSpec = "*"
          }
          inputs = @{
            "solution" = "**\\*.sln"
            "msbuildArgs" = ""
            "platform" = '$(platform)'
            "configuration"= '$(config)'
            "clean" = "false"
            "restoreNugetPackages" = "true"
            "vsLocationMethod" = "version"
            "vsVersion" = "latest"
            "vsLocation" =  ""
            "msbuildLocationMethod" = "version"
            "msbuildVersion" = "latest" 
            "msbuildArchitecture" = "x86"
            "msbuildLocation" = ""
            "logProjectEvents" = "true"
          }
        },
        @{
          "enabled" = $true
          "continueOnError" = $false
          "alwaysRun" = $false
          "displayName" = "Test Assemblies **\\*test*.dll;-:**\\obj\\**"
          "task" = @{
            "id" = "ef087383-ee5e-42c7-9a53-ab56c98420f9"
            "versionSpec" = "*"
          }
          "inputs" = @{
            "testAssembly" = "**\\*test*.dll;-:**\\obj\\**"
            "testFiltercriteria" = ""
            "runSettingsFile" = ""
            "codeCoverageEnabled" = "true"
            "otherConsoleOptions" = ""
            "vsTestVersion" = "14.0"
            "pathtoCustomTestAdapters" = ""
          }
        }
      )
      "repository" = @{
        "id" = $Repository.Id
        "type" = "tfsgit"
        "name" = $Repository.Name
        "localPath" = "`$(sys.sourceFolder)/$($Repository.Name)"
        "defaultBranch" ="refs/heads/master"
        "url" = $Repository.Url
        "clean" = "false"
      }
      "options" = @(
        @{
          "enabled" = $true
          "definition" = @{
            "id" = "7c555368-ca64-4199-add6-9ebaf0b0137d"
          }
          "inputs" = @{
            "parallel" = "false"
            "multipliers" = @("config","platform")
          }
        }
      )
      "variables" = @{
        "forceClean" = @{
          "value" = "false"
          "allowOverride" = $true
        }
        "config" =  @{
          "value" = "debug, release"
          "allowOverride" = $true
        }
        "platform" = @{
          "value" = "any cpu"
          "allowOverride" = $true
        }
      }
      "triggers" = @()
      "comment" = $Comment
    } | ConvertTo-Json -Depth 20

    Invoke-VstsEndpoint -Session $Session -Path 'build/definitions' -ApiVersion 2.0 -Method POST -Body $Body -Project $Project
}

function Get-VstsBuildQueue {
    <#
        .SYNOPSIS
            Gets build definitions for the collection.
    #>


    param(
        [Parameter(Mandatory)]
        $Session
        )

     $Result = Invoke-VstsEndpoint -Session $Session -Path 'build/queues' -ApiVersion 2.0
     $Result.Value   
}

function ConvertTo-VstsGitRepository {
    <#
        .SYNOPSIS
            Converts a TFVC repository to a VSTS Git repository.
    #>

    param(
        [Parameter(Mandatory)]$Session,
        [Parameter(Mandatory)]$TargetName, 
        [Parameter(Mandatory)]$SourceFolder, 
        [Parameter(Mandatory)]$ProjectName)

    $GitCommand = Get-Command git 
    if ($GitCommand -eq $null -or $GitCommand.CommandType -ne 'Application' -or $GitCommand.Name -ne 'git.exe')
    {
        throw "Git-tfs needs to be installed to use this command. See https://github.com/git-tfs/git-tfs. You can install with Chocolatey: cinst gittfs"
    }

    $GitTfsCommand = Get-Command git-tfs 
    if ($GitTfsCommand -eq $null -or $GitTfsCommand.CommandType -ne 'Application' -or $GitTfsCommand.Name -ne 'git-tfs.exe')
    {
        throw "Git-tfs needs to be installed to use this command. See https://github.com/git-tfs/git-tfs. You can install with Chocolatey: cinst gittfs"
    }

    git tfs clone "https://$($Session.AccountName).visualstudio.com/defaultcollection" "$/$ProjectName/$SourceFolder" --branches=none

    Push-Location (Split-Path $SourceFolder -Leaf)

    New-VstsGitRepository -Session $Session -RepositoryName $TargetName -Project $ProjectName | Out-Null

    git checkout -b develop
    git remote add origin https://$($Session.AccountName).visualstudio.com/DefaultCollection/$ProjectName/_git/$TargetName
    git push --all origin
    git tfs cleanup

    Pop-Location
    Remove-Item (Split-Path $SourceFolder -Leaf) -Force
}