parameters:
  - name: Paths
    type: object
    default: []
  - name: Repositories
    type: object
    default:
      - Name: $(Build.Repository.Name)
        Commitish: $(Build.SourceVersion)
        WorkingDirectory: $(System.DefaultWorkingDirectory)
  - name: SkipCheckoutNone
    type: boolean
    default: false
  - name: TokenToUseForAuth
    type: string
    default: ''
  - name: PreserveAuthToken
    type: boolean
    default: false

steps:
  - ${{ if not(parameters.SkipCheckoutNone) }}:
      - checkout: none

  - ${{ if ne(parameters.TokenToUseForAuth, '') }}:
    - pwsh: |
        $base64Token = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("nobody:${{ parameters.TokenToUseForAuth }}"))
        Write-Host "##vso[task.setvariable variable=_base64AuthToken;issecret=true;]$base64Token"
        git config set --global "http.extraheader" "AUTHORIZATION: basic $base64Token"
      displayName: Setup git config auth header

  - task: PowerShell@2
    ${{ if eq(length(parameters.Repositories), 1) }}:
      displayName: 'Sparse checkout ${{ parameters.Repositories[0].Name }}'
    ${{ else }}:
      displayName: 'Sparse checkout repositories'
    inputs:
      targetType: inline
      # Define this inline, because of the chicken/egg problem with loading a script when nothing
      # has been checked out yet.
      script: |
        # Setting $PSNativeCommandArgumentPassing to 'Legacy' to use PowerShell
        # 7.2 behavior for command argument passing. Newer behaviors will result
        # in errors from git.exe.
        $PSNativeCommandArgumentPassing = 'Legacy'

        function Retry()
        {
            Run 3 @args
        }

        function Run()
        {
            $retries, $command, $arguments = $args
            if ($retries -isnot [int]) {
                $command, $arguments = $args
                $retries = 0
            }
            Write-Host "==>" $command $arguments
            $attempt = 0
            $sleep = 5

            while ($true) {
                $attempt++
                & $command $arguments
                if (!$LASTEXITCODE) { return }
                if ($attempt -gt $retries) { exit $LASTEXITCODE }
                Write-Warning "Attempt $attempt failed: $_. Trying again in $sleep seconds..."
                Start-Sleep -Seconds $sleep
                $sleep *= 2
            }
        }

        function SparseCheckout([Array]$paths, [Hashtable]$repository)
        {
            $dir = $repository.WorkingDirectory
            if (!$dir) {
              $dir = "./$($repository.Name)"
            }
            New-Item $dir -ItemType Directory -Force | Out-Null
            Push-Location $dir

            if (Test-Path .git/info/sparse-checkout) {
              $hasInitialized = $true
              Write-Host "Repository $($repository.Name) has already been initialized in $pwd. Skipping this step."
            } else {
              Write-Host "Repository $($repository.Name) is being initialized in $pwd"

              if ($repository.Commitish -match '^refs/pull/\d+/merge$') {
                Retry git clone --no-checkout --filter=tree:0 -c remote.origin.fetch=''+$($repository.Commitish):refs/remotes/origin/$($repository.Commitish)'' https://github.com/$($repository.Name) .
              } else {
                Retry git clone --no-checkout --filter=tree:0 https://github.com/$($repository.Name) .
              }

              # Turn off git GC for sparse checkout. Note: The devops checkout task does this by default
              Run git config gc.auto 0

              Run git sparse-checkout init

              # Set non-cone mode otherwise path filters will not work in git >= 2.37.0
              # See https://github.blog/2022-06-27-highlights-from-git-2-37/#tidbits
              # '/*' '!/*/' -> only checkout files in top level directory
              # '/eng' -> checkout required eng/ scripts/configs
              # '.config' -> required for files like .config/1espt/PipelineAutobaseliningConfig.yml and .config/guardian/.gdnbaselines used by 1es PT scripts
              git sparse-checkout set --no-cone '/*' '!/*/' '/eng' '/.config'
            }

            # Prevent wildcard expansion in Invoke-Expression (e.g. for checkout path '/*')
            $quotedPaths = $paths | ForEach-Object { "'$_'" }
            $gitsparsecmd = "git sparse-checkout add $quotedPaths"
            Write-Host $gitsparsecmd
            Invoke-Expression -Command $gitsparsecmd

            Write-Host "Set sparse checkout paths to:"
            Get-Content .git/info/sparse-checkout

            # sparse-checkout commands after initial checkout will auto-checkout again
            if (!$hasInitialized) {
              # Remove refs/heads/ prefix from branch names
              $commitish = $repository.Commitish -replace '^refs/heads/', ''

              # use -- to prevent git from interpreting the commitish as a path
              # This will use the default branch if repo.Commitish is empty
              Retry git -c advice.detachedHead=false checkout $commitish --
            } else {
              Write-Host "Skipping checkout as repo has already been initialized"
            }

            Pop-Location
        }

        # Paths may be sourced as a yaml object literal OR a dynamically generated variable json string.
        # If the latter, convertToJson will wrap the 'string' in quotes, so remove them.
        $paths = '${{ convertToJson(parameters.Paths) }}'.Trim('"') | ConvertFrom-Json
        # Replace windows backslash paths, as Azure Pipelines default directories are sometimes formatted like 'D:\a\1\s'
        $repositories = '${{ convertToJson(parameters.Repositories) }}' -replace '\\', '/' | ConvertFrom-Json -AsHashtable
        foreach ($repo in $Repositories) {
          SparseCheckout $paths $repo
        }
      pwsh: true
      workingDirectory: $(System.DefaultWorkingDirectory)

  - ${{ if and(ne(parameters.TokenToUseForAuth, ''), not(parameters.PreserveAuthToken)) }}:
    - pwsh: |
        git config unset --global "http.extraheader"
      displayName: Removing git config auth header
      condition: always()
